coati-payroll 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coati-payroll might be problematic. Click here for more details.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,173 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Employee processing service for building calculation variables."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+ from typing import Any
21
+
22
+ from coati_payroll.model import Empleado, Planilla, AcumuladoAnual
23
+ from ..domain.employee_calculation import EmpleadoCalculo
24
+ from ..repositories.acumulado_repository import AcumuladoRepository
25
+ from ..repositories.config_repository import ConfigRepository
26
+
27
+
28
+ class EmployeeProcessingService:
29
+ """Service for processing employee calculations and building variables."""
30
+
31
+ def __init__(
32
+ self,
33
+ config_repository: ConfigRepository,
34
+ acumulado_repository: AcumuladoRepository,
35
+ ):
36
+ self.config_repo = config_repository
37
+ self.acumulado_repo = acumulado_repository
38
+
39
+ def build_calculation_variables(
40
+ self,
41
+ emp_calculo: EmpleadoCalculo,
42
+ planilla: Planilla,
43
+ periodo_inicio: date,
44
+ periodo_fin: date,
45
+ fecha_calculo: date,
46
+ ) -> dict[str, Any]:
47
+ """Build the calculation variables for an employee."""
48
+ empleado = emp_calculo.empleado
49
+ tipo_planilla = planilla.tipo_planilla
50
+
51
+ config = self.config_repo.get_for_empresa(planilla.empresa_id)
52
+
53
+ # Calculate days in period
54
+ dias_periodo = (periodo_fin - periodo_inicio).days + 1
55
+
56
+ # Calculate seniority using configuration
57
+ fecha_alta = empleado.fecha_alta or date.today()
58
+ antiguedad_dias = (fecha_calculo - fecha_alta).days
59
+ antiguedad_meses = antiguedad_dias // config.dias_mes_antiguedad
60
+ antiguedad_anios = antiguedad_dias // config.dias_anio_antiguedad
61
+
62
+ # Calculate remaining months in fiscal year
63
+ mes_inicio_fiscal = tipo_planilla.mes_inicio_fiscal if tipo_planilla else 1
64
+ meses_restantes = config.meses_anio_financiero - fecha_calculo.month + mes_inicio_fiscal
65
+ if meses_restantes > config.meses_anio_financiero:
66
+ meses_restantes -= config.meses_anio_financiero
67
+ if meses_restantes <= 0:
68
+ meses_restantes = 1
69
+
70
+ # Build variables dictionary
71
+ variables = {
72
+ # Employee base data
73
+ "salario_base": emp_calculo.salario_base,
74
+ "salario_mensual": emp_calculo.salario_mensual,
75
+ "tipo_cambio": emp_calculo.tipo_cambio,
76
+ # Period data
77
+ "fecha_calculo": fecha_calculo,
78
+ "periodo_inicio": periodo_inicio,
79
+ "periodo_fin": periodo_fin,
80
+ "dias_periodo": Decimal(str(dias_periodo)),
81
+ # Seniority
82
+ "fecha_alta": fecha_alta,
83
+ "antiguedad_dias": Decimal(str(antiguedad_dias)),
84
+ "antiguedad_meses": Decimal(str(antiguedad_meses)),
85
+ "antiguedad_anios": Decimal(str(antiguedad_anios)),
86
+ # Fiscal calculations
87
+ "meses_restantes": Decimal(str(meses_restantes)),
88
+ "periodos_por_anio": Decimal(
89
+ str(tipo_planilla.periodos_por_anio if tipo_planilla else config.meses_anio_financiero)
90
+ ),
91
+ # Accumulated values (will be populated from AcumuladoAnual)
92
+ "salario_acumulado": Decimal("0.00"),
93
+ "impuesto_acumulado": Decimal("0.00"),
94
+ "ir_retenido_acumulado": Decimal("0.00"),
95
+ "salario_acumulado_mes": Decimal("0.00"),
96
+ }
97
+
98
+ # Add employee implementation initial values
99
+ if empleado.salario_acumulado:
100
+ variables["salario_acumulado"] = Decimal(str(empleado.salario_acumulado))
101
+ if empleado.impuesto_acumulado:
102
+ variables["impuesto_acumulado"] = Decimal(str(empleado.impuesto_acumulado))
103
+ variables["ir_retenido_acumulado"] = Decimal(str(empleado.impuesto_acumulado))
104
+
105
+ # Add novelties
106
+ for codigo, valor in emp_calculo.novedades.items():
107
+ variables[f"novedad_{codigo}"] = valor
108
+
109
+ # Load accumulated annual values
110
+ acumulado = self._get_acumulado_anual(empleado, planilla, fecha_calculo)
111
+ if acumulado:
112
+ variables["salario_acumulado"] += Decimal(str(acumulado.salario_bruto_acumulado or 0))
113
+ variables["impuesto_acumulado"] += Decimal(str(acumulado.impuesto_retenido_acumulado or 0))
114
+ variables["ir_retenido_acumulado"] += Decimal(str(acumulado.impuesto_retenido_acumulado or 0))
115
+ variables["salario_acumulado_mes"] = Decimal(str(acumulado.salario_acumulado_mes or 0))
116
+
117
+ # Additional accumulated values for progressive tax calculations
118
+ variables["salario_bruto_acumulado"] = Decimal(str(acumulado.salario_bruto_acumulado or 0))
119
+ variables["salario_gravable_acumulado"] = Decimal(str(acumulado.salario_gravable_acumulado or 0))
120
+ variables["deducciones_antes_impuesto_acumulado"] = Decimal(
121
+ str(acumulado.deducciones_antes_impuesto_acumulado or 0)
122
+ )
123
+ variables["periodos_procesados"] = Decimal(str(acumulado.periodos_procesados or 0))
124
+ variables["meses_trabajados"] = Decimal(str(acumulado.periodos_procesados or 0))
125
+
126
+ # Calculate net accumulated salary
127
+ variables["salario_neto_acumulado"] = Decimal(str(acumulado.salario_bruto_acumulado or 0)) - Decimal(
128
+ str(acumulado.deducciones_antes_impuesto_acumulado or 0)
129
+ )
130
+
131
+ # Include initial accumulated values from employee
132
+ variables["salario_inicial_acumulado"] = Decimal(str(empleado.salario_acumulado or 0))
133
+ variables["impuesto_inicial_acumulado"] = Decimal(str(empleado.impuesto_acumulado or 0))
134
+
135
+ return variables
136
+
137
+ def _get_acumulado_anual(
138
+ self, empleado: Empleado, planilla: Planilla, fecha_calculo: date
139
+ ) -> AcumuladoAnual | None:
140
+ """Get accumulated annual values for employee."""
141
+ if not planilla.tipo_planilla:
142
+ return None
143
+
144
+ tipo_planilla = planilla.tipo_planilla
145
+
146
+ # Calculate fiscal period
147
+ anio = fecha_calculo.year
148
+ mes_inicio = tipo_planilla.mes_inicio_fiscal
149
+ dia_inicio = tipo_planilla.dia_inicio_fiscal
150
+
151
+ if fecha_calculo.month < mes_inicio:
152
+ anio -= 1
153
+
154
+ periodo_fiscal_inicio = date(anio, mes_inicio, dia_inicio)
155
+
156
+ # Look up existing accumulated record
157
+ from sqlalchemy import select
158
+ from coati_payroll.model import db
159
+
160
+ acumulado = (
161
+ db.session.execute(
162
+ select(AcumuladoAnual).filter(
163
+ AcumuladoAnual.empleado_id == empleado.id,
164
+ AcumuladoAnual.tipo_planilla_id == tipo_planilla.id,
165
+ AcumuladoAnual.empresa_id == planilla.empresa_id,
166
+ AcumuladoAnual.periodo_fiscal_inicio == periodo_fiscal_inicio,
167
+ )
168
+ )
169
+ .unique()
170
+ .scalar_one_or_none()
171
+ )
172
+
173
+ return acumulado
@@ -0,0 +1,374 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Payroll execution service - main business logic orchestrator."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date, datetime, timezone
19
+ from decimal import Decimal, ROUND_HALF_UP
20
+ from typing import Any
21
+
22
+ from coati_payroll.model import db, Planilla, Empleado, Nomina
23
+ from coati_payroll.enums import NominaEstado
24
+ from coati_payroll.formula_engine import FormulaEngineError
25
+ from ..domain.employee_calculation import EmpleadoCalculo
26
+ from ..repositories.planilla_repository import PlanillaRepository
27
+ from ..repositories.config_repository import ConfigRepository
28
+ from ..repositories.exchange_rate_repository import ExchangeRateRepository
29
+ from ..repositories.novelty_repository import NoveltyRepository
30
+ from ..repositories.acumulado_repository import AcumuladoRepository
31
+ from ..validators.planilla_validator import PlanillaValidator
32
+ from ..validators.employee_validator import EmployeeValidator
33
+ from ..validators import ValidationError, NominaEngineError
34
+ from ..calculators.salary_calculator import SalaryCalculator
35
+ from ..calculators.exchange_rate_calculator import ExchangeRateCalculator
36
+ from ..calculators.concept_calculator import ConceptCalculator
37
+ from ..calculators.perception_calculator import PerceptionCalculator
38
+ from ..calculators.deduction_calculator import DeductionCalculator
39
+ from ..calculators.benefit_calculator import BenefitCalculator
40
+ from ..processors.loan_processor import LoanProcessor
41
+ from ..processors.accumulation_processor import AccumulationProcessor
42
+ from ..processors.vacation_processor import VacationProcessor
43
+ from ..processors.novelty_processor import NoveltyProcessor
44
+ from ..processors.accounting_processor import AccountingProcessor
45
+ from ..services.employee_processing_service import EmployeeProcessingService
46
+ from ..services.snapshot_service import SnapshotService
47
+ from ..services.accounting_voucher_service import AccountingVoucherService
48
+
49
+
50
+ class PayrollExecutionService:
51
+ """Main service for executing payroll runs."""
52
+
53
+ def __init__(self, session):
54
+ self.session = session
55
+
56
+ # Initialize repositories
57
+ self.planilla_repo = PlanillaRepository(session)
58
+ self.config_repo = ConfigRepository(session)
59
+ self.exchange_rate_repo = ExchangeRateRepository(session)
60
+ self.novelty_repo = NoveltyRepository(session)
61
+ self.acumulado_repo = AcumuladoRepository(session)
62
+
63
+ # Initialize validators
64
+ self.planilla_validator = PlanillaValidator(self.planilla_repo)
65
+ self.employee_validator = EmployeeValidator()
66
+
67
+ # Initialize calculators (warnings list will be set later)
68
+ self.salary_calculator = SalaryCalculator(self.config_repo)
69
+ self.exchange_rate_calculator = ExchangeRateCalculator(self.exchange_rate_repo)
70
+ self.concept_calculator = ConceptCalculator(self.config_repo, []) # warnings set in execute_payroll
71
+ self.perception_calculator = PerceptionCalculator(self.concept_calculator)
72
+ self.deduction_calculator = DeductionCalculator(self.concept_calculator, []) # warnings set in execute_payroll
73
+ self.benefit_calculator = BenefitCalculator(self.concept_calculator)
74
+
75
+ # Initialize processors
76
+ self.novelty_processor = NoveltyProcessor(self.novelty_repo)
77
+ self.accumulation_processor = AccumulationProcessor(self.acumulado_repo)
78
+ self.accounting_processor = AccountingProcessor()
79
+
80
+ # Initialize services
81
+ self.employee_processing_service = EmployeeProcessingService(self.config_repo, self.acumulado_repo)
82
+ self.snapshot_service = SnapshotService(session)
83
+ self.accounting_voucher_service = AccountingVoucherService(session)
84
+
85
+ def execute_payroll(
86
+ self,
87
+ planilla: Planilla,
88
+ periodo_inicio: date,
89
+ periodo_fin: date,
90
+ fecha_calculo: date,
91
+ usuario: str | None,
92
+ ) -> tuple[Nomina | None, list[EmpleadoCalculo], list[str], list[str]]:
93
+ """Execute a complete payroll run."""
94
+ errors: list[str] = []
95
+ warnings: list[str] = []
96
+
97
+ # Update warnings list for calculators (they need shared reference)
98
+ self.concept_calculator.warnings = warnings
99
+ self.deduction_calculator.warnings = warnings
100
+
101
+ # Validate planilla
102
+ from ..domain.payroll_context import PayrollContext
103
+
104
+ context = PayrollContext(
105
+ planilla_id=planilla.id,
106
+ periodo_inicio=periodo_inicio,
107
+ periodo_fin=periodo_fin,
108
+ fecha_calculo=fecha_calculo,
109
+ usuario=usuario,
110
+ )
111
+
112
+ validation_result = self.planilla_validator.validate(context)
113
+ if not validation_result.is_valid:
114
+ errors.extend(validation_result.errors)
115
+ return None, [], errors, warnings
116
+
117
+ # Capture configuration snapshots for recalculation consistency
118
+ snapshot = self.snapshot_service.capture_complete_snapshot(planilla, fecha_calculo)
119
+
120
+ # Create the Nomina record
121
+ nomina = Nomina(
122
+ planilla_id=planilla.id,
123
+ periodo_inicio=periodo_inicio,
124
+ periodo_fin=periodo_fin,
125
+ generado_por=usuario,
126
+ estado=NominaEstado.GENERADO,
127
+ total_bruto=Decimal("0.00"),
128
+ total_deducciones=Decimal("0.00"),
129
+ total_neto=Decimal("0.00"),
130
+ fecha_calculo_original=fecha_calculo,
131
+ configuracion_snapshot=snapshot["configuracion"],
132
+ tipos_cambio_snapshot=snapshot["tipos_cambio"],
133
+ catalogos_snapshot=snapshot["catalogos"],
134
+ )
135
+ db.session.add(nomina)
136
+ db.session.flush()
137
+
138
+ # Initialize processors that need nomina
139
+ loan_processor = LoanProcessor(nomina, fecha_calculo, periodo_inicio, periodo_fin)
140
+ vacation_processor = VacationProcessor(planilla, periodo_inicio, periodo_fin, usuario, warnings)
141
+
142
+ # Update warnings reference for calculators (shared list)
143
+ self.concept_calculator.warnings = warnings
144
+ self.deduction_calculator.warnings = warnings
145
+
146
+ # Process each employee
147
+ empleados_calculo: list[EmpleadoCalculo] = []
148
+
149
+ for planilla_empleado in planilla.planilla_empleados:
150
+ if not planilla_empleado.activo:
151
+ continue
152
+
153
+ empleado = planilla_empleado.empleado
154
+ if not empleado.activo:
155
+ warnings.append(
156
+ f"Empleado {empleado.primer_nombre} {empleado.primer_apellido} no está activo y será omitido."
157
+ )
158
+ continue
159
+
160
+ try:
161
+ emp_calculo = self._process_employee(
162
+ empleado,
163
+ planilla,
164
+ periodo_inicio,
165
+ periodo_fin,
166
+ fecha_calculo,
167
+ nomina,
168
+ loan_processor,
169
+ vacation_processor,
170
+ )
171
+ empleados_calculo.append(emp_calculo)
172
+ except (NominaEngineError, FormulaEngineError) as e:
173
+ # Capture all payroll engine and formula errors
174
+ errors.append(
175
+ f"Error procesando empleado {empleado.primer_nombre} {empleado.primer_apellido}: {str(e)}"
176
+ )
177
+ except Exception as e:
178
+ # Capture any unexpected error to prevent 500 errors
179
+ errors.append(
180
+ f"Error inesperado procesando empleado {empleado.primer_nombre} {empleado.primer_apellido}: "
181
+ f"{type(e).__name__}: {str(e)}"
182
+ )
183
+
184
+ # Calculate totals
185
+ self._calculate_totals(nomina, empleados_calculo)
186
+
187
+ # Update planilla last execution
188
+ planilla.ultima_ejecucion = datetime.now(timezone.utc)
189
+
190
+ # Save errors and warnings to log_procesamiento for transparency
191
+ self._save_log_entries(nomina, errors, warnings, empleados_calculo)
192
+
193
+ # Generate accounting voucher
194
+ try:
195
+ self.accounting_voucher_service.generate_accounting_voucher(nomina, planilla, fecha_calculo, usuario)
196
+ db.session.flush()
197
+ except Exception as e:
198
+ # Don't fail the payroll if voucher generation fails
199
+ warnings.append(f"Advertencia al generar comprobante contable: {str(e)}")
200
+
201
+ return nomina, empleados_calculo, errors, warnings
202
+
203
+ def _save_log_entries(
204
+ self,
205
+ nomina: Nomina,
206
+ errors: list[str],
207
+ warnings: list[str],
208
+ empleados_calculo: list[EmpleadoCalculo],
209
+ ) -> None:
210
+ """Save errors, warnings and processing info to nomina.log_procesamiento.
211
+
212
+ This ensures all processing issues are visible in the nomina log,
213
+ not just as flash messages.
214
+ """
215
+ log_entries: list[dict[str, Any]] = []
216
+ timestamp = datetime.now(timezone.utc).isoformat()
217
+
218
+ # Log successful employee processing
219
+ for emp_calculo in empleados_calculo:
220
+ empleado = emp_calculo.empleado
221
+ log_entries.append(
222
+ {
223
+ "timestamp": timestamp,
224
+ "empleado": f"{empleado.primer_nombre} {empleado.primer_apellido}",
225
+ "status": "success",
226
+ "message": f"Procesado correctamente. Salario neto: {emp_calculo.salario_neto}",
227
+ }
228
+ )
229
+
230
+ # Log errors
231
+ for error in errors:
232
+ log_entries.append(
233
+ {
234
+ "timestamp": timestamp,
235
+ "empleado": "SISTEMA",
236
+ "status": "error",
237
+ "message": error,
238
+ }
239
+ )
240
+
241
+ # Log warnings
242
+ for warning in warnings:
243
+ log_entries.append(
244
+ {
245
+ "timestamp": timestamp,
246
+ "empleado": "SISTEMA",
247
+ "status": "warning",
248
+ "message": warning,
249
+ }
250
+ )
251
+
252
+ nomina.log_procesamiento = log_entries
253
+
254
+ def _process_employee(
255
+ self,
256
+ empleado: Empleado,
257
+ planilla: Planilla,
258
+ periodo_inicio: date,
259
+ periodo_fin: date,
260
+ fecha_calculo: date,
261
+ nomina: Nomina,
262
+ loan_processor: LoanProcessor,
263
+ vacation_processor: VacationProcessor,
264
+ ) -> EmpleadoCalculo:
265
+ """Process a single employee's payroll."""
266
+ # Validate employee
267
+ employee_validation = self.employee_validator.validate_employee(
268
+ empleado, planilla.empresa_id, periodo_inicio, periodo_fin
269
+ )
270
+ if not employee_validation.is_valid:
271
+ # Include specific validation errors (errors is already a list of strings)
272
+ error_messages = employee_validation.errors
273
+ raise ValidationError(f"Empleado {empleado.codigo_empleado}: {'; '.join(error_messages)}")
274
+
275
+ emp_calculo = EmpleadoCalculo(empleado, planilla)
276
+
277
+ # Get exchange rate
278
+ emp_calculo.tipo_cambio = self.exchange_rate_calculator.get_exchange_rate(empleado, planilla, fecha_calculo)
279
+
280
+ # Apply exchange rate to convert employee salary to planilla currency
281
+ salario_mensual = emp_calculo.salario_base
282
+ if emp_calculo.tipo_cambio != Decimal("1.00"):
283
+ salario_mensual = (salario_mensual * emp_calculo.tipo_cambio).quantize(
284
+ Decimal("0.01"), rounding=ROUND_HALF_UP
285
+ )
286
+
287
+ # Calculate salary for the pay period
288
+ emp_calculo.salario_base = self.salary_calculator.calculate_period_salary(
289
+ salario_mensual, planilla, periodo_inicio, periodo_fin, fecha_calculo
290
+ )
291
+
292
+ # Store the monthly salary for use in calculations
293
+ emp_calculo.salario_mensual = salario_mensual
294
+
295
+ # Load employee novelties
296
+ emp_calculo.novedades = self.novelty_processor.load_novelties(empleado, periodo_inicio, periodo_fin)
297
+
298
+ # Build calculation variables
299
+ emp_calculo.variables_calculo = self.employee_processing_service.build_calculation_variables(
300
+ emp_calculo, planilla, periodo_inicio, periodo_fin, fecha_calculo
301
+ )
302
+
303
+ # Process perceptions
304
+ percepciones = self.perception_calculator.calculate(emp_calculo, planilla, fecha_calculo)
305
+ emp_calculo.percepciones = percepciones
306
+ emp_calculo.total_percepciones = sum(p.monto for p in percepciones)
307
+
308
+ # Calculate gross salary
309
+ emp_calculo.salario_bruto = emp_calculo.salario_base + emp_calculo.total_percepciones
310
+
311
+ # Process deductions
312
+ deducciones = self.deduction_calculator.calculate(emp_calculo, planilla, fecha_calculo)
313
+ emp_calculo.deducciones = deducciones
314
+ emp_calculo.total_deducciones = sum(d.monto for d in deducciones)
315
+
316
+ # Apply automatic loan/advance deductions
317
+ saldo_disponible = emp_calculo.salario_bruto - emp_calculo.total_deducciones
318
+
319
+ loan_deductions = loan_processor.process_loans(
320
+ empleado.id, saldo_disponible, planilla.aplicar_prestamos_automatico, planilla.prioridad_prestamos
321
+ )
322
+ emp_calculo.deducciones.extend(loan_deductions)
323
+ emp_calculo.total_deducciones += sum(d.monto for d in loan_deductions)
324
+ saldo_disponible -= sum(d.monto for d in loan_deductions)
325
+
326
+ advance_deductions = loan_processor.process_advances(
327
+ empleado.id, saldo_disponible, planilla.aplicar_adelantos_automatico, planilla.prioridad_adelantos
328
+ )
329
+ emp_calculo.deducciones.extend(advance_deductions)
330
+ emp_calculo.total_deducciones += sum(d.monto for d in advance_deductions)
331
+
332
+ # Calculate net salary
333
+ emp_calculo.salario_neto = emp_calculo.salario_bruto - emp_calculo.total_deducciones
334
+
335
+ # Ensure net salary is not negative
336
+ # Note: warnings list is shared via loan_processor context, so warnings will be added there
337
+ if emp_calculo.salario_neto < 0:
338
+ emp_calculo.salario_neto = Decimal("0.00")
339
+
340
+ # Process employer benefits
341
+ prestaciones = self.benefit_calculator.calculate(emp_calculo, planilla, fecha_calculo)
342
+ emp_calculo.prestaciones = prestaciones
343
+ emp_calculo.total_prestaciones = sum(p.monto for p in prestaciones)
344
+
345
+ # Create NominaEmpleado record and update accumulados
346
+ nomina_empleado = self.accounting_processor.create_nomina_empleado(emp_calculo, nomina)
347
+
348
+ # Update accumulated annual values
349
+ self.accumulation_processor.update_accumulations(emp_calculo, planilla, periodo_fin, fecha_calculo)
350
+
351
+ # Create prestacion accumulation transactions
352
+ self.accounting_processor.create_prestacion_transactions(
353
+ emp_calculo, nomina, planilla, periodo_fin, fecha_calculo
354
+ )
355
+
356
+ # Process vacation accrual and usage
357
+ vacation_processor.process_vacations(empleado, emp_calculo, nomina_empleado)
358
+
359
+ return emp_calculo
360
+
361
+ def _calculate_totals(self, nomina: Nomina, empleados_calculo: list[EmpleadoCalculo]) -> None:
362
+ """Calculate grand totals for the nomina."""
363
+ total_bruto = Decimal("0.00")
364
+ total_deducciones = Decimal("0.00")
365
+ total_neto = Decimal("0.00")
366
+
367
+ for emp_calculo in empleados_calculo:
368
+ total_bruto += emp_calculo.salario_bruto
369
+ total_deducciones += emp_calculo.total_deducciones
370
+ total_neto += emp_calculo.salario_neto
371
+
372
+ nomina.total_bruto = total_bruto
373
+ nomina.total_deducciones = total_deducciones
374
+ nomina.total_neto = total_neto