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,295 @@
1
+ """Snapshot Service for Payroll Recalculation Consistency.
2
+
3
+ This service captures immutable snapshots of all configuration data needed
4
+ to ensure payroll calculations can be recalculated consistently.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import date
10
+ from typing import Any
11
+
12
+ from coati_payroll.model import (
13
+ ConfiguracionCalculos,
14
+ Percepcion,
15
+ Deduccion,
16
+ Prestacion,
17
+ Planilla,
18
+ TipoCambio,
19
+ db,
20
+ )
21
+
22
+
23
+ class SnapshotService:
24
+ """Service for capturing configuration snapshots for payroll consistency."""
25
+
26
+ def __init__(self, session):
27
+ self.session = session
28
+
29
+ def capture_configuration_snapshot(self, empresa_id: str) -> dict[str, Any]:
30
+ """Capture complete company configuration snapshot.
31
+
32
+ Args:
33
+ empresa_id: Company ID
34
+
35
+ Returns:
36
+ Dictionary with all configuration values
37
+ """
38
+ config = self.session.execute(
39
+ db.select(ConfiguracionCalculos).filter(
40
+ ConfiguracionCalculos.empresa_id == empresa_id,
41
+ ConfiguracionCalculos.activo.is_(True),
42
+ )
43
+ ).scalar_one_or_none()
44
+
45
+ if not config:
46
+ return {}
47
+
48
+ return {
49
+ "empresa_id": config.empresa_id,
50
+ "pais_id": config.pais_id,
51
+ "dias_mes_nomina": config.dias_mes_nomina,
52
+ "dias_anio_nomina": config.dias_anio_nomina,
53
+ "horas_jornada_diaria": str(config.horas_jornada_diaria),
54
+ "dias_mes_vacaciones": config.dias_mes_vacaciones,
55
+ "dias_anio_vacaciones": config.dias_anio_vacaciones,
56
+ "considerar_bisiesto_vacaciones": config.considerar_bisiesto_vacaciones,
57
+ "dias_anio_financiero": config.dias_anio_financiero,
58
+ "meses_anio_financiero": config.meses_anio_financiero,
59
+ "dias_quincena": config.dias_quincena,
60
+ "liquidacion_modo_dias": config.liquidacion_modo_dias,
61
+ "liquidacion_factor_calendario": config.liquidacion_factor_calendario,
62
+ "liquidacion_factor_laboral": config.liquidacion_factor_laboral,
63
+ "dias_mes_antiguedad": config.dias_mes_antiguedad,
64
+ "dias_anio_antiguedad": config.dias_anio_antiguedad,
65
+ "activo": config.activo,
66
+ }
67
+
68
+ def capture_exchange_rates_snapshot(self, planilla: Planilla, fecha_calculo: date) -> dict[str, Any]:
69
+ """Capture exchange rates snapshot for all currencies used.
70
+
71
+ Args:
72
+ planilla: Planilla being processed
73
+ fecha_calculo: Calculation date
74
+
75
+ Returns:
76
+ Dictionary with exchange rates by currency
77
+ """
78
+ rates = {}
79
+
80
+ # Get all unique currencies from employees in this planilla
81
+ from coati_payroll.model import Empleado, PlanillaEmpleado
82
+
83
+ empleados = (
84
+ self.session.execute(
85
+ db.select(Empleado)
86
+ .join(PlanillaEmpleado)
87
+ .filter(
88
+ PlanillaEmpleado.planilla_id == planilla.id,
89
+ PlanillaEmpleado.activo.is_(True),
90
+ Empleado.activo.is_(True),
91
+ )
92
+ )
93
+ .scalars()
94
+ .all()
95
+ )
96
+
97
+ monedas_usadas = {emp.moneda_id for emp in empleados if emp.moneda_id}
98
+ monedas_usadas.add(planilla.moneda_id)
99
+
100
+ # Get exchange rates for each currency
101
+ for moneda_id in monedas_usadas:
102
+ if moneda_id == planilla.moneda_id:
103
+ rates[moneda_id] = {"tasa": "1.00", "fecha": fecha_calculo.isoformat()}
104
+ else:
105
+ tipo_cambio = (
106
+ self.session.execute(
107
+ db.select(TipoCambio)
108
+ .filter(
109
+ TipoCambio.moneda_origen_id == moneda_id,
110
+ TipoCambio.moneda_destino_id == planilla.moneda_id,
111
+ TipoCambio.fecha_vigencia <= fecha_calculo,
112
+ )
113
+ .order_by(TipoCambio.fecha_vigencia.desc())
114
+ )
115
+ .scalars()
116
+ .first()
117
+ )
118
+
119
+ if tipo_cambio:
120
+ rates[moneda_id] = {
121
+ "tasa": str(tipo_cambio.tasa),
122
+ "fecha": tipo_cambio.fecha_vigencia.isoformat(),
123
+ "moneda_destino_id": tipo_cambio.moneda_destino_id,
124
+ }
125
+
126
+ return rates
127
+
128
+ def capture_catalogs_snapshot(self, planilla: Planilla) -> dict[str, Any]:
129
+ """Capture complete catalogs snapshot (percepciones, deducciones, prestaciones).
130
+
131
+ Args:
132
+ planilla: Planilla being processed
133
+
134
+ Returns:
135
+ Dictionary with all catalog items and their formulas
136
+ """
137
+ snapshot = {
138
+ "percepciones": [],
139
+ "deducciones": [],
140
+ "prestaciones": [],
141
+ }
142
+
143
+ # Capture Percepciones linked to this planilla
144
+ from coati_payroll.model import PlanillaIngreso
145
+
146
+ percepciones_ids = (
147
+ self.session.execute(
148
+ db.select(PlanillaIngreso.percepcion_id).filter(
149
+ PlanillaIngreso.planilla_id == planilla.id,
150
+ PlanillaIngreso.activo.is_(True),
151
+ )
152
+ )
153
+ .scalars()
154
+ .all()
155
+ )
156
+
157
+ if percepciones_ids:
158
+ percepciones = (
159
+ self.session.execute(
160
+ db.select(Percepcion).filter(
161
+ Percepcion.id.in_(percepciones_ids),
162
+ Percepcion.activo.is_(True),
163
+ )
164
+ )
165
+ .scalars()
166
+ .all()
167
+ )
168
+ else:
169
+ percepciones = []
170
+
171
+ for p in percepciones:
172
+ snapshot["percepciones"].append(
173
+ {
174
+ "id": p.id,
175
+ "codigo": p.codigo,
176
+ "nombre": p.nombre,
177
+ "descripcion": p.descripcion,
178
+ "formula_tipo": p.formula_tipo,
179
+ "formula": p.formula,
180
+ "monto_default": str(p.monto_default) if p.monto_default else None,
181
+ "porcentaje": str(p.porcentaje) if p.porcentaje else None,
182
+ "gravable": p.gravable,
183
+ "base_calculo": p.base_calculo,
184
+ "estado_aprobacion": p.estado_aprobacion,
185
+ }
186
+ )
187
+
188
+ # Capture Deducciones linked to this planilla
189
+ from coati_payroll.model import PlanillaDeduccion
190
+
191
+ deducciones_ids = (
192
+ self.session.execute(
193
+ db.select(PlanillaDeduccion.deduccion_id).filter(
194
+ PlanillaDeduccion.planilla_id == planilla.id,
195
+ PlanillaDeduccion.activo.is_(True),
196
+ )
197
+ )
198
+ .scalars()
199
+ .all()
200
+ )
201
+
202
+ if deducciones_ids:
203
+ deducciones = (
204
+ self.session.execute(
205
+ db.select(Deduccion).filter(
206
+ Deduccion.id.in_(deducciones_ids),
207
+ Deduccion.activo.is_(True),
208
+ )
209
+ )
210
+ .scalars()
211
+ .all()
212
+ )
213
+ else:
214
+ deducciones = []
215
+
216
+ for d in deducciones:
217
+ snapshot["deducciones"].append(
218
+ {
219
+ "id": d.id,
220
+ "codigo": d.codigo,
221
+ "nombre": d.nombre,
222
+ "descripcion": d.descripcion,
223
+ "formula_tipo": d.formula_tipo,
224
+ "formula": d.formula,
225
+ "monto_default": str(d.monto_default) if d.monto_default else None,
226
+ "porcentaje": str(d.porcentaje) if d.porcentaje else None,
227
+ "antes_impuesto": d.antes_impuesto,
228
+ "base_calculo": d.base_calculo,
229
+ "estado_aprobacion": d.estado_aprobacion,
230
+ }
231
+ )
232
+
233
+ # Capture Prestaciones linked to this planilla
234
+ from coati_payroll.model import PlanillaPrestacion
235
+
236
+ prestaciones_ids = (
237
+ self.session.execute(
238
+ db.select(PlanillaPrestacion.prestacion_id).filter(
239
+ PlanillaPrestacion.planilla_id == planilla.id,
240
+ PlanillaPrestacion.activo.is_(True),
241
+ )
242
+ )
243
+ .scalars()
244
+ .all()
245
+ )
246
+
247
+ if prestaciones_ids:
248
+ prestaciones = (
249
+ self.session.execute(
250
+ db.select(Prestacion).filter(
251
+ Prestacion.id.in_(prestaciones_ids),
252
+ Prestacion.activo.is_(True),
253
+ )
254
+ )
255
+ .scalars()
256
+ .all()
257
+ )
258
+ else:
259
+ prestaciones = []
260
+
261
+ for pr in prestaciones:
262
+ snapshot["prestaciones"].append(
263
+ {
264
+ "id": pr.id,
265
+ "codigo": pr.codigo,
266
+ "nombre": pr.nombre,
267
+ "descripcion": pr.descripcion,
268
+ "formula_tipo": pr.formula_tipo,
269
+ "formula": pr.formula,
270
+ "monto_default": str(pr.monto_default) if pr.monto_default else None,
271
+ "porcentaje": str(pr.porcentaje) if pr.porcentaje else None,
272
+ "base_calculo": pr.base_calculo,
273
+ "tipo_acumulacion": pr.tipo_acumulacion,
274
+ "estado_aprobacion": pr.estado_aprobacion,
275
+ }
276
+ )
277
+
278
+ return snapshot
279
+
280
+ def capture_complete_snapshot(self, planilla: Planilla, fecha_calculo: date) -> dict[str, Any]:
281
+ """Capture complete snapshot of all configuration data.
282
+
283
+ Args:
284
+ planilla: Planilla being processed
285
+ fecha_calculo: Calculation date
286
+
287
+ Returns:
288
+ Complete snapshot dictionary
289
+ """
290
+ return {
291
+ "configuracion": self.capture_configuration_snapshot(planilla.empresa_id),
292
+ "tipos_cambio": self.capture_exchange_rates_snapshot(planilla, fecha_calculo),
293
+ "catalogos": self.capture_catalogs_snapshot(planilla),
294
+ "fecha_captura": fecha_calculo.isoformat(),
295
+ }
@@ -0,0 +1,31 @@
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
+ """Validators for payroll processing."""
15
+
16
+ from .base_validator import BaseValidator, NominaEngineError, ValidationError, CalculationError
17
+ from .planilla_validator import PlanillaValidator
18
+ from .employee_validator import EmployeeValidator
19
+ from .period_validator import PeriodValidator
20
+ from .currency_validator import CurrencyValidator
21
+
22
+ __all__ = [
23
+ "BaseValidator",
24
+ "NominaEngineError",
25
+ "ValidationError",
26
+ "CalculationError",
27
+ "PlanillaValidator",
28
+ "EmployeeValidator",
29
+ "PeriodValidator",
30
+ "CurrencyValidator",
31
+ ]
@@ -0,0 +1,48 @@
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
+ """Base validator interface and exceptions."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from abc import ABC, abstractmethod
19
+
20
+ from ..domain.payroll_context import PayrollContext
21
+ from ..results.validation_result import ValidationResult
22
+
23
+
24
+ class NominaEngineError(Exception):
25
+ """Base exception for payroll engine errors."""
26
+
27
+ pass
28
+
29
+
30
+ class ValidationError(NominaEngineError):
31
+ """Exception for validation errors."""
32
+
33
+ pass
34
+
35
+
36
+ class CalculationError(NominaEngineError):
37
+ """Exception for calculation errors."""
38
+
39
+ pass
40
+
41
+
42
+ class BaseValidator(ABC):
43
+ """Base validator interface."""
44
+
45
+ @abstractmethod
46
+ def validate(self, context: PayrollContext) -> ValidationResult:
47
+ """Validate the given context."""
48
+ pass
@@ -0,0 +1,50 @@
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
+ """Validator for currency and exchange rates."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+
20
+ from ..domain.payroll_context import PayrollContext
21
+ from ..results.validation_result import ValidationResult
22
+ from ..validators.base_validator import BaseValidator
23
+ from ..repositories.exchange_rate_repository import ExchangeRateRepository
24
+
25
+
26
+ class CurrencyValidator(BaseValidator):
27
+ """Validates currency and exchange rate availability."""
28
+
29
+ def __init__(self, exchange_rate_repository: ExchangeRateRepository):
30
+ self.exchange_rate_repo = exchange_rate_repository
31
+
32
+ def validate(self, context: PayrollContext) -> ValidationResult:
33
+ """Validate currency - this is a placeholder as currency validation is done per-employee."""
34
+ result = ValidationResult()
35
+ return result
36
+
37
+ def validate_exchange_rate(self, moneda_origen_id: str, moneda_destino_id: str, fecha: date) -> ValidationResult:
38
+ """Validate that exchange rate exists for currency pair."""
39
+ result = ValidationResult()
40
+
41
+ if moneda_origen_id == moneda_destino_id:
42
+ return result # Same currency, no exchange rate needed
43
+
44
+ rate = self.exchange_rate_repo.get_rate(moneda_origen_id, moneda_destino_id, fecha)
45
+ if rate is None:
46
+ result.add_error(
47
+ f"No se encontró tipo de cambio de {moneda_origen_id} a {moneda_destino_id} para la fecha {fecha}"
48
+ )
49
+
50
+ return result
@@ -0,0 +1,87 @@
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
+ """Validator for Employee."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from coati_payroll.model import Empleado
22
+ from ..domain.payroll_context import PayrollContext
23
+ from ..results.validation_result import ValidationResult
24
+ from ..validators.base_validator import BaseValidator
25
+
26
+
27
+ class EmployeeValidator(BaseValidator):
28
+ """Validates that an employee is eligible for payroll processing."""
29
+
30
+ def validate(self, context: PayrollContext) -> ValidationResult:
31
+ """Validate employee - this method signature is required by BaseValidator."""
32
+ # This validator is called per-employee, not per-context
33
+ # So we provide a separate method
34
+ result = ValidationResult()
35
+ return result
36
+
37
+ def validate_employee(
38
+ self, empleado: Empleado, planilla_empresa_id: str | None, periodo_inicio: date, periodo_fin: date
39
+ ) -> ValidationResult:
40
+ """Validate employee for payroll processing."""
41
+ result = ValidationResult()
42
+
43
+ if not empleado.activo:
44
+ result.add_error(f"Empleado {empleado.codigo_empleado} no está activo")
45
+
46
+ if empleado.fecha_alta:
47
+ if empleado.fecha_alta > date.today():
48
+ result.add_error(
49
+ f"Empleado {empleado.codigo_empleado}: fecha de ingreso ({empleado.fecha_alta}) "
50
+ f"es posterior a la fecha actual"
51
+ )
52
+ if empleado.fecha_alta > periodo_fin:
53
+ result.add_error(
54
+ f"Empleado {empleado.codigo_empleado}: fecha de ingreso ({empleado.fecha_alta}) "
55
+ f"es posterior al período a procesar ({periodo_fin})"
56
+ )
57
+ else:
58
+ result.add_error(f"Empleado {empleado.codigo_empleado} no tiene fecha de ingreso definida")
59
+
60
+ if empleado.fecha_baja and empleado.fecha_baja < periodo_inicio:
61
+ result.add_error(
62
+ f"Empleado {empleado.codigo_empleado}: fecha de salida ({empleado.fecha_baja}) "
63
+ f"es anterior al inicio del período ({periodo_inicio})"
64
+ )
65
+
66
+ if not empleado.identificacion_personal:
67
+ result.add_error(f"Empleado {empleado.codigo_empleado} no tiene identificación personal")
68
+
69
+ if empleado.salario_base <= Decimal("0.00"):
70
+ result.add_error(
71
+ f"Empleado {empleado.codigo_empleado} tiene salario base inválido ({empleado.salario_base})"
72
+ )
73
+
74
+ if not empleado.empresa_id:
75
+ result.add_error(f"Empleado {empleado.codigo_empleado} no está asignado a ninguna empresa")
76
+
77
+ if planilla_empresa_id and empleado.empresa_id:
78
+ if empleado.empresa_id != planilla_empresa_id:
79
+ result.add_error(
80
+ f"Empleado {empleado.codigo_empleado} pertenece a empresa diferente a la planilla. "
81
+ f"Empleado empresa_id={empleado.empresa_id}, Planilla empresa_id={planilla_empresa_id}"
82
+ )
83
+
84
+ if not empleado.moneda_id:
85
+ result.add_error(f"Empleado {empleado.codigo_empleado} no tiene moneda definida")
86
+
87
+ return result
@@ -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
+ """Validator for payroll period."""
15
+
16
+ from __future__ import annotations
17
+
18
+
19
+ from ..domain.payroll_context import PayrollContext
20
+ from ..results.validation_result import ValidationResult
21
+ from ..validators.base_validator import BaseValidator
22
+
23
+
24
+ class PeriodValidator(BaseValidator):
25
+ """Validates payroll period."""
26
+
27
+ def validate(self, context: PayrollContext) -> ValidationResult:
28
+ """Validate period."""
29
+ result = ValidationResult()
30
+
31
+ if context.periodo_inicio > context.periodo_fin:
32
+ result.add_error(
33
+ f"Período inválido: inicio ({context.periodo_inicio}) posterior a fin ({context.periodo_fin})"
34
+ )
35
+
36
+ dias_periodo = (context.periodo_fin - context.periodo_inicio).days + 1
37
+ if dias_periodo <= 0:
38
+ result.add_error("El período debe tener al menos un día.")
39
+ elif dias_periodo > 366:
40
+ result.add_error(
41
+ f"Período excesivamente largo: {dias_periodo} días. Los períodos no deben exceder 366 días."
42
+ )
43
+
44
+ return result