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,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
|