Here I’, posting the current server script we use to perform the modifications on the Journal Entry produced by the Payroll Module before it is posted.
Please excuse the Spanglish and some of the redactions.
While it is far from perfect, it helps in our use case.
This is only meant as a reference, and should not be used with proper understanding of what is doing as it is heavily reliant in our specific implementation.
"""
GL Entry
Mauricio Vidal
Version 2025-01-23
--------------------------------------------------------------------------------
Propósito:
--------------------------------------------------------------------------------
Modificar el `Journal Entry` que genera el `Payroll Entry` antes de que sea
Submitted, para asegurar que los campos tengan el contenido esperado en la
implementación específica de ERPNext para *** que en general
consiste en:
1. Exclusivamente para los Journals generados por Payroll
1.1. Cada uno de sus registros deben identificar al empleado/eps/caja/etc
para que las cuentas por pagar puedan identificar al causante de ello.
1.2. El Journal identifica al tercero que corresponde según la definición
formal de la DIAN.
--------------------------------------------------------------------------------
Implementación:
--------------------------------------------------------------------------------
Este script debe estar guardado en la sección de ERPNext `Server Script` en un
registro con los siguientes datos:
`Name`: `Journal Entry digest` (O cualquier otro descriptivo)
`Script Type`: `Doctype Event`
`Reference Document Type`: `Journal Entry`
`DocType Event`: `Before Validate`
--------------------------------------------------------------------------------
Requerimientos:
--------------------------------------------------------------------------------
- La tabla `DIAN terceros` definida.
- Campos adicionales en `Account` para establecer al tipo y tipo de tercero
(este último en caso que sea necesario) a los registros de tipo `Payable`.
- Campos adicionales en `Cost Center`.
- La existencia del tipo de documento `Business Unit` asociado a `Dimensions`.
--------------------------------------------------------------------------------
Limitaciones:
--------------------------------------------------------------------------------
- Solo funciona para un empleado por documento generado por el Payroll.
Requerimos que sea así para asegurar que corresponda al documento electrónico
transmitido a DIAN.
"""
VA_ENABLE_CONSOLE_DEBUG = False
VA_DOCTYPE_ACCOUNT_FIELD_REQUIRED_PARTY_TYPE = 'custom_required_party_type'
VA_DOCTYPE_ACCOUNT_FIELD_REQUIRED_PARTY = 'custom_required_party'
VA_DOCTYPE_COST_CENTER_FIELD_ASSOCIATED_BUSINESS_UNIT = 'custom_associated_business_unit'
VA_DOCTYPE_GL_ENTRY_FIELD_BUSINESS_UNIT = 'business_unit'
VA_DOCTYPE_DIAN_TERCEROS_NAME = 'DIAN terceros'
VA_DOCTYPE_EMPLOYEE_FIELD_DIAN_TERCERO = 'custom_dian_tercero'
VA_DOCTYPE_JOURNAL_FIELD_DIAN_TERCERO = 'custom_dian_tercero'
# Al parecer los objetos de los campos en ERPNext no son siempre None cuando no
# están asignados, por lo que acá hacemos explícito el valor None como cadena.
# Lo asignamos a `doc` porque por alguna razón, no estaría disponible a otros
# objetos dentro de este script.
doc.VA_DEFINE_NONE = "None"
def aux_decide_value(field, value_if_none, keep_original: bool = False):
"""
This auxiliar function helps to deal with the ambiguous None, Empty strings.
Returns:
- The value of the `field` if it is not None or ''.
- The string from `value_if_none`, otherwise.
"""
if field is None or field == '':
return value_if_none
if keep_original:
return field
return str(field)
# Workaround para hacer disponible la función anterior a otros objetos
doc.va_aux_decide_value = aux_decide_value
def aux_set_required_value_to_field_on_register(
doc,
register,
current_value,
value_required_by_specification,
field_name,
):
"""
This auxiliar function helps to set a value for a field.
doc: The current document.
register: all the fields on the doc.
current_value: The variable that is going to be updated if necessary.
value_required_by_specification: The variable that holds the value that
is the required according to the table of the specification.
field_name: The string that identifies the field to be updated on the
document.
Returns:
- The updated current value according to the preference on the
specification.
"""
# The value is only updated, if that is defined on the specification
if value_required_by_specification != doc.VA_DEFINE_NONE:
register.set(field_name, value_required_by_specification)
# Vuelve a cargar la variable modificada
return doc.va_aux_decide_value(register.get(field_name), doc.VA_DEFINE_NONE)
else:
return current_value
# Workaround para hacer disponible la función anterior a otros objetos
doc.va_aux_set_required_value_to_field_on_register = aux_set_required_value_to_field_on_register
"""
Here we check if this Journal Entry is generated by the Payroll module to
perform the desired changes on its records.
The mechanism implemented is based on the assumption that is any of the
Journal records has a reference to a `Payroll Entry` document, then it is
considered produced by the Payroll module.
"""
# Flag with the result
has_any_payroll_entry = False
more_than_one_employee_found = False
# We will also determine the employee and tercero IDs.
current_employee_id = None
current_dian_tercero = None
# Obtenemos todos los registros contables en el Journal
got_current_accounting_records = doc.va_aux_decide_value(doc.get('accounts'), doc.VA_DEFINE_NONE, keep_original=True)
for i in got_current_accounting_records:
# Check if the record has been marked by the Payroll.
if i.get('reference_type') == 'Payroll Entry':
# It means this whole Journal is generated by the Payroll module
has_any_payroll_entry = True
# This record should also have the party defined as Employee
current_employee_type = i.get('party_type')
if current_employee_type == 'Employee':
if current_employee_id is not None:
more_than_one_employee_found = True
# Get the Employee ID according to the Payroll module
current_employee_id = i.get('party')
# Now, determine the DIAN Tercer assigned to that Employee
current_dian_tercero = frappe.db.get_value('Employee', current_employee_id, VA_DOCTYPE_EMPLOYEE_FIELD_DIAN_TERCERO)
else:
frappe.throw("Unexpected party type for the Journal register that contains the reference to the Payroll Entry: '" + current_employee_type + "'. Please contact our support TEAM")
if more_than_one_employee_found:
frappe.throw("The Journal will contain data for more than one employee, which is not currently implemented. Please contact our support TEAM.")
"""
Update of the Journal.
Only if it is produced by the Payroll module.
"""
if has_any_payroll_entry is True:
# Asignamos el tercero según el registro de la DIAN para el empleado.
doc.set(VA_DOCTYPE_JOURNAL_FIELD_DIAN_TERCERO, current_dian_tercero)
# Recorremos las cuentas contables para modificar lo pertinente
for i in got_current_accounting_records:
"""
Primero nos concentramos en establecer el valor de los campos del
registro actual y los valores según las especificaciones asociadas.
"""
# Por claridad, definimos el contenido de lo que tenemos actualmente
got_current_voucher_type = doc.va_aux_decide_value(i.get('voucher_type'), doc.VA_DEFINE_NONE)
got_current_account = doc.va_aux_decide_value(i.get('account'), doc.VA_DEFINE_NONE)
got_current_party_type = doc.va_aux_decide_value(i.get('party_type'), doc.VA_DEFINE_NONE)
got_current_party = doc.va_aux_decide_value(i.get('party'), doc.VA_DEFINE_NONE)
got_current_cost_center = doc.va_aux_decide_value(i.get('cost_center'), doc.VA_DEFINE_NONE)
got_current_business_unit = doc.va_aux_decide_value(i.get(VA_DOCTYPE_GL_ENTRY_FIELD_BUSINESS_UNIT), doc.VA_DEFINE_NONE)
got_current_project = doc.va_aux_decide_value(i.get('project'), doc.VA_DEFINE_NONE)
got_current_reference_type = doc.va_aux_decide_value(i.get('reference_type'), doc.VA_DEFINE_NONE)
# Solo para debug
if VA_ENABLE_CONSOLE_DEBUG:
frappe.throw("En el documento, Type: '" + got_current_voucher_type + "' Account: '" + got_current_account + "' Party Type: '" + got_current_party_type + "' Party: '" + got_current_party + "' Cost Center: '" + got_current_cost_center + "' Business Unit: '" + got_current_business_unit + "'Project: " + got_current_project + "' Reference Type: '" + got_current_reference_type + "'")
# Obtenemos las definiciones que nos interesan evaluar para la cuenta actual.
got_from_account_definition = frappe.db.get_value('Account', got_current_account, [VA_DOCTYPE_ACCOUNT_FIELD_REQUIRED_PARTY_TYPE, VA_DOCTYPE_ACCOUNT_FIELD_REQUIRED_PARTY, 'root_type', 'account_type'] )
got_party_type_from_account_definition = doc.va_aux_decide_value(got_from_account_definition[0], doc.VA_DEFINE_NONE)
got_party_from_account_definition = doc.va_aux_decide_value(got_from_account_definition[1], doc.VA_DEFINE_NONE)
got_account_root_type_from_account_definition = doc.va_aux_decide_value(got_from_account_definition[2], doc.VA_DEFINE_NONE)
got_account_type_from_account_definition = doc.va_aux_decide_value(got_from_account_definition[3], doc.VA_DEFINE_NONE)
# Obtenemos las definiciones que nos interesan evaluar para el centro de costo
# actual.
got_from_cost_center_definition = frappe.db.get_value('Cost Center', got_current_cost_center, VA_DOCTYPE_COST_CENTER_FIELD_ASSOCIATED_BUSINESS_UNIT )
got_business_unit_from_cost_center_definition = doc.va_aux_decide_value(got_from_cost_center_definition, doc.VA_DEFINE_NONE)
# Obtenemos las definiciones que nos interesan evaluar para el proyecto actual.
got_from_project_definition = frappe.db.get_value('Project', got_current_project, 'cost_center' )
got_cost_center_from_project_definition = doc.va_aux_decide_value(got_from_project_definition, doc.VA_DEFINE_NONE)
"""
Todas los registros contables deben tener una unidad de negocio
asignada según la definición del centro de costo.
"""
# La unidad de negocio, si no está registrada pero sí lo está el centro de costo, se toma de la definición para el centro de costo
if got_current_business_unit == doc.VA_DEFINE_NONE and got_current_cost_center != doc.VA_DEFINE_NONE:
got_current_business_unit = doc.va_aux_set_required_value_to_field_on_register(doc, i, got_current_business_unit, got_business_unit_from_cost_center_definition, VA_DOCTYPE_GL_ENTRY_FIELD_BUSINESS_UNIT)
"""
Queremos evitar cambios que no sean los estrictamente esperados.
Por ello filtramos para establecer los casos en que deben tratarse.
"""
# Solo nos interesan las cuentas por pagar
if got_account_root_type_from_account_definition == 'Liability':
# Solo si el tipo de tercero ni el tercero están especificados actualmente
if got_current_party_type == doc.VA_DEFINE_NONE and got_current_party == doc.VA_DEFINE_NONE:
# Establece el tipo de tercero según la definición
got_current_party_type = doc.va_aux_set_required_value_to_field_on_register(doc, i, got_current_party_type, got_party_type_from_account_definition, 'party_type')
# Para el tipo empleado, establece el empleado
if got_party_type_from_account_definition == 'Employee':
got_current_party = doc.va_aux_set_required_value_to_field_on_register(doc, i, got_current_party, current_employee_id, 'party')
# O establecemos el proveedor si es lo solicitado por la definición según su tipo Supplier
elif got_party_type_from_account_definition == 'Supplier':
got_current_party = doc.va_aux_set_required_value_to_field_on_register(doc, i, got_current_party, got_party_from_account_definition, 'party')
# Solo para debug.
# Sirve para que ERPNext reporte un error (en el recuadro de error cerca al botón del Payroll Entry que genera los Salary Slip) mostrando en detalle el estado de este punto.
if VA_ENABLE_CONSOLE_DEBUG:
assert False