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.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- 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()
|