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,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
+ """Payroll execution engine - Domain-driven architecture.
15
+
16
+ This module provides a modular payroll execution engine organized by domain.
17
+ Main modules:
18
+ - domain: Domain models (immutable data structures)
19
+ - validators: Business validations
20
+ - calculators: Calculation logic
21
+ - processors: Specialized processors (loans, vacations, etc.)
22
+ - repositories: Data access layer
23
+ - services: Business services
24
+ - results: Result DTOs
25
+
26
+ This package maintains backward compatibility with the original nomina_engine.py
27
+ implementation while providing a new modular structure for future development.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from .engine import (
33
+ NominaEngine,
34
+ ejecutar_nomina,
35
+ EmpleadoCalculo,
36
+ )
37
+
38
+ # Export calculation items for backward compatibility
39
+ from .domain.calculation_items import (
40
+ DeduccionItem,
41
+ PercepcionItem,
42
+ PrestacionItem,
43
+ )
44
+
45
+ # Export domain models
46
+ from .domain import (
47
+ PayrollContext,
48
+ EmployeeCalculation,
49
+ )
50
+
51
+ # Export results
52
+ from .results import (
53
+ ValidationResult,
54
+ ErrorResult,
55
+ PayrollResult,
56
+ )
57
+
58
+ # Export exceptions
59
+ from .validators import (
60
+ NominaEngineError,
61
+ ValidationError,
62
+ CalculationError,
63
+ )
64
+
65
+ __all__ = [
66
+ # Main engine
67
+ "NominaEngine",
68
+ "ejecutar_nomina",
69
+ # Legacy compatibility
70
+ "EmpleadoCalculo",
71
+ "DeduccionItem",
72
+ "PercepcionItem",
73
+ "PrestacionItem",
74
+ # Domain models
75
+ "PayrollContext",
76
+ "EmployeeCalculation",
77
+ # Results
78
+ "ValidationResult",
79
+ "ErrorResult",
80
+ "PayrollResult",
81
+ # Exceptions
82
+ "NominaEngineError",
83
+ "ValidationError",
84
+ "CalculationError",
85
+ ]
86
+
87
+ __version__ = "2.0.0"
@@ -0,0 +1,30 @@
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
+ """Calculators for payroll processing."""
15
+
16
+ from .concept_calculator import ConceptCalculator
17
+ from .salary_calculator import SalaryCalculator
18
+ from .perception_calculator import PerceptionCalculator
19
+ from .deduction_calculator import DeductionCalculator
20
+ from .benefit_calculator import BenefitCalculator
21
+ from .exchange_rate_calculator import ExchangeRateCalculator
22
+
23
+ __all__ = [
24
+ "ConceptCalculator",
25
+ "SalaryCalculator",
26
+ "PerceptionCalculator",
27
+ "DeductionCalculator",
28
+ "BenefitCalculator",
29
+ "ExchangeRateCalculator",
30
+ ]
@@ -0,0 +1,79 @@
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
+ """Benefit calculator for payroll processing."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from coati_payroll.model import Planilla
22
+ from ..domain.employee_calculation import EmpleadoCalculo
23
+ from ..domain.calculation_items import PrestacionItem
24
+ from .concept_calculator import ConceptCalculator
25
+
26
+
27
+ class BenefitCalculator:
28
+ """Calculator for employer benefits (prestaciones)."""
29
+
30
+ def __init__(self, concept_calculator: ConceptCalculator):
31
+ self.concept_calculator = concept_calculator
32
+
33
+ def calculate(self, emp_calculo: EmpleadoCalculo, planilla: Planilla, fecha_calculo: date) -> list[PrestacionItem]:
34
+ """Calculate all benefits for an employee."""
35
+ prestaciones = []
36
+
37
+ for planilla_prestacion in planilla.planilla_prestaciones:
38
+ if not planilla_prestacion.activo:
39
+ continue
40
+
41
+ prestacion = planilla_prestacion.prestacion
42
+ if not prestacion or not prestacion.activo:
43
+ continue
44
+
45
+ # Check validity dates
46
+ if prestacion.vigente_desde and prestacion.vigente_desde > fecha_calculo:
47
+ continue
48
+ if prestacion.valido_hasta and prestacion.valido_hasta < fecha_calculo:
49
+ continue
50
+
51
+ # Calculate benefit amount
52
+ monto = self.concept_calculator.calculate(
53
+ emp_calculo,
54
+ prestacion.formula_tipo,
55
+ prestacion.monto_default,
56
+ prestacion.porcentaje,
57
+ prestacion.formula,
58
+ planilla_prestacion.monto_predeterminado,
59
+ planilla_prestacion.porcentaje,
60
+ codigo_concepto=prestacion.codigo,
61
+ base_calculo=getattr(prestacion, "base_calculo", None),
62
+ unidad_calculo=getattr(prestacion, "unidad_calculo", None),
63
+ )
64
+
65
+ # Apply ceiling if defined
66
+ if prestacion.tope_aplicacion and monto > Decimal(str(prestacion.tope_aplicacion)):
67
+ monto = Decimal(str(prestacion.tope_aplicacion))
68
+
69
+ if monto > 0:
70
+ item = PrestacionItem(
71
+ codigo=prestacion.codigo,
72
+ nombre=prestacion.nombre,
73
+ monto=monto,
74
+ orden=planilla_prestacion.orden or 0,
75
+ prestacion_id=prestacion.id,
76
+ )
77
+ prestaciones.append(item)
78
+
79
+ return prestaciones
@@ -0,0 +1,254 @@
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
+ """Concept calculator using Strategy pattern."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal, ROUND_HALF_UP
19
+
20
+ from coati_payroll.enums import FormulaType
21
+ from coati_payroll.formula_engine import FormulaEngine, FormulaEngineError
22
+ from coati_payroll.model import db, Deduccion, ReglaCalculo
23
+ from ..domain.employee_calculation import EmpleadoCalculo
24
+
25
+
26
+ class ConceptCalculator:
27
+ """Calculator for payroll concepts using Strategy pattern."""
28
+
29
+ def __init__(self, config_repository, warnings: list[str]):
30
+ self.config_repo = config_repository
31
+ self.warnings = warnings
32
+
33
+ def calculate(
34
+ self,
35
+ emp_calculo: EmpleadoCalculo,
36
+ formula_tipo: str,
37
+ monto_default: Decimal | None,
38
+ porcentaje: Decimal | None,
39
+ formula: dict | None,
40
+ monto_override: Decimal | None,
41
+ porcentaje_override: Decimal | None,
42
+ codigo_concepto: str | None = None,
43
+ base_calculo: str | None = None,
44
+ unidad_calculo: str | None = None,
45
+ ) -> Decimal:
46
+ """Calculate concept amount."""
47
+ # Use overrides if provided
48
+ if monto_override:
49
+ monto_calculado = Decimal(str(monto_override))
50
+ elif porcentaje_override:
51
+ monto_calculado = (emp_calculo.salario_base * Decimal(str(porcentaje_override)) / Decimal("100")).quantize(
52
+ Decimal("0.01"), rounding=ROUND_HALF_UP
53
+ )
54
+ else:
55
+ match formula_tipo:
56
+ case FormulaType.FIJO:
57
+ monto_calculado = Decimal(str(monto_default or 0))
58
+
59
+ case FormulaType.PORCENTAJE_SALARIO | FormulaType.PORCENTAJE:
60
+ if porcentaje:
61
+ monto_calculado = (
62
+ emp_calculo.salario_base * Decimal(str(porcentaje)) / Decimal("100")
63
+ ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
64
+ else:
65
+ monto_calculado = Decimal("0.00")
66
+
67
+ case FormulaType.PORCENTAJE_BRUTO:
68
+ if porcentaje:
69
+ monto_calculado = (
70
+ emp_calculo.salario_bruto * Decimal(str(porcentaje)) / Decimal("100")
71
+ ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
72
+ else:
73
+ monto_calculado = Decimal("0.00")
74
+
75
+ case FormulaType.HORAS:
76
+ monto_calculado = self._calculate_hours(emp_calculo, porcentaje, codigo_concepto, base_calculo)
77
+
78
+ case FormulaType.DIAS:
79
+ monto_calculado = self._calculate_days(emp_calculo, porcentaje, codigo_concepto, base_calculo)
80
+
81
+ case FormulaType.FORMULA:
82
+ monto_calculado = self._calculate_formula(emp_calculo, formula, codigo_concepto)
83
+
84
+ case FormulaType.REGLA_CALCULO:
85
+ monto_calculado = self._calculate_regla_calculo(emp_calculo, codigo_concepto)
86
+
87
+ case _:
88
+ monto_calculado = Decimal(str(monto_default or 0))
89
+
90
+ # Ensure calculated amounts are never negative
91
+ if monto_calculado < 0:
92
+ self.warnings.append(
93
+ f"Concepto '{codigo_concepto or 'desconocido'}': Configuración incorrecta resultó en "
94
+ f"monto negativo ({monto_calculado}). Ajustando a 0.00. "
95
+ f"Verifique la configuración del concepto (porcentaje o monto)."
96
+ )
97
+ return Decimal("0.00")
98
+
99
+ return monto_calculado
100
+
101
+ def _calculate_hours(
102
+ self,
103
+ emp_calculo: EmpleadoCalculo,
104
+ porcentaje: Decimal | None,
105
+ codigo_concepto: str | None,
106
+ base_calculo: str | None,
107
+ ) -> Decimal:
108
+ """Calculate based on hours."""
109
+ if not codigo_concepto or codigo_concepto not in emp_calculo.novedades:
110
+ return Decimal("0.00")
111
+
112
+ horas = emp_calculo.novedades[codigo_concepto]
113
+ if horas <= 0:
114
+ return Decimal("0.00")
115
+
116
+ # Determine base for calculation
117
+ if base_calculo == "salario_bruto":
118
+ base = emp_calculo.salario_bruto
119
+ else:
120
+ base = emp_calculo.salario_mensual
121
+
122
+ # Calculate hourly rate using configuration
123
+ config = self.config_repo.get_for_empresa(emp_calculo.planilla.empresa_id)
124
+ dias_base = Decimal(str(config.dias_mes_nomina))
125
+ horas_dia = Decimal(str(config.horas_jornada_diaria))
126
+ tasa_hora = (base / dias_base / horas_dia).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
127
+
128
+ # Apply percentage
129
+ if porcentaje:
130
+ tasa_hora = (tasa_hora * Decimal(str(porcentaje)) / Decimal("100")).quantize(
131
+ Decimal("0.01"), rounding=ROUND_HALF_UP
132
+ )
133
+
134
+ # Calculate total for hours
135
+ return (tasa_hora * horas).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
136
+
137
+ def _calculate_days(
138
+ self,
139
+ emp_calculo: EmpleadoCalculo,
140
+ porcentaje: Decimal | None,
141
+ codigo_concepto: str | None,
142
+ base_calculo: str | None,
143
+ ) -> Decimal:
144
+ """Calculate based on days."""
145
+ if not codigo_concepto or codigo_concepto not in emp_calculo.novedades:
146
+ return Decimal("0.00")
147
+
148
+ dias = emp_calculo.novedades[codigo_concepto]
149
+ if dias <= 0:
150
+ return Decimal("0.00")
151
+
152
+ # Determine base for calculation
153
+ if base_calculo == "salario_bruto":
154
+ base = emp_calculo.salario_bruto
155
+ else:
156
+ base = emp_calculo.salario_mensual
157
+
158
+ # Calculate daily rate using configuration
159
+ config = self.config_repo.get_for_empresa(emp_calculo.planilla.empresa_id)
160
+ dias_base = Decimal(str(config.dias_mes_nomina))
161
+ tasa_dia = (base / dias_base).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
162
+
163
+ # Apply percentage
164
+ if porcentaje:
165
+ tasa_dia = (tasa_dia * Decimal(str(porcentaje)) / Decimal("100")).quantize(
166
+ Decimal("0.01"), rounding=ROUND_HALF_UP
167
+ )
168
+
169
+ # Calculate total for days
170
+ return (tasa_dia * dias).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
171
+
172
+ def _calculate_formula(
173
+ self, emp_calculo: EmpleadoCalculo, formula: dict | None, codigo_concepto: str | None
174
+ ) -> Decimal:
175
+ """Calculate using formula engine."""
176
+ if not formula or not isinstance(formula, dict):
177
+ return Decimal("0.00")
178
+
179
+ try:
180
+ # Merge variables with formula inputs
181
+ inputs = {**emp_calculo.variables_calculo}
182
+ inputs["salario_bruto"] = emp_calculo.salario_bruto
183
+ inputs["total_percepciones"] = emp_calculo.total_percepciones
184
+ inputs["total_deducciones"] = emp_calculo.total_deducciones
185
+
186
+ # Calculate before-tax deductions already processed in this period
187
+ deducciones_antes_impuesto_periodo = Decimal("0.00")
188
+ for ded in emp_calculo.deducciones:
189
+ if ded.deduccion_id:
190
+ ded_obj = db.session.get(Deduccion, ded.deduccion_id)
191
+ if ded_obj and ded_obj.antes_impuesto:
192
+ deducciones_antes_impuesto_periodo += ded.monto
193
+ inputs["deducciones_antes_impuesto_periodo"] = deducciones_antes_impuesto_periodo
194
+ # Legacy alias for backward compatibility (deprecated but kept to avoid breaking existing schemas)
195
+ inputs["inss_periodo"] = deducciones_antes_impuesto_periodo
196
+ # New generic aliases (preferred for new schemas)
197
+ inputs["pre_tax_deductions"] = deducciones_antes_impuesto_periodo
198
+ inputs["social_security_deduction"] = deducciones_antes_impuesto_periodo
199
+
200
+ engine = FormulaEngine(formula)
201
+ result = engine.execute(inputs)
202
+ return Decimal(str(result.get("output", 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
203
+ except FormulaEngineError as e:
204
+ self.warnings.append(f"Error en fórmula: {str(e)}")
205
+ return Decimal("0.00")
206
+
207
+ def _calculate_regla_calculo(self, emp_calculo: EmpleadoCalculo, codigo_concepto: str | None) -> Decimal:
208
+ """Calculate using ReglaCalculo."""
209
+ from sqlalchemy import select
210
+
211
+ # Find the ReglaCalculo linked to this deduction
212
+ regla = db.session.execute(
213
+ select(ReglaCalculo).filter_by(deduccion_id=codigo_concepto).filter(ReglaCalculo.activo.is_(True))
214
+ ).scalar_one_or_none()
215
+
216
+ if not regla:
217
+ # Try finding by deduccion_id matching deduccion's id
218
+ deduccion_obj = db.session.execute(select(Deduccion).filter_by(codigo=codigo_concepto)).scalar_one_or_none()
219
+ if deduccion_obj:
220
+ regla = db.session.execute(
221
+ select(ReglaCalculo).filter_by(deduccion_id=deduccion_obj.id).filter(ReglaCalculo.activo.is_(True))
222
+ ).scalar_one_or_none()
223
+
224
+ if not regla or not regla.esquema_json:
225
+ self.warnings.append(f"ReglaCalculo no encontrada para deducción {codigo_concepto}")
226
+ return Decimal("0.00")
227
+
228
+ try:
229
+ # Prepare inputs for formula engine
230
+ inputs = {**emp_calculo.variables_calculo}
231
+ inputs["salario_bruto"] = emp_calculo.salario_bruto
232
+ inputs["total_percepciones"] = emp_calculo.total_percepciones
233
+ inputs["total_deducciones"] = emp_calculo.total_deducciones
234
+
235
+ # Calculate before-tax deductions already processed
236
+ deducciones_antes_impuesto_periodo = Decimal("0.00")
237
+ for ded in emp_calculo.deducciones:
238
+ if ded.deduccion_id:
239
+ ded_obj = db.session.get(Deduccion, ded.deduccion_id)
240
+ if ded_obj and ded_obj.antes_impuesto:
241
+ deducciones_antes_impuesto_periodo += ded.monto
242
+ inputs["deducciones_antes_impuesto_periodo"] = deducciones_antes_impuesto_periodo
243
+ # Legacy alias for backward compatibility (deprecated but kept to avoid breaking existing schemas)
244
+ inputs["inss_periodo"] = deducciones_antes_impuesto_periodo
245
+ # New generic aliases (preferred for new schemas)
246
+ inputs["pre_tax_deductions"] = deducciones_antes_impuesto_periodo
247
+ inputs["social_security_deduction"] = deducciones_antes_impuesto_periodo
248
+
249
+ engine = FormulaEngine(regla.esquema_json)
250
+ result = engine.execute(inputs)
251
+ return Decimal(str(result.get("output", 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
252
+ except FormulaEngineError as e:
253
+ self.warnings.append(f"Error en ReglaCalculo {regla.codigo}: {str(e)}")
254
+ return Decimal("0.00")
@@ -0,0 +1,105 @@
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
+ """Deduction calculator for payroll processing."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+
20
+ from coati_payroll.model import Planilla
21
+ from ..domain.employee_calculation import EmpleadoCalculo
22
+ from ..domain.calculation_items import DeduccionItem
23
+ from .concept_calculator import ConceptCalculator
24
+
25
+
26
+ class DeductionCalculator:
27
+ """Calculator for deductions (salary subtractions)."""
28
+
29
+ def __init__(self, concept_calculator: ConceptCalculator, warnings: list[str]):
30
+ self.concept_calculator = concept_calculator
31
+ self.warnings = warnings
32
+
33
+ def calculate(self, emp_calculo: EmpleadoCalculo, planilla: Planilla, fecha_calculo: date) -> list[DeduccionItem]:
34
+ """Calculate all deductions for an employee, applying priority order."""
35
+ deducciones_pendientes: list[DeduccionItem] = []
36
+
37
+ for planilla_deduccion in planilla.planilla_deducciones:
38
+ if not planilla_deduccion.activo:
39
+ continue
40
+
41
+ deduccion = planilla_deduccion.deduccion
42
+ if not deduccion or not deduccion.activo:
43
+ continue
44
+
45
+ # Check validity dates
46
+ if deduccion.vigente_desde and deduccion.vigente_desde > fecha_calculo:
47
+ continue
48
+ if deduccion.valido_hasta and deduccion.valido_hasta < fecha_calculo:
49
+ continue
50
+
51
+ # Calculate deduction amount
52
+ monto = self.concept_calculator.calculate(
53
+ emp_calculo,
54
+ deduccion.formula_tipo,
55
+ deduccion.monto_default,
56
+ deduccion.porcentaje,
57
+ deduccion.formula,
58
+ planilla_deduccion.monto_predeterminado,
59
+ planilla_deduccion.porcentaje,
60
+ codigo_concepto=deduccion.codigo,
61
+ base_calculo=getattr(deduccion, "base_calculo", None),
62
+ unidad_calculo=getattr(deduccion, "unidad_calculo", None),
63
+ )
64
+
65
+ if monto > 0:
66
+ item = DeduccionItem(
67
+ codigo=deduccion.codigo,
68
+ nombre=deduccion.nombre,
69
+ monto=monto,
70
+ prioridad=planilla_deduccion.prioridad,
71
+ es_obligatoria=planilla_deduccion.es_obligatoria,
72
+ deduccion_id=deduccion.id,
73
+ )
74
+ deducciones_pendientes.append(item)
75
+
76
+ # Sort by priority (lower number = higher priority)
77
+ deducciones_pendientes.sort(key=lambda x: x.prioridad)
78
+
79
+ # Apply deductions in priority order
80
+ saldo_disponible = emp_calculo.salario_bruto
81
+ deducciones_aplicadas = []
82
+
83
+ for deduccion in deducciones_pendientes:
84
+ monto_aplicar = min(deduccion.monto, saldo_disponible)
85
+
86
+ if monto_aplicar <= 0 and not deduccion.es_obligatoria:
87
+ self.warnings.append(
88
+ f"Empleado {emp_calculo.empleado.primer_nombre} "
89
+ f"{emp_calculo.empleado.primer_apellido}: "
90
+ f"Deducción {deduccion.codigo} omitida por saldo insuficiente."
91
+ )
92
+ continue
93
+
94
+ item = DeduccionItem(
95
+ codigo=deduccion.codigo,
96
+ nombre=deduccion.nombre,
97
+ monto=monto_aplicar,
98
+ prioridad=deduccion.prioridad,
99
+ es_obligatoria=deduccion.es_obligatoria,
100
+ deduccion_id=deduccion.deduccion_id,
101
+ )
102
+ deducciones_aplicadas.append(item)
103
+ saldo_disponible -= monto_aplicar
104
+
105
+ return deducciones_aplicadas
@@ -0,0 +1,51 @@
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
+ """Exchange rate calculator for payroll processing."""
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, Planilla
22
+ from ..repositories.exchange_rate_repository import ExchangeRateRepository
23
+
24
+
25
+ class ExchangeRateCalculator:
26
+ """Calculator for exchange rates."""
27
+
28
+ def __init__(self, exchange_rate_repository: ExchangeRateRepository):
29
+ self.exchange_rate_repo = exchange_rate_repository
30
+
31
+ def get_exchange_rate(self, empleado: Empleado, planilla: Planilla, fecha_calculo: date) -> Decimal:
32
+ """Get exchange rate for employee's currency to planilla currency."""
33
+ if not empleado.moneda_id:
34
+ return Decimal("1.00")
35
+
36
+ if empleado.moneda_id == planilla.moneda_id:
37
+ return Decimal("1.00")
38
+
39
+ rate = self.exchange_rate_repo.get_rate(empleado.moneda_id, planilla.moneda_id, fecha_calculo)
40
+ if rate is None:
41
+ from ..validators import CalculationError
42
+
43
+ raise CalculationError(
44
+ f"No se encontró tipo de cambio para empleado "
45
+ f"{empleado.primer_nombre} {empleado.primer_apellido}. "
46
+ f"Se requiere un tipo de cambio de {empleado.moneda.codigo if empleado.moneda else 'desconocido'} "
47
+ f"a {planilla.moneda.codigo if planilla.moneda else 'desconocido'} "
48
+ f"para la fecha {fecha_calculo.strftime('%d/%m/%Y')}."
49
+ )
50
+
51
+ return Decimal(str(rate))
@@ -0,0 +1,75 @@
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
+ """Perception calculator for payroll processing."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+
20
+ from coati_payroll.model import Planilla
21
+ from ..domain.employee_calculation import EmpleadoCalculo
22
+ from ..domain.calculation_items import PercepcionItem
23
+ from .concept_calculator import ConceptCalculator
24
+
25
+
26
+ class PerceptionCalculator:
27
+ """Calculator for perceptions (income additions)."""
28
+
29
+ def __init__(self, concept_calculator: ConceptCalculator):
30
+ self.concept_calculator = concept_calculator
31
+
32
+ def calculate(self, emp_calculo: EmpleadoCalculo, planilla: Planilla, fecha_calculo: date) -> list[PercepcionItem]:
33
+ """Calculate all perceptions for an employee."""
34
+ percepciones = []
35
+
36
+ for planilla_percepcion in planilla.planilla_percepciones:
37
+ if not planilla_percepcion.activo:
38
+ continue
39
+
40
+ percepcion = planilla_percepcion.percepcion
41
+ if not percepcion or not percepcion.activo:
42
+ continue
43
+
44
+ # Check validity dates
45
+ if percepcion.vigente_desde and percepcion.vigente_desde > fecha_calculo:
46
+ continue
47
+ if percepcion.valido_hasta and percepcion.valido_hasta < fecha_calculo:
48
+ continue
49
+
50
+ # Calculate perception amount
51
+ monto = self.concept_calculator.calculate(
52
+ emp_calculo,
53
+ percepcion.formula_tipo,
54
+ percepcion.monto_default,
55
+ percepcion.porcentaje,
56
+ percepcion.formula,
57
+ planilla_percepcion.monto_predeterminado,
58
+ planilla_percepcion.porcentaje,
59
+ codigo_concepto=percepcion.codigo,
60
+ base_calculo=getattr(percepcion, "base_calculo", None),
61
+ unidad_calculo=getattr(percepcion, "unidad_calculo", None),
62
+ )
63
+
64
+ if monto > 0:
65
+ item = PercepcionItem(
66
+ codigo=percepcion.codigo,
67
+ nombre=percepcion.nombre,
68
+ monto=monto,
69
+ orden=planilla_percepcion.orden or 0,
70
+ gravable=percepcion.gravable,
71
+ percepcion_id=percepcion.id,
72
+ )
73
+ percepciones.append(item)
74
+
75
+ return percepciones