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,247 @@
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 formula engine facade."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Any
20
+
21
+ from coati_payroll.i18n import _
22
+ from coati_payroll.log import TRACE_LEVEL_NUM, is_trace_enabled, log
23
+
24
+ from ..formula_engine.data_sources import AVAILABLE_DATA_SOURCES
25
+ from .ast.type_converter import to_decimal
26
+ from .exceptions import ValidationError
27
+ from .execution.execution_context import ExecutionContext
28
+ from .execution.step_executor import StepExecutor
29
+ from .execution.variable_store import VariableStore
30
+ from .results.execution_result import ExecutionResult
31
+ from .steps.step_factory import StepFactory
32
+ from .validation.schema_validator import SchemaValidator
33
+ from .validation.tax_table_validator import TaxTableValidator
34
+
35
+
36
+ class FormulaEngine:
37
+ """Engine for executing JSON-based calculation rules for payroll.
38
+
39
+ This engine provides a secure way to execute complex calculations for
40
+ perceptions, deductions, taxes, and other payroll formulas defined in
41
+ JSON format. It supports variables, formulas, conditionals, and rate
42
+ table lookups.
43
+
44
+ NOTE: Only Percepciones and Deducciones affect employee net pay.
45
+ Prestaciones are employer costs calculated separately.
46
+ """
47
+
48
+ def __init__(self, schema: dict[str, Any], strict_mode: bool = False):
49
+ """Initialize the formula engine with a calculation schema.
50
+
51
+ Args:
52
+ schema: JSON schema defining the calculation rules
53
+ strict_mode: If True, warnings are treated as errors. Default: False
54
+
55
+ Raises:
56
+ ValidationError: If schema is invalid
57
+ """
58
+ self.schema = schema
59
+ self.strict_mode = strict_mode
60
+ # Initialize variables and results for backward compatibility with tests
61
+ self.variables: dict[str, Decimal] = {}
62
+ self.results: dict[str, Any] = {}
63
+
64
+ # Validate schema
65
+ schema_validator = SchemaValidator()
66
+ schema_validator.validate(schema)
67
+
68
+ # Validate tax tables
69
+ tax_table_validator = TaxTableValidator(strict_mode)
70
+ warnings = tax_table_validator.validate_all(schema.get("tax_tables", {}))
71
+
72
+ # Handle warnings
73
+ if warnings:
74
+ if strict_mode:
75
+ raise ValidationError(
76
+ f"Advertencias en tablas de impuestos (modo estricto activado): {', '.join(warnings)}"
77
+ )
78
+ else:
79
+ for warning in warnings:
80
+ log.warning(f"Validación de tabla de impuestos: {warning}")
81
+
82
+ def _trace(self, message: str) -> None:
83
+ """Trace helper for logging."""
84
+ if is_trace_enabled():
85
+ try:
86
+ log.log(TRACE_LEVEL_NUM, message)
87
+ except Exception:
88
+ pass
89
+
90
+ def execute(self, inputs: dict[str, Any]) -> dict[str, Any]:
91
+ """Execute the calculation schema with provided inputs.
92
+
93
+ Args:
94
+ inputs: Dictionary of input values
95
+
96
+ Returns:
97
+ Dictionary containing all results and the final output
98
+
99
+ Raises:
100
+ CalculationError: If execution fails
101
+ """
102
+ # Initialize components
103
+ variable_store = VariableStore()
104
+ step_factory = StepFactory()
105
+ step_executor = StepExecutor()
106
+
107
+ # Prepare initial variables
108
+ initial_vars = self._prepare_initial_variables(inputs)
109
+ variable_store.variables = initial_vars
110
+
111
+ # Update instance variables for backward compatibility
112
+ self.variables = initial_vars.copy()
113
+ self.results = {}
114
+
115
+ # Create execution context
116
+ context = ExecutionContext(
117
+ variables=initial_vars,
118
+ tax_tables=self.schema.get("tax_tables", {}),
119
+ trace_callback=self._trace,
120
+ )
121
+
122
+ meta = self.schema.get("meta", {})
123
+ self._trace(
124
+ _("Iniciando ejecución de esquema '%(name)s' pasos=%(count)s")
125
+ % {"name": meta.get("name", "sin nombre"), "count": len(self.schema.get("steps", []))}
126
+ )
127
+
128
+ # Create and execute steps
129
+ steps = [step_factory.create_step(step) for step in self.schema.get("steps", [])]
130
+
131
+ step_results = {}
132
+ for step in steps:
133
+ result = step_executor.execute(step, context)
134
+ step_results[step.name] = result
135
+
136
+ # Update context with step result
137
+ if isinstance(result, dict):
138
+ # Tax lookup result - extract tax value
139
+ tax_value = result.get("tax", Decimal("0"))
140
+ context = context.with_variable(step.name, tax_value)
141
+ variable_store.set(step.name, result)
142
+ self.variables[step.name] = tax_value
143
+ self.results[step.name] = result
144
+ else:
145
+ # Regular calculation result
146
+ context = context.with_variable(step.name, result)
147
+ variable_store.set(step.name, result)
148
+ self.variables[step.name] = result
149
+ self.results[step.name] = result
150
+
151
+ if context.trace_callback:
152
+ context.trace_callback(
153
+ _("Resultado paso '%(name)s' => %(result)s") % {"name": step.name, "result": result}
154
+ )
155
+
156
+ # Get the final output
157
+ output_name = self.schema.get("output", "")
158
+ final_result = context.variables.get(output_name, Decimal("0"))
159
+
160
+ self._trace(_("Resultado final '%(name)s' => %(value)s") % {"name": output_name, "value": final_result})
161
+
162
+ # Create and return result
163
+ execution_result = ExecutionResult(
164
+ variables=context.variables,
165
+ step_results=step_results,
166
+ final_output=final_result,
167
+ )
168
+
169
+ return execution_result.to_dict()
170
+
171
+ def _prepare_initial_variables(self, inputs: dict[str, Any]) -> dict[str, Decimal]:
172
+ """Prepare initial variables from inputs and defaults.
173
+
174
+ Args:
175
+ inputs: Input values provided by caller
176
+
177
+ Returns:
178
+ Dictionary of variable names to Decimal values
179
+ """
180
+ variables = {}
181
+ for input_def in self.schema.get("inputs", []):
182
+ name = input_def.get("name")
183
+ default = input_def.get("default", 0)
184
+
185
+ # Use provided input or default
186
+ if name in inputs:
187
+ variables[name] = to_decimal(inputs[name])
188
+ else:
189
+ variables[name] = to_decimal(default)
190
+
191
+ source = "input" if name in inputs else "default"
192
+ self._trace(
193
+ _("Input '%(name)s' cargado desde %(source)s => %(value)s")
194
+ % {"name": name, "source": source, "value": variables[name]}
195
+ )
196
+
197
+ return variables
198
+
199
+
200
+ def calculate_with_rule(
201
+ rule_schema: dict[str, Any],
202
+ employee_data: dict[str, Any],
203
+ accumulated_data: dict[str, Any] | None = None,
204
+ ) -> dict[str, Any]:
205
+ """Calculate using a rule schema with employee and accumulated data.
206
+
207
+ This is a convenience function that combines employee data with
208
+ accumulated annual data for payroll calculations.
209
+
210
+ Args:
211
+ rule_schema: The JSON calculation schema
212
+ employee_data: Employee-specific data (salary, etc.)
213
+ accumulated_data: Optional accumulated annual data
214
+
215
+ Returns:
216
+ Calculation results including the final output
217
+ """
218
+ inputs = {**employee_data}
219
+
220
+ if accumulated_data:
221
+ # Add accumulated values with prefix
222
+ for key, value in accumulated_data.items():
223
+ inputs[f"acumulado_{key}"] = value
224
+
225
+ engine = FormulaEngine(rule_schema)
226
+ return engine.execute(inputs)
227
+
228
+
229
+ def get_available_sources_for_ui() -> list[dict]:
230
+ """Get available data sources formatted for the UI dropdown.
231
+
232
+ Returns:
233
+ List of dictionaries with source information for the schema editor
234
+ """
235
+ sources = []
236
+ for category, data in AVAILABLE_DATA_SOURCES.items():
237
+ for field_name, field_info in data["fields"].items():
238
+ sources.append(
239
+ {
240
+ "value": f"{category}.{field_name}",
241
+ "label": f"{data['label']} - {field_info['label']}",
242
+ "type": field_info["type"],
243
+ "description": field_info["description"],
244
+ "category": category,
245
+ }
246
+ )
247
+ return sources
@@ -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
+ """Exceptions for formula engine."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from coati_payroll.schema_validator import ValidationError as _BaseValidationError
19
+
20
+
21
+ class FormulaEngineError(Exception):
22
+ """Base exception for formula engine errors.
23
+
24
+ Python 3.11+ enhancement: Supports exception notes via add_note() method.
25
+ """
26
+
27
+ pass
28
+
29
+
30
+ # Alias for backward compatibility
31
+ TaxEngineError = FormulaEngineError
32
+
33
+
34
+ class ValidationError(_BaseValidationError, FormulaEngineError):
35
+ """Exception for validation errors in schema or data.
36
+
37
+ Inherits from both the base ValidationError (for schema_validator) and
38
+ FormulaEngineError (for backward compatibility with existing error handling).
39
+
40
+ Python 3.11+ enhancement: Can use add_note() to append contextual information.
41
+ """
42
+
43
+ pass
44
+
45
+
46
+ class CalculationError(FormulaEngineError):
47
+ """Exception for calculation errors during execution.
48
+
49
+ Python 3.11+ enhancement: Supports add_note() for additional context.
50
+ """
51
+
52
+ pass
@@ -0,0 +1,24 @@
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
+ """Execution context and step execution modules."""
15
+
16
+ from .execution_context import ExecutionContext
17
+ from .step_executor import StepExecutor
18
+ from .variable_store import VariableStore
19
+
20
+ __all__ = [
21
+ "ExecutionContext",
22
+ "StepExecutor",
23
+ "VariableStore",
24
+ ]
@@ -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
+ """Execution context for formula engine."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from decimal import Decimal
20
+ from typing import Any, Callable
21
+
22
+ from ..ast.safe_operators import SAFE_FUNCTIONS, SAFE_OPERATORS
23
+
24
+
25
+ @dataclass
26
+ class ExecutionContext:
27
+ """Context for formula execution."""
28
+
29
+ variables: dict[str, Decimal]
30
+ tax_tables: dict[str, Any]
31
+ trace_callback: Callable[[str], None] | None = None
32
+ safe_operators: dict[type, Any] = field(default_factory=lambda: SAFE_OPERATORS)
33
+ safe_functions: dict[str, Any] = field(default_factory=lambda: SAFE_FUNCTIONS)
34
+
35
+ def with_variable(self, name: str, value: Decimal) -> "ExecutionContext":
36
+ """Create a new context with an additional variable.
37
+
38
+ Args:
39
+ name: Variable name
40
+ value: Variable value
41
+
42
+ Returns:
43
+ New context with updated variables
44
+ """
45
+ new_vars = {**self.variables, name: value}
46
+ return ExecutionContext(
47
+ variables=new_vars,
48
+ tax_tables=self.tax_tables,
49
+ trace_callback=self.trace_callback,
50
+ safe_operators=self.safe_operators,
51
+ safe_functions=self.safe_functions,
52
+ )
@@ -0,0 +1,62 @@
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
+ """Step executor for formula engine."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from coati_payroll.i18n import _
21
+
22
+ from ..exceptions import CalculationError
23
+ from .execution_context import ExecutionContext
24
+
25
+ if TYPE_CHECKING:
26
+ from ..steps.base_step import Step
27
+
28
+
29
+ class StepExecutor:
30
+ """Executes steps in sequence."""
31
+
32
+ def execute(self, step: "Step", context: ExecutionContext) -> Any:
33
+ """Execute a step and update context.
34
+
35
+ Args:
36
+ step: Step to execute
37
+ context: Execution context
38
+
39
+ Returns:
40
+ Step execution result
41
+ """
42
+ if context.trace_callback:
43
+ context.trace_callback(
44
+ _("Ejecutando paso '%(name)s' tipo=%(type)s variables_disponibles=%(vars)s")
45
+ % {
46
+ "name": step.name,
47
+ "type": step.config.get("type"),
48
+ "vars": list(context.variables.keys()),
49
+ }
50
+ )
51
+
52
+ try:
53
+ result = step.execute(context)
54
+ return result
55
+ except Exception as e:
56
+ step_name = step.name
57
+ step_type = step.config.get("type", "unknown")
58
+ error = CalculationError(f"Error in step '{step_name}': {e}")
59
+ if hasattr(error, "add_note"):
60
+ error.add_note(f"Step type: {step_type}")
61
+ error.add_note(f"Available variables: {list(context.variables.keys())}")
62
+ raise error from e
@@ -0,0 +1,59 @@
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
+ """Variable store for formula execution."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Any
20
+
21
+
22
+ class VariableStore:
23
+ """Manages variables during formula execution."""
24
+
25
+ def __init__(self):
26
+ """Initialize variable store."""
27
+ self.variables: dict[str, Decimal] = {}
28
+ self.results: dict[str, Any] = {}
29
+
30
+ def set(self, name: str, value: Decimal | Any) -> None:
31
+ """Set a variable value.
32
+
33
+ Args:
34
+ name: Variable name
35
+ value: Variable value
36
+ """
37
+ if isinstance(value, Decimal):
38
+ self.variables[name] = value
39
+ else:
40
+ # For complex types like tax lookup results
41
+ self.variables[name] = value.get("tax", Decimal("0")) if isinstance(value, dict) else Decimal("0")
42
+ self.results[name] = value
43
+
44
+ def get(self, name: str, default: Decimal = Decimal("0")) -> Decimal:
45
+ """Get a variable value.
46
+
47
+ Args:
48
+ name: Variable name
49
+ default: Default value if not found
50
+
51
+ Returns:
52
+ Variable value
53
+ """
54
+ return self.variables.get(name, default)
55
+
56
+ def clear(self) -> None:
57
+ """Clear all variables and results."""
58
+ self.variables.clear()
59
+ self.results.clear()