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,45 @@
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
+ """Tax lookup step implementation."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import TYPE_CHECKING
20
+
21
+ from ..tables.table_lookup import TableLookup
22
+ from .base_step import Step
23
+
24
+ if TYPE_CHECKING:
25
+ from ..execution.execution_context import ExecutionContext
26
+
27
+
28
+ class TaxLookupStep(Step):
29
+ """Step for looking up values in tax tables."""
30
+
31
+ def execute(self, context: "ExecutionContext") -> dict[str, Decimal]:
32
+ """Execute tax lookup step.
33
+
34
+ Args:
35
+ context: Execution context
36
+
37
+ Returns:
38
+ Dictionary with tax calculation results
39
+ """
40
+ table_name = self.config.get("table", "")
41
+ input_var = self.config.get("input", "")
42
+ input_value = context.variables.get(input_var, Decimal("0"))
43
+
44
+ table_lookup = TableLookup(context.tax_tables, context.trace_callback)
45
+ return table_lookup.lookup(table_name, input_value)
@@ -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
+ """Tax table and bracket calculation modules."""
15
+
16
+ from .tax_table import TaxTable
17
+ from .bracket_calculator import BracketCalculator
18
+ from .table_lookup import TableLookup
19
+
20
+ __all__ = [
21
+ "TaxTable",
22
+ "BracketCalculator",
23
+ "TableLookup",
24
+ ]
@@ -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
+ """Bracket tax calculation."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal, ROUND_HALF_UP
19
+ from typing import Any
20
+
21
+ from ..ast.type_converter import to_decimal
22
+
23
+
24
+ class BracketCalculator:
25
+ """Calculates tax for a specific bracket."""
26
+
27
+ @staticmethod
28
+ def calculate(bracket: dict[str, Any], input_value: Decimal) -> dict[str, Decimal]:
29
+ """Calculate tax for a specific bracket.
30
+
31
+ Args:
32
+ bracket: Tax bracket definition
33
+ input_value: Value being taxed
34
+
35
+ Returns:
36
+ Dictionary with calculated tax components
37
+ """
38
+ rate = to_decimal(bracket.get("rate", 0))
39
+ fixed = to_decimal(bracket.get("fixed", 0))
40
+ over = to_decimal(bracket.get("over", 0))
41
+
42
+ # Calculate tax: fixed + (input_value - over) * rate
43
+ excess = max(input_value - over, Decimal("0"))
44
+ tax = fixed + (excess * rate)
45
+
46
+ return {
47
+ "tax": tax.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
48
+ "rate": rate,
49
+ "fixed": fixed,
50
+ "over": over,
51
+ }
@@ -0,0 +1,161 @@
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
+ """Table lookup implementation."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Any, Callable
20
+
21
+ from coati_payroll.i18n import _
22
+
23
+ from ..ast.type_converter import to_decimal
24
+ from ..exceptions import CalculationError
25
+ from .bracket_calculator import BracketCalculator
26
+
27
+
28
+ class TableLookup:
29
+ """Handles lookups in tax tables."""
30
+
31
+ def __init__(self, tax_tables: dict[str, Any], trace_callback: Callable[[str], None] | None = None):
32
+ """Initialize table lookup.
33
+
34
+ Args:
35
+ tax_tables: Dictionary of tax table names to table definitions
36
+ trace_callback: Optional callback for trace logging
37
+ """
38
+ self.tax_tables = tax_tables
39
+ self.trace_callback = trace_callback or (lambda _: None)
40
+
41
+ def lookup(self, table_name: str, input_value: Decimal) -> dict[str, Decimal]:
42
+ """Look up tax bracket in a tax table.
43
+
44
+ Args:
45
+ table_name: Name of the tax table
46
+ input_value: Value to look up
47
+
48
+ Returns:
49
+ Dictionary with 'tax', 'rate', 'fixed', 'over' values
50
+
51
+ Raises:
52
+ CalculationError: If table not found or lookup fails
53
+ """
54
+ if table_name not in self.tax_tables:
55
+ raise CalculationError(f"Tax table '{table_name}' not found")
56
+
57
+ table = self.tax_tables[table_name]
58
+ if not isinstance(table, list):
59
+ raise CalculationError(f"Tax table '{table_name}' must be a list")
60
+
61
+ if not table:
62
+ # Defensive: empty table
63
+ self.trace_callback(
64
+ _("Advertencia: tabla de impuestos '%(table)s' está vacía, devolviendo ceros") % {"table": table_name}
65
+ )
66
+ return {
67
+ "tax": Decimal("0"),
68
+ "rate": Decimal("0"),
69
+ "fixed": Decimal("0"),
70
+ "over": Decimal("0"),
71
+ }
72
+
73
+ self.trace_callback(
74
+ _("Buscando tabla de impuestos '%(table)s' con valor %(value)s; brackets=%(count)s")
75
+ % {"table": table_name, "value": input_value, "count": len(table)}
76
+ )
77
+
78
+ # Defensive: Sort brackets by min value if not already sorted
79
+ try:
80
+ sorted_table = sorted(table, key=lambda b: to_decimal(b.get("min", 0)))
81
+ if sorted_table != table:
82
+ self.trace_callback(
83
+ _("Advertencia: tabla '%(table)s' no estaba ordenada, ordenando automáticamente")
84
+ % {"table": table_name}
85
+ )
86
+ table = sorted_table
87
+ except Exception as e:
88
+ self.trace_callback(
89
+ _("Advertencia: no se pudo ordenar la tabla '%(table)s': %(error)s")
90
+ % {"table": table_name, "error": str(e)}
91
+ )
92
+
93
+ # Find the applicable bracket
94
+ matched_brackets = []
95
+ for i, bracket in enumerate(table):
96
+ try:
97
+ min_val = to_decimal(bracket.get("min", 0))
98
+ max_val = bracket.get("max")
99
+
100
+ if max_val is None:
101
+ # Open-ended bracket (highest tier)
102
+ if input_value >= min_val:
103
+ matched_brackets.append((i, bracket, min_val, None))
104
+ else:
105
+ max_val = to_decimal(max_val)
106
+ # Defensive: validate bracket range
107
+ if max_val < min_val:
108
+ msg = _("Advertencia: tramo %(index)s de tabla '%(table)s' tiene max < min, omitiendo")
109
+ self.trace_callback(msg % {"index": i, "table": table_name})
110
+ continue
111
+
112
+ if min_val <= input_value <= max_val:
113
+ matched_brackets.append((i, bracket, min_val, max_val))
114
+ except Exception as e:
115
+ # Defensive: skip invalid brackets
116
+ self.trace_callback(
117
+ _("Advertencia: error procesando tramo %(index)s de tabla '%(table)s': %(error)s")
118
+ % {"index": i, "table": table_name, "error": str(e)}
119
+ )
120
+ continue
121
+
122
+ # Handle multiple matches (overlaps) - use the first valid match
123
+ if matched_brackets:
124
+ if len(matched_brackets) > 1:
125
+ # Multiple brackets match - this indicates an overlap
126
+ self.trace_callback(
127
+ _(
128
+ "ADVERTENCIA CRÍTICA: múltiples tramos coinciden para valor %(value)s en tabla '%(table)s'. "
129
+ "Esto indica solapamiento. Usando el primer tramo encontrado."
130
+ )
131
+ % {"value": input_value, "table": table_name}
132
+ )
133
+
134
+ i, bracket, min_val, max_val = matched_brackets[0]
135
+ result = BracketCalculator.calculate(bracket, input_value)
136
+ if max_val is None:
137
+ self.trace_callback(
138
+ _("Aplicando tramo abierto desde %(min)s para valor %(value)s -> %(result)s")
139
+ % {"min": min_val, "value": input_value, "result": result}
140
+ )
141
+ else:
142
+ self.trace_callback(
143
+ _("Aplicando tramo %(min)s - %(max)s para valor %(value)s -> %(result)s")
144
+ % {"min": min_val, "max": max_val, "value": input_value, "result": result}
145
+ )
146
+ return result
147
+
148
+ # If no bracket found, return zeros
149
+ self.trace_callback(
150
+ _(
151
+ "No se encontró tramo para valor %(value)s en tabla '%(table)s', devolviendo ceros. "
152
+ "Esto puede indicar un gap en la configuración de la tabla."
153
+ )
154
+ % {"value": input_value, "table": table_name}
155
+ )
156
+ return {
157
+ "tax": Decimal("0"),
158
+ "rate": Decimal("0"),
159
+ "fixed": Decimal("0"),
160
+ "over": Decimal("0"),
161
+ }
@@ -0,0 +1,32 @@
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
+ """Tax table data structure."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+
21
+ class TaxTable:
22
+ """Represents a tax table with brackets."""
23
+
24
+ def __init__(self, name: str, brackets: list[dict[str, Any]]):
25
+ """Initialize tax table.
26
+
27
+ Args:
28
+ name: Table name
29
+ brackets: List of bracket dictionaries
30
+ """
31
+ self.name = name
32
+ self.brackets = brackets
@@ -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
+ """Validation modules for formula engine."""
15
+
16
+ from .schema_validator import SchemaValidator
17
+ from .tax_table_validator import TaxTableValidator
18
+ from .security_validator import SecurityValidator
19
+
20
+ __all__ = [
21
+ "SchemaValidator",
22
+ "TaxTableValidator",
23
+ "SecurityValidator",
24
+ ]
@@ -0,0 +1,37 @@
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
+ """Schema validation for formula engine."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from coati_payroll.schema_validator import validate_schema
21
+
22
+ from ..exceptions import ValidationError
23
+
24
+
25
+ class SchemaValidator:
26
+ """Validates formula engine schemas."""
27
+
28
+ def validate(self, schema: dict[str, Any]) -> None:
29
+ """Validate a calculation schema.
30
+
31
+ Args:
32
+ schema: JSON schema to validate
33
+
34
+ Raises:
35
+ ValidationError: If schema is invalid
36
+ """
37
+ validate_schema(schema, error_class=ValidationError)
@@ -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
+ """Security validation for AST nodes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+
20
+ from ..ast.safe_operators import ALLOWED_AST_TYPES, SAFE_FUNCTIONS
21
+ from ..exceptions import CalculationError
22
+
23
+
24
+ class SecurityValidator:
25
+ """Validates AST security for expression evaluation."""
26
+
27
+ @staticmethod
28
+ def validate_ast_security(node: ast.AST) -> None:
29
+ """Validate that an AST node only contains safe operations.
30
+
31
+ Args:
32
+ node: AST node to validate
33
+
34
+ Raises:
35
+ CalculationError: If unsafe operations are detected
36
+ """
37
+ # Validate all nodes in the tree in a single pass
38
+ for child in ast.walk(node):
39
+ if not isinstance(child, ALLOWED_AST_TYPES):
40
+ raise CalculationError(
41
+ f"Unsafe operation detected: {child.__class__.__name__}. "
42
+ "Only basic arithmetic and safe functions are allowed."
43
+ )
44
+ # Validate function calls
45
+ if isinstance(child, ast.Call):
46
+ if not isinstance(child.func, ast.Name):
47
+ raise CalculationError("Only named functions are allowed")
48
+ if child.func.id not in SAFE_FUNCTIONS:
49
+ raise CalculationError(
50
+ f"Function '{child.func.id}' is not allowed. "
51
+ f"Allowed functions: {', '.join(SAFE_FUNCTIONS.keys())}"
52
+ )
@@ -0,0 +1,205 @@
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
+ """Tax table validation for formula engine."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Any
20
+
21
+ from ..ast.type_converter import to_decimal
22
+ from ..exceptions import ValidationError
23
+
24
+
25
+ class TaxTableValidator:
26
+ """Validates tax tables for integrity."""
27
+
28
+ def __init__(self, strict_mode: bool = False):
29
+ """Initialize validator.
30
+
31
+ Args:
32
+ strict_mode: If True, warnings are treated as errors
33
+ """
34
+ self.strict_mode = strict_mode
35
+
36
+ def validate_table(self, table_name: str, table: list[dict[str, Any]]) -> tuple[list[str], list[str]]:
37
+ """Validate a tax table for critical integrity issues.
38
+
39
+ Args:
40
+ table_name: Name of the tax table being validated
41
+ table: List of tax bracket dictionaries
42
+
43
+ Returns:
44
+ Tuple of (errors, warnings) lists
45
+
46
+ Raises:
47
+ ValidationError: If table has critical errors
48
+ """
49
+ errors: list[str] = []
50
+ warnings: list[str] = []
51
+
52
+ if not table:
53
+ raise ValidationError(
54
+ f"La tabla de impuestos '{table_name}' está vacía. " "Debe contener al menos un tramo."
55
+ )
56
+
57
+ # Validate each bracket structure
58
+ for i, bracket in enumerate(table):
59
+ if not isinstance(bracket, dict):
60
+ raise ValidationError(f"El tramo {i} de la tabla '{table_name}' debe ser un diccionario")
61
+
62
+ min_val = bracket.get("min")
63
+ max_val = bracket.get("max")
64
+
65
+ if min_val is None:
66
+ raise ValidationError(f"El tramo {i} de la tabla '{table_name}' debe tener un valor 'min'")
67
+
68
+ try:
69
+ min_decimal = to_decimal(min_val)
70
+ except ValidationError as e:
71
+ raise ValidationError(
72
+ f"El valor 'min' del tramo {i} de la tabla '{table_name}' es inválido: {e}"
73
+ ) from e
74
+
75
+ if max_val is not None:
76
+ try:
77
+ max_decimal = to_decimal(max_val)
78
+ if max_decimal < min_decimal:
79
+ raise ValidationError(
80
+ f"El tramo {i} de la tabla '{table_name}' tiene 'max' ({max_val}) "
81
+ f"menor que 'min' ({min_val}). El límite superior debe ser mayor o igual al inferior."
82
+ )
83
+ except ValidationError as e:
84
+ raise ValidationError(
85
+ f"El valor 'max' del tramo {i} de la tabla '{table_name}' es inválido: {e}"
86
+ ) from e
87
+
88
+ # Validate fixed and over values
89
+ fixed = bracket.get("fixed", 0)
90
+ over = bracket.get("over", 0)
91
+
92
+ try:
93
+ fixed_decimal = to_decimal(fixed)
94
+ over_decimal = to_decimal(over)
95
+
96
+ if fixed_decimal < 0:
97
+ errors.append(
98
+ f"El tramo {i} de la tabla '{table_name}' tiene 'fixed' negativo ({fixed}). "
99
+ "El valor 'fixed' no puede ser negativo."
100
+ )
101
+
102
+ if over_decimal < 0:
103
+ errors.append(
104
+ f"El tramo {i} de la tabla '{table_name}' tiene 'over' negativo ({over}). "
105
+ "El valor 'over' no puede ser negativo."
106
+ )
107
+
108
+ if over_decimal > min_decimal:
109
+ errors.append(
110
+ f"El tramo {i} de la tabla '{table_name}' tiene 'over' ({over}) mayor que 'min' ({min_val}). "
111
+ "El valor 'over' debe ser menor o igual a 'min'."
112
+ )
113
+ except ValidationError as e:
114
+ errors.append(f"Valores inválidos en tramo {i} de tabla '{table_name}': {e}")
115
+
116
+ # Validate ordering and overlaps
117
+ for i in range(len(table) - 1):
118
+ current = table[i]
119
+ next_bracket = table[i + 1]
120
+
121
+ current_min = to_decimal(current.get("min", 0))
122
+ current_max = current.get("max")
123
+ next_min = to_decimal(next_bracket.get("min", 0))
124
+
125
+ # Check ordering: next bracket's min should be >= current bracket's min
126
+ if next_min < current_min:
127
+ raise ValidationError(
128
+ f"La tabla de impuestos '{table_name}' no está ordenada. "
129
+ f"El tramo {i + 1} tiene 'min'={next_min} que es menor que el 'min'={current_min} "
130
+ f"del tramo {i}. Los tramos deben estar ordenados de menor a mayor."
131
+ )
132
+
133
+ # Check for overlaps and gaps
134
+ if current_max is not None:
135
+ current_max_decimal = to_decimal(current_max)
136
+
137
+ # Check for overlap or gap
138
+ if current_max_decimal > next_min:
139
+ # Overlap detected
140
+ overlap_start = next_min
141
+ overlap_end = current_max_decimal
142
+ raise ValidationError(
143
+ f"La tabla de impuestos '{table_name}' tiene tramos solapados. "
144
+ f"Los tramos {i} y {i + 1} se solapan en el rango [{overlap_start}, {overlap_end}]. "
145
+ f"El tramo {i} termina en {current_max_decimal} y el tramo {i + 1} comienza en {next_min}. "
146
+ "Los tramos no deben solaparse."
147
+ )
148
+ elif current_max_decimal < next_min:
149
+ # Check for significant gap
150
+ gap_size = next_min - current_max_decimal
151
+ tolerance = Decimal("0.01") # Allow 1 cent gap for rounding
152
+
153
+ if gap_size > tolerance:
154
+ warnings.append(
155
+ f"La tabla de impuestos '{table_name}' tiene un gap significativo entre "
156
+ f"los tramos {i} y {i + 1}. "
157
+ f"El tramo {i} termina en {current_max_decimal} y el tramo {i + 1} comienza en {next_min}. "
158
+ f"Hay un gap de {gap_size} que no está cubierto por ningún tramo."
159
+ )
160
+ else:
161
+ # Current bracket is open-ended, but there's a next bracket - this is an error
162
+ raise ValidationError(
163
+ f"La tabla de impuestos '{table_name}' tiene un tramo abierto (sin 'max') en la posición {i}, "
164
+ f"pero hay tramos adicionales después. El tramo abierto debe ser el último de la tabla."
165
+ )
166
+
167
+ # Validate that only the last bracket can be open-ended
168
+ for i in range(len(table) - 1):
169
+ if table[i].get("max") is None:
170
+ raise ValidationError(
171
+ f"La tabla de impuestos '{table_name}' tiene un tramo abierto (sin 'max') en la posición {i}, "
172
+ "pero no es el último tramo. Solo el último tramo puede ser abierto."
173
+ )
174
+
175
+ # Raise errors if any critical errors found
176
+ if errors:
177
+ raise ValidationError(f"Errores críticos en la tabla de impuestos '{table_name}': {'; '.join(errors)}")
178
+
179
+ return errors, warnings
180
+
181
+ def validate_all(self, tax_tables: dict[str, Any]) -> list[str]:
182
+ """Validate all tax tables in the schema.
183
+
184
+ Args:
185
+ tax_tables: Dictionary of tax table names to table definitions
186
+
187
+ Returns:
188
+ List of warning messages (non-critical issues)
189
+
190
+ Raises:
191
+ ValidationError: If any tax table has critical validation errors
192
+ """
193
+ if not isinstance(tax_tables, dict):
194
+ raise ValidationError("'tax_tables' debe ser un diccionario")
195
+
196
+ all_warnings: list[str] = []
197
+
198
+ for table_name, table in tax_tables.items():
199
+ if not isinstance(table, list):
200
+ raise ValidationError(f"La tabla de impuestos '{table_name}' debe ser una lista de tramos")
201
+
202
+ errors, warnings = self.validate_table(table_name, table)
203
+ all_warnings.extend(warnings)
204
+
205
+ return all_warnings