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,86 @@
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
+ """Salary calculator for payroll processing."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date, timedelta
19
+ from decimal import Decimal, ROUND_HALF_UP
20
+
21
+ from coati_payroll.model import Planilla, ConfiguracionCalculos
22
+ from ..repositories.config_repository import ConfigRepository
23
+
24
+
25
+ class SalaryCalculator:
26
+ """Calculator for salary period calculations."""
27
+
28
+ def __init__(self, config_repository: ConfigRepository):
29
+ self.config_repo = config_repository
30
+
31
+ def calculate_period_salary(
32
+ self, salario_mensual: Decimal, planilla: Planilla, periodo_inicio: date, periodo_fin: date, fecha_calculo: date
33
+ ) -> Decimal:
34
+ """Calculate salary for the pay period based on actual days."""
35
+ if not planilla or not planilla.tipo_planilla:
36
+ return salario_mensual
37
+
38
+ if not periodo_fin or not periodo_inicio:
39
+ return salario_mensual
40
+
41
+ dias_periodo = (periodo_fin - periodo_inicio).days + 1
42
+
43
+ if dias_periodo <= 0:
44
+ from ..validators import ValidationError
45
+
46
+ raise ValidationError(f"Período inválido: inicio ({periodo_inicio}) posterior a fin ({periodo_fin})")
47
+ if dias_periodo > 366:
48
+ from ..validators import ValidationError
49
+
50
+ raise ValidationError(
51
+ f"Período excesivamente largo: {dias_periodo} días. Los períodos no deben exceder 366 días."
52
+ )
53
+
54
+ tipo_planilla = planilla.tipo_planilla
55
+ periodicidad = tipo_planilla.periodicidad.lower() if tipo_planilla.periodicidad else ""
56
+
57
+ if periodicidad == "mensual":
58
+ is_first_of_month = periodo_inicio.day == 1
59
+ next_day = periodo_fin + timedelta(days=1)
60
+ is_last_of_month = next_day.day == 1
61
+ same_month = periodo_inicio.year == periodo_fin.year and periodo_inicio.month == periodo_fin.month
62
+
63
+ if is_first_of_month and is_last_of_month and same_month:
64
+ return salario_mensual
65
+
66
+ elif periodicidad == "quincenal":
67
+ config = self.config_repo.get_for_empresa(planilla.empresa_id)
68
+ dias_configurados = tipo_planilla.dias or config.dias_quincena
69
+ if dias_periodo == dias_configurados:
70
+ divisor = Decimal(str(config.dias_mes_nomina)) / Decimal(str(config.dias_quincena))
71
+ return (salario_mensual / divisor).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
72
+
73
+ config = self.config_repo.get_for_empresa(planilla.empresa_id)
74
+ dias_base = Decimal(str(config.dias_mes_nomina))
75
+ salario_diario = (salario_mensual / dias_base).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
76
+ salario_periodo = (salario_diario * Decimal(str(dias_periodo))).quantize(
77
+ Decimal("0.01"), rounding=ROUND_HALF_UP
78
+ )
79
+
80
+ return salario_periodo
81
+
82
+ def calculate_hourly_rate(self, salario_mensual: Decimal, config: ConfiguracionCalculos) -> Decimal:
83
+ """Calculate hourly rate from monthly salary."""
84
+ dias_base = Decimal(str(config.dias_mes_nomina))
85
+ horas_dia = Decimal(str(config.horas_jornada_diaria))
86
+ return (salario_mensual / dias_base / horas_dia).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
@@ -0,0 +1,27 @@
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
+ """Domain models for payroll processing."""
15
+
16
+ from .payroll_context import PayrollContext
17
+ from .employee_calculation import EmployeeCalculation, EmpleadoCalculo
18
+ from .calculation_items import DeduccionItem, PercepcionItem, PrestacionItem
19
+
20
+ __all__ = [
21
+ "PayrollContext",
22
+ "EmployeeCalculation",
23
+ "EmpleadoCalculo",
24
+ "DeduccionItem",
25
+ "PercepcionItem",
26
+ "PrestacionItem",
27
+ ]
@@ -0,0 +1,52 @@
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
+ """Calculation items - immutable domain models for payroll items."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import NamedTuple
20
+
21
+
22
+ class DeduccionItem(NamedTuple):
23
+ """Represents a deduction to be applied."""
24
+
25
+ codigo: str
26
+ nombre: str
27
+ monto: Decimal
28
+ prioridad: int
29
+ es_obligatoria: bool
30
+ deduccion_id: str | None = None
31
+ tipo: str = "deduccion" # deduccion, prestamo, adelanto
32
+
33
+
34
+ class PercepcionItem(NamedTuple):
35
+ """Represents a perception to be applied."""
36
+
37
+ codigo: str
38
+ nombre: str
39
+ monto: Decimal
40
+ orden: int
41
+ gravable: bool
42
+ percepcion_id: str | None = None
43
+
44
+
45
+ class PrestacionItem(NamedTuple):
46
+ """Represents an employer benefit to be calculated."""
47
+
48
+ codigo: str
49
+ nombre: str
50
+ monto: Decimal
51
+ orden: int
52
+ prestacion_id: str | None = None
@@ -0,0 +1,53 @@
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 calculation domain models."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Any
20
+
21
+ from coati_payroll.model import Empleado, Planilla
22
+ from .calculation_items import DeduccionItem, PercepcionItem, PrestacionItem
23
+
24
+
25
+ class EmpleadoCalculo:
26
+ """Container for employee calculation data during payroll processing.
27
+
28
+ This is a mutable container used during payroll processing.
29
+ For backward compatibility, this class maintains the same interface
30
+ as the original implementation.
31
+ """
32
+
33
+ def __init__(self, empleado: Empleado, planilla: Planilla):
34
+ self.empleado = empleado
35
+ self.planilla = planilla
36
+ self.salario_base = Decimal(str(empleado.salario_base or 0))
37
+ self.salario_mensual = Decimal(str(empleado.salario_base or 0))
38
+ self.percepciones: list[PercepcionItem] = []
39
+ self.deducciones: list[DeduccionItem] = []
40
+ self.prestaciones: list[PrestacionItem] = []
41
+ self.total_percepciones = Decimal("0.00")
42
+ self.total_deducciones = Decimal("0.00")
43
+ self.total_prestaciones = Decimal("0.00")
44
+ self.salario_bruto = Decimal("0.00")
45
+ self.salario_neto = Decimal("0.00")
46
+ self.tipo_cambio = Decimal("1.00")
47
+ self.moneda_origen_id = empleado.moneda_id
48
+ self.novedades: dict[str, Decimal] = {}
49
+ self.variables_calculo: dict[str, Any] = {}
50
+
51
+
52
+ # Alias for backward compatibility
53
+ EmployeeCalculation = EmpleadoCalculo
@@ -0,0 +1,44 @@
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 context - immutable domain model."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from datetime import date
20
+ from typing import Optional, TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from ..results.validation_result import ValidationResult
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class PayrollContext:
28
+ """Immutable context for payroll execution."""
29
+
30
+ planilla_id: str
31
+ periodo_inicio: date
32
+ periodo_fin: date
33
+ fecha_calculo: date
34
+ usuario: Optional[str] = None
35
+ validation_result: "ValidationResult | None" = None
36
+ errors: list[str] = field(default_factory=list)
37
+ warnings: list[str] = field(default_factory=list)
38
+
39
+ def __post_init__(self):
40
+ """Initialize default validation_result if None."""
41
+ if self.validation_result is None:
42
+ from ..results.validation_result import ValidationResult
43
+
44
+ object.__setattr__(self, "validation_result", ValidationResult())
@@ -0,0 +1,188 @@
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
+ """Main payroll engine orchestrator - refactored implementation."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+
20
+ from coati_payroll.model import db, Planilla, Nomina
21
+ from coati_payroll.log import TRACE_LEVEL_NUM, is_trace_enabled, log
22
+ from .domain.employee_calculation import EmpleadoCalculo
23
+ from .services.payroll_execution_service import PayrollExecutionService
24
+
25
+
26
+ class NominaEngine:
27
+ """Engine for executing payroll runs.
28
+
29
+ This engine processes a Planilla configuration and generates a complete
30
+ Nomina with all employee calculations. It handles:
31
+
32
+ 1. Perceptions (ingresos) - add to gross salary
33
+ 2. Deductions (deducciones) - subtract from net salary, in priority order
34
+ 3. Benefits (prestaciones) - employer costs, don't affect employee pay
35
+ 4. Automatic deductions - loans and advances from Adelanto table
36
+ 5. Accumulated annual values - for progressive tax calculations
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ planilla: Planilla,
42
+ periodo_inicio: date,
43
+ periodo_fin: date,
44
+ fecha_calculo: date | None = None,
45
+ usuario: str | None = None,
46
+ ):
47
+ """Initialize the payroll engine.
48
+
49
+ Args:
50
+ planilla: The Planilla to execute
51
+ periodo_inicio: Start date of the payroll period
52
+ periodo_fin: End date of the payroll period
53
+ fecha_calculo: Date of calculation (defaults to today)
54
+ usuario: Username executing the payroll
55
+ """
56
+ self.planilla = planilla
57
+ self.periodo_inicio = periodo_inicio
58
+ self.periodo_fin = periodo_fin
59
+ self.fecha_calculo = fecha_calculo or date.today()
60
+ self.usuario = usuario
61
+ self.nomina: Nomina | None = None
62
+ self.empleados_calculo: list[EmpleadoCalculo] = []
63
+ self.errors: list[str] = []
64
+ self.warnings: list[str] = []
65
+
66
+ # Initialize execution service
67
+ self.execution_service = PayrollExecutionService(db.session)
68
+
69
+ def _trace(self, message: str) -> None:
70
+ """Trace helper for logging."""
71
+ if is_trace_enabled():
72
+ log.log(TRACE_LEVEL_NUM, message)
73
+
74
+ def validar_planilla(self) -> bool:
75
+ """Validate that the planilla is ready for execution.
76
+
77
+ Returns:
78
+ True if valid, False otherwise
79
+ """
80
+ from .domain.payroll_context import PayrollContext
81
+
82
+ context = PayrollContext(
83
+ planilla_id=self.planilla.id,
84
+ periodo_inicio=self.periodo_inicio,
85
+ periodo_fin=self.periodo_fin,
86
+ fecha_calculo=self.fecha_calculo,
87
+ usuario=self.usuario,
88
+ )
89
+
90
+ validation_result = self.execution_service.planilla_validator.validate(context)
91
+ if not validation_result.is_valid:
92
+ self.errors.extend(validation_result.errors)
93
+ return False
94
+
95
+ return True
96
+
97
+ def ejecutar(self) -> Nomina | None:
98
+ """Execute the payroll run.
99
+
100
+ Returns:
101
+ The generated Nomina record, or None if execution failed
102
+ """
103
+ # Validate planilla
104
+ if not self.validar_planilla():
105
+ return None
106
+
107
+ # Execute payroll using service
108
+ nomina, empleados_calculo, errors, warnings = self.execution_service.execute_payroll(
109
+ self.planilla,
110
+ self.periodo_inicio,
111
+ self.periodo_fin,
112
+ self.fecha_calculo,
113
+ self.usuario,
114
+ )
115
+
116
+ self.nomina = nomina
117
+ self.empleados_calculo = empleados_calculo
118
+ self.errors = errors
119
+ self.warnings = warnings
120
+
121
+ if nomina:
122
+ # Commit the transaction
123
+ db.session.commit()
124
+ return nomina
125
+ else:
126
+ # Rollback on failure
127
+ db.session.rollback()
128
+ return None
129
+
130
+
131
+ def ejecutar_nomina(
132
+ planilla_id: str,
133
+ periodo_inicio: date,
134
+ periodo_fin: date,
135
+ fecha_calculo: date | None = None,
136
+ usuario: str | None = None,
137
+ ) -> tuple[Nomina | None, list[str], list[str]]:
138
+ """Execute a payroll run for a planilla.
139
+
140
+ Convenience function for executing a payroll run.
141
+
142
+ Args:
143
+ planilla_id: ID of the Planilla to execute
144
+ periodo_inicio: Start date of the payroll period
145
+ periodo_fin: End date of the payroll period
146
+ fecha_calculo: Date of calculation (defaults to today)
147
+ usuario: Username executing the payroll
148
+
149
+ Returns:
150
+ Tuple of (Nomina or None, list of errors, list of warnings)
151
+ """
152
+ # Eagerly load all relationships needed for payroll processing
153
+ from sqlalchemy.orm import joinedload
154
+ from sqlalchemy import select
155
+ from coati_payroll.model import PlanillaIngreso, PlanillaDeduccion, PlanillaPrestacion, PlanillaEmpleado
156
+
157
+ planilla = (
158
+ db.session.execute(
159
+ select(Planilla)
160
+ .options(
161
+ joinedload(Planilla.planilla_percepciones).joinedload(PlanillaIngreso.percepcion),
162
+ joinedload(Planilla.planilla_deducciones).joinedload(PlanillaDeduccion.deduccion),
163
+ joinedload(Planilla.planilla_prestaciones).joinedload(PlanillaPrestacion.prestacion),
164
+ joinedload(Planilla.planilla_empleados).joinedload(PlanillaEmpleado.empleado),
165
+ joinedload(Planilla.planilla_reglas_calculo),
166
+ joinedload(Planilla.tipo_planilla),
167
+ joinedload(Planilla.moneda),
168
+ )
169
+ .filter(Planilla.id == planilla_id)
170
+ )
171
+ .unique()
172
+ .scalar_one_or_none()
173
+ )
174
+
175
+ if not planilla:
176
+ return None, ["Planilla no encontrada."], []
177
+
178
+ engine = NominaEngine(
179
+ planilla=planilla,
180
+ periodo_inicio=periodo_inicio,
181
+ periodo_fin=periodo_fin,
182
+ fecha_calculo=fecha_calculo,
183
+ usuario=usuario,
184
+ )
185
+
186
+ nomina = engine.ejecutar()
187
+
188
+ return nomina, engine.errors, engine.warnings
@@ -0,0 +1,28 @@
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
+ """Processors for specialized payroll operations."""
15
+
16
+ from .loan_processor import LoanProcessor
17
+ from .accumulation_processor import AccumulationProcessor
18
+ from .vacation_processor import VacationProcessor
19
+ from .novelty_processor import NoveltyProcessor
20
+ from .accounting_processor import AccountingProcessor
21
+
22
+ __all__ = [
23
+ "LoanProcessor",
24
+ "AccumulationProcessor",
25
+ "VacationProcessor",
26
+ "NoveltyProcessor",
27
+ "AccountingProcessor",
28
+ ]
@@ -0,0 +1,171 @@
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
+ """Accounting processor for creating payroll records."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from coati_payroll.model import db, Nomina, NominaEmpleado, NominaDetalle, Prestacion, PrestacionAcumulada
22
+ from ..domain.employee_calculation import EmpleadoCalculo
23
+
24
+
25
+ class AccountingProcessor:
26
+ """Processor for creating accounting records (NominaEmpleado, NominaDetalle, PrestacionAcumulada)."""
27
+
28
+ def create_nomina_empleado(self, emp_calculo: EmpleadoCalculo, nomina: Nomina) -> NominaEmpleado:
29
+ """Create NominaEmpleado record with all details."""
30
+ empleado = emp_calculo.empleado
31
+
32
+ nomina_empleado = NominaEmpleado(
33
+ nomina_id=nomina.id,
34
+ empleado_id=empleado.id,
35
+ salario_bruto=emp_calculo.salario_bruto,
36
+ total_ingresos=emp_calculo.total_percepciones,
37
+ total_deducciones=emp_calculo.total_deducciones,
38
+ salario_neto=emp_calculo.salario_neto,
39
+ moneda_origen_id=emp_calculo.moneda_origen_id,
40
+ tipo_cambio_aplicado=emp_calculo.tipo_cambio,
41
+ cargo_snapshot=empleado.cargo,
42
+ area_snapshot=empleado.area,
43
+ centro_costos_snapshot=empleado.centro_costos,
44
+ sueldo_base_historico=emp_calculo.salario_base,
45
+ )
46
+ db.session.add(nomina_empleado)
47
+ db.session.flush()
48
+
49
+ # Create detail records for perceptions
50
+ orden = 0
51
+ for percepcion in emp_calculo.percepciones:
52
+ orden += 1
53
+ detalle = NominaDetalle(
54
+ nomina_empleado_id=nomina_empleado.id,
55
+ tipo="ingreso",
56
+ codigo=percepcion.codigo,
57
+ descripcion=percepcion.nombre,
58
+ monto=percepcion.monto,
59
+ orden=orden,
60
+ percepcion_id=percepcion.percepcion_id,
61
+ )
62
+ db.session.add(detalle)
63
+
64
+ # Create detail records for deductions
65
+ for deduccion in emp_calculo.deducciones:
66
+ orden += 1
67
+ detalle = NominaDetalle(
68
+ nomina_empleado_id=nomina_empleado.id,
69
+ tipo="deduccion",
70
+ codigo=deduccion.codigo,
71
+ descripcion=deduccion.nombre,
72
+ monto=deduccion.monto,
73
+ orden=orden,
74
+ deduccion_id=deduccion.deduccion_id,
75
+ )
76
+ db.session.add(detalle)
77
+
78
+ # Create detail records for benefits
79
+ for prestacion in emp_calculo.prestaciones:
80
+ orden += 1
81
+ detalle = NominaDetalle(
82
+ nomina_empleado_id=nomina_empleado.id,
83
+ tipo="prestacion",
84
+ codigo=prestacion.codigo,
85
+ descripcion=prestacion.nombre,
86
+ monto=prestacion.monto,
87
+ orden=orden,
88
+ prestacion_id=prestacion.prestacion_id,
89
+ )
90
+ db.session.add(detalle)
91
+
92
+ return nomina_empleado
93
+
94
+ def create_prestacion_transactions(
95
+ self,
96
+ emp_calculo: EmpleadoCalculo,
97
+ nomina: Nomina,
98
+ planilla,
99
+ periodo_fin: date,
100
+ fecha_calculo: date,
101
+ ) -> None:
102
+ """Create transactional records for accumulated benefits."""
103
+ empleado = emp_calculo.empleado
104
+ periodo_anio = periodo_fin.year
105
+ periodo_mes = periodo_fin.month
106
+ moneda_id = planilla.moneda_id
107
+
108
+ for prestacion_item in emp_calculo.prestaciones:
109
+ if not prestacion_item.prestacion_id:
110
+ continue
111
+
112
+ prestacion = db.session.get(Prestacion, prestacion_item.prestacion_id)
113
+ if not prestacion:
114
+ continue
115
+
116
+ # Get the previous balance
117
+ from sqlalchemy import select
118
+
119
+ ultima_transaccion = (
120
+ db.session.execute(
121
+ select(PrestacionAcumulada)
122
+ .filter(
123
+ PrestacionAcumulada.empleado_id == empleado.id,
124
+ PrestacionAcumulada.prestacion_id == prestacion.id,
125
+ )
126
+ .order_by(
127
+ PrestacionAcumulada.fecha_transaccion.desc(),
128
+ PrestacionAcumulada.creado.desc(),
129
+ )
130
+ .limit(1)
131
+ )
132
+ .unique()
133
+ .scalars()
134
+ .first()
135
+ )
136
+
137
+ saldo_anterior = ultima_transaccion.saldo_nuevo if ultima_transaccion else Decimal("0.00")
138
+
139
+ # For monthly settlement benefits, reset balance if new month
140
+ if prestacion.tipo_acumulacion == "mensual":
141
+ if ultima_transaccion and (
142
+ ultima_transaccion.anio != periodo_anio or ultima_transaccion.mes != periodo_mes
143
+ ):
144
+ saldo_anterior = Decimal("0.00")
145
+
146
+ # Calculate new balance
147
+ monto_transaccion = prestacion_item.monto
148
+ saldo_nuevo = saldo_anterior + monto_transaccion
149
+
150
+ # Create the transaction record
151
+ transaccion = PrestacionAcumulada(
152
+ empleado_id=empleado.id,
153
+ prestacion_id=prestacion.id,
154
+ fecha_transaccion=fecha_calculo,
155
+ tipo_transaccion="adicion",
156
+ anio=periodo_anio,
157
+ mes=periodo_mes,
158
+ moneda_id=moneda_id,
159
+ monto_transaccion=monto_transaccion,
160
+ saldo_anterior=saldo_anterior,
161
+ saldo_nuevo=saldo_nuevo,
162
+ nomina_id=nomina.id,
163
+ observaciones=(
164
+ f"Provisión nómina {nomina.periodo_inicio.strftime('%Y-%m-%d')} - "
165
+ f"{nomina.periodo_fin.strftime('%Y-%m-%d')}"
166
+ ),
167
+ procesado_por=nomina.generado_por,
168
+ creado_por=nomina.generado_por,
169
+ )
170
+
171
+ db.session.add(transaccion)