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