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,295 @@
|
|
|
1
|
+
"""Snapshot Service for Payroll Recalculation Consistency.
|
|
2
|
+
|
|
3
|
+
This service captures immutable snapshots of all configuration data needed
|
|
4
|
+
to ensure payroll calculations can be recalculated consistently.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import date
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from coati_payroll.model import (
|
|
13
|
+
ConfiguracionCalculos,
|
|
14
|
+
Percepcion,
|
|
15
|
+
Deduccion,
|
|
16
|
+
Prestacion,
|
|
17
|
+
Planilla,
|
|
18
|
+
TipoCambio,
|
|
19
|
+
db,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SnapshotService:
|
|
24
|
+
"""Service for capturing configuration snapshots for payroll consistency."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, session):
|
|
27
|
+
self.session = session
|
|
28
|
+
|
|
29
|
+
def capture_configuration_snapshot(self, empresa_id: str) -> dict[str, Any]:
|
|
30
|
+
"""Capture complete company configuration snapshot.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
empresa_id: Company ID
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dictionary with all configuration values
|
|
37
|
+
"""
|
|
38
|
+
config = self.session.execute(
|
|
39
|
+
db.select(ConfiguracionCalculos).filter(
|
|
40
|
+
ConfiguracionCalculos.empresa_id == empresa_id,
|
|
41
|
+
ConfiguracionCalculos.activo.is_(True),
|
|
42
|
+
)
|
|
43
|
+
).scalar_one_or_none()
|
|
44
|
+
|
|
45
|
+
if not config:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"empresa_id": config.empresa_id,
|
|
50
|
+
"pais_id": config.pais_id,
|
|
51
|
+
"dias_mes_nomina": config.dias_mes_nomina,
|
|
52
|
+
"dias_anio_nomina": config.dias_anio_nomina,
|
|
53
|
+
"horas_jornada_diaria": str(config.horas_jornada_diaria),
|
|
54
|
+
"dias_mes_vacaciones": config.dias_mes_vacaciones,
|
|
55
|
+
"dias_anio_vacaciones": config.dias_anio_vacaciones,
|
|
56
|
+
"considerar_bisiesto_vacaciones": config.considerar_bisiesto_vacaciones,
|
|
57
|
+
"dias_anio_financiero": config.dias_anio_financiero,
|
|
58
|
+
"meses_anio_financiero": config.meses_anio_financiero,
|
|
59
|
+
"dias_quincena": config.dias_quincena,
|
|
60
|
+
"liquidacion_modo_dias": config.liquidacion_modo_dias,
|
|
61
|
+
"liquidacion_factor_calendario": config.liquidacion_factor_calendario,
|
|
62
|
+
"liquidacion_factor_laboral": config.liquidacion_factor_laboral,
|
|
63
|
+
"dias_mes_antiguedad": config.dias_mes_antiguedad,
|
|
64
|
+
"dias_anio_antiguedad": config.dias_anio_antiguedad,
|
|
65
|
+
"activo": config.activo,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def capture_exchange_rates_snapshot(self, planilla: Planilla, fecha_calculo: date) -> dict[str, Any]:
|
|
69
|
+
"""Capture exchange rates snapshot for all currencies used.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
planilla: Planilla being processed
|
|
73
|
+
fecha_calculo: Calculation date
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary with exchange rates by currency
|
|
77
|
+
"""
|
|
78
|
+
rates = {}
|
|
79
|
+
|
|
80
|
+
# Get all unique currencies from employees in this planilla
|
|
81
|
+
from coati_payroll.model import Empleado, PlanillaEmpleado
|
|
82
|
+
|
|
83
|
+
empleados = (
|
|
84
|
+
self.session.execute(
|
|
85
|
+
db.select(Empleado)
|
|
86
|
+
.join(PlanillaEmpleado)
|
|
87
|
+
.filter(
|
|
88
|
+
PlanillaEmpleado.planilla_id == planilla.id,
|
|
89
|
+
PlanillaEmpleado.activo.is_(True),
|
|
90
|
+
Empleado.activo.is_(True),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
.scalars()
|
|
94
|
+
.all()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
monedas_usadas = {emp.moneda_id for emp in empleados if emp.moneda_id}
|
|
98
|
+
monedas_usadas.add(planilla.moneda_id)
|
|
99
|
+
|
|
100
|
+
# Get exchange rates for each currency
|
|
101
|
+
for moneda_id in monedas_usadas:
|
|
102
|
+
if moneda_id == planilla.moneda_id:
|
|
103
|
+
rates[moneda_id] = {"tasa": "1.00", "fecha": fecha_calculo.isoformat()}
|
|
104
|
+
else:
|
|
105
|
+
tipo_cambio = (
|
|
106
|
+
self.session.execute(
|
|
107
|
+
db.select(TipoCambio)
|
|
108
|
+
.filter(
|
|
109
|
+
TipoCambio.moneda_origen_id == moneda_id,
|
|
110
|
+
TipoCambio.moneda_destino_id == planilla.moneda_id,
|
|
111
|
+
TipoCambio.fecha_vigencia <= fecha_calculo,
|
|
112
|
+
)
|
|
113
|
+
.order_by(TipoCambio.fecha_vigencia.desc())
|
|
114
|
+
)
|
|
115
|
+
.scalars()
|
|
116
|
+
.first()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if tipo_cambio:
|
|
120
|
+
rates[moneda_id] = {
|
|
121
|
+
"tasa": str(tipo_cambio.tasa),
|
|
122
|
+
"fecha": tipo_cambio.fecha_vigencia.isoformat(),
|
|
123
|
+
"moneda_destino_id": tipo_cambio.moneda_destino_id,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return rates
|
|
127
|
+
|
|
128
|
+
def capture_catalogs_snapshot(self, planilla: Planilla) -> dict[str, Any]:
|
|
129
|
+
"""Capture complete catalogs snapshot (percepciones, deducciones, prestaciones).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
planilla: Planilla being processed
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary with all catalog items and their formulas
|
|
136
|
+
"""
|
|
137
|
+
snapshot = {
|
|
138
|
+
"percepciones": [],
|
|
139
|
+
"deducciones": [],
|
|
140
|
+
"prestaciones": [],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Capture Percepciones linked to this planilla
|
|
144
|
+
from coati_payroll.model import PlanillaIngreso
|
|
145
|
+
|
|
146
|
+
percepciones_ids = (
|
|
147
|
+
self.session.execute(
|
|
148
|
+
db.select(PlanillaIngreso.percepcion_id).filter(
|
|
149
|
+
PlanillaIngreso.planilla_id == planilla.id,
|
|
150
|
+
PlanillaIngreso.activo.is_(True),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
.scalars()
|
|
154
|
+
.all()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if percepciones_ids:
|
|
158
|
+
percepciones = (
|
|
159
|
+
self.session.execute(
|
|
160
|
+
db.select(Percepcion).filter(
|
|
161
|
+
Percepcion.id.in_(percepciones_ids),
|
|
162
|
+
Percepcion.activo.is_(True),
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
.scalars()
|
|
166
|
+
.all()
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
percepciones = []
|
|
170
|
+
|
|
171
|
+
for p in percepciones:
|
|
172
|
+
snapshot["percepciones"].append(
|
|
173
|
+
{
|
|
174
|
+
"id": p.id,
|
|
175
|
+
"codigo": p.codigo,
|
|
176
|
+
"nombre": p.nombre,
|
|
177
|
+
"descripcion": p.descripcion,
|
|
178
|
+
"formula_tipo": p.formula_tipo,
|
|
179
|
+
"formula": p.formula,
|
|
180
|
+
"monto_default": str(p.monto_default) if p.monto_default else None,
|
|
181
|
+
"porcentaje": str(p.porcentaje) if p.porcentaje else None,
|
|
182
|
+
"gravable": p.gravable,
|
|
183
|
+
"base_calculo": p.base_calculo,
|
|
184
|
+
"estado_aprobacion": p.estado_aprobacion,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Capture Deducciones linked to this planilla
|
|
189
|
+
from coati_payroll.model import PlanillaDeduccion
|
|
190
|
+
|
|
191
|
+
deducciones_ids = (
|
|
192
|
+
self.session.execute(
|
|
193
|
+
db.select(PlanillaDeduccion.deduccion_id).filter(
|
|
194
|
+
PlanillaDeduccion.planilla_id == planilla.id,
|
|
195
|
+
PlanillaDeduccion.activo.is_(True),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
.scalars()
|
|
199
|
+
.all()
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if deducciones_ids:
|
|
203
|
+
deducciones = (
|
|
204
|
+
self.session.execute(
|
|
205
|
+
db.select(Deduccion).filter(
|
|
206
|
+
Deduccion.id.in_(deducciones_ids),
|
|
207
|
+
Deduccion.activo.is_(True),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
.scalars()
|
|
211
|
+
.all()
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
deducciones = []
|
|
215
|
+
|
|
216
|
+
for d in deducciones:
|
|
217
|
+
snapshot["deducciones"].append(
|
|
218
|
+
{
|
|
219
|
+
"id": d.id,
|
|
220
|
+
"codigo": d.codigo,
|
|
221
|
+
"nombre": d.nombre,
|
|
222
|
+
"descripcion": d.descripcion,
|
|
223
|
+
"formula_tipo": d.formula_tipo,
|
|
224
|
+
"formula": d.formula,
|
|
225
|
+
"monto_default": str(d.monto_default) if d.monto_default else None,
|
|
226
|
+
"porcentaje": str(d.porcentaje) if d.porcentaje else None,
|
|
227
|
+
"antes_impuesto": d.antes_impuesto,
|
|
228
|
+
"base_calculo": d.base_calculo,
|
|
229
|
+
"estado_aprobacion": d.estado_aprobacion,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Capture Prestaciones linked to this planilla
|
|
234
|
+
from coati_payroll.model import PlanillaPrestacion
|
|
235
|
+
|
|
236
|
+
prestaciones_ids = (
|
|
237
|
+
self.session.execute(
|
|
238
|
+
db.select(PlanillaPrestacion.prestacion_id).filter(
|
|
239
|
+
PlanillaPrestacion.planilla_id == planilla.id,
|
|
240
|
+
PlanillaPrestacion.activo.is_(True),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
.scalars()
|
|
244
|
+
.all()
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if prestaciones_ids:
|
|
248
|
+
prestaciones = (
|
|
249
|
+
self.session.execute(
|
|
250
|
+
db.select(Prestacion).filter(
|
|
251
|
+
Prestacion.id.in_(prestaciones_ids),
|
|
252
|
+
Prestacion.activo.is_(True),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
.scalars()
|
|
256
|
+
.all()
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
prestaciones = []
|
|
260
|
+
|
|
261
|
+
for pr in prestaciones:
|
|
262
|
+
snapshot["prestaciones"].append(
|
|
263
|
+
{
|
|
264
|
+
"id": pr.id,
|
|
265
|
+
"codigo": pr.codigo,
|
|
266
|
+
"nombre": pr.nombre,
|
|
267
|
+
"descripcion": pr.descripcion,
|
|
268
|
+
"formula_tipo": pr.formula_tipo,
|
|
269
|
+
"formula": pr.formula,
|
|
270
|
+
"monto_default": str(pr.monto_default) if pr.monto_default else None,
|
|
271
|
+
"porcentaje": str(pr.porcentaje) if pr.porcentaje else None,
|
|
272
|
+
"base_calculo": pr.base_calculo,
|
|
273
|
+
"tipo_acumulacion": pr.tipo_acumulacion,
|
|
274
|
+
"estado_aprobacion": pr.estado_aprobacion,
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return snapshot
|
|
279
|
+
|
|
280
|
+
def capture_complete_snapshot(self, planilla: Planilla, fecha_calculo: date) -> dict[str, Any]:
|
|
281
|
+
"""Capture complete snapshot of all configuration data.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
planilla: Planilla being processed
|
|
285
|
+
fecha_calculo: Calculation date
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Complete snapshot dictionary
|
|
289
|
+
"""
|
|
290
|
+
return {
|
|
291
|
+
"configuracion": self.capture_configuration_snapshot(planilla.empresa_id),
|
|
292
|
+
"tipos_cambio": self.capture_exchange_rates_snapshot(planilla, fecha_calculo),
|
|
293
|
+
"catalogos": self.capture_catalogs_snapshot(planilla),
|
|
294
|
+
"fecha_captura": fecha_calculo.isoformat(),
|
|
295
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
"""Validators for payroll processing."""
|
|
15
|
+
|
|
16
|
+
from .base_validator import BaseValidator, NominaEngineError, ValidationError, CalculationError
|
|
17
|
+
from .planilla_validator import PlanillaValidator
|
|
18
|
+
from .employee_validator import EmployeeValidator
|
|
19
|
+
from .period_validator import PeriodValidator
|
|
20
|
+
from .currency_validator import CurrencyValidator
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BaseValidator",
|
|
24
|
+
"NominaEngineError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
"CalculationError",
|
|
27
|
+
"PlanillaValidator",
|
|
28
|
+
"EmployeeValidator",
|
|
29
|
+
"PeriodValidator",
|
|
30
|
+
"CurrencyValidator",
|
|
31
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
"""Base validator interface and exceptions."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
from ..domain.payroll_context import PayrollContext
|
|
21
|
+
from ..results.validation_result import ValidationResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NominaEngineError(Exception):
|
|
25
|
+
"""Base exception for payroll engine errors."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationError(NominaEngineError):
|
|
31
|
+
"""Exception for validation errors."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CalculationError(NominaEngineError):
|
|
37
|
+
"""Exception for calculation errors."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseValidator(ABC):
|
|
43
|
+
"""Base validator interface."""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def validate(self, context: PayrollContext) -> ValidationResult:
|
|
47
|
+
"""Validate the given context."""
|
|
48
|
+
pass
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
"""Validator for currency and exchange rates."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import date
|
|
19
|
+
|
|
20
|
+
from ..domain.payroll_context import PayrollContext
|
|
21
|
+
from ..results.validation_result import ValidationResult
|
|
22
|
+
from ..validators.base_validator import BaseValidator
|
|
23
|
+
from ..repositories.exchange_rate_repository import ExchangeRateRepository
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CurrencyValidator(BaseValidator):
|
|
27
|
+
"""Validates currency and exchange rate availability."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, exchange_rate_repository: ExchangeRateRepository):
|
|
30
|
+
self.exchange_rate_repo = exchange_rate_repository
|
|
31
|
+
|
|
32
|
+
def validate(self, context: PayrollContext) -> ValidationResult:
|
|
33
|
+
"""Validate currency - this is a placeholder as currency validation is done per-employee."""
|
|
34
|
+
result = ValidationResult()
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def validate_exchange_rate(self, moneda_origen_id: str, moneda_destino_id: str, fecha: date) -> ValidationResult:
|
|
38
|
+
"""Validate that exchange rate exists for currency pair."""
|
|
39
|
+
result = ValidationResult()
|
|
40
|
+
|
|
41
|
+
if moneda_origen_id == moneda_destino_id:
|
|
42
|
+
return result # Same currency, no exchange rate needed
|
|
43
|
+
|
|
44
|
+
rate = self.exchange_rate_repo.get_rate(moneda_origen_id, moneda_destino_id, fecha)
|
|
45
|
+
if rate is None:
|
|
46
|
+
result.add_error(
|
|
47
|
+
f"No se encontró tipo de cambio de {moneda_origen_id} a {moneda_destino_id} para la fecha {fecha}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return result
|
|
@@ -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
|
+
"""Validator for Employee."""
|
|
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
|
|
22
|
+
from ..domain.payroll_context import PayrollContext
|
|
23
|
+
from ..results.validation_result import ValidationResult
|
|
24
|
+
from ..validators.base_validator import BaseValidator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EmployeeValidator(BaseValidator):
|
|
28
|
+
"""Validates that an employee is eligible for payroll processing."""
|
|
29
|
+
|
|
30
|
+
def validate(self, context: PayrollContext) -> ValidationResult:
|
|
31
|
+
"""Validate employee - this method signature is required by BaseValidator."""
|
|
32
|
+
# This validator is called per-employee, not per-context
|
|
33
|
+
# So we provide a separate method
|
|
34
|
+
result = ValidationResult()
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def validate_employee(
|
|
38
|
+
self, empleado: Empleado, planilla_empresa_id: str | None, periodo_inicio: date, periodo_fin: date
|
|
39
|
+
) -> ValidationResult:
|
|
40
|
+
"""Validate employee for payroll processing."""
|
|
41
|
+
result = ValidationResult()
|
|
42
|
+
|
|
43
|
+
if not empleado.activo:
|
|
44
|
+
result.add_error(f"Empleado {empleado.codigo_empleado} no está activo")
|
|
45
|
+
|
|
46
|
+
if empleado.fecha_alta:
|
|
47
|
+
if empleado.fecha_alta > date.today():
|
|
48
|
+
result.add_error(
|
|
49
|
+
f"Empleado {empleado.codigo_empleado}: fecha de ingreso ({empleado.fecha_alta}) "
|
|
50
|
+
f"es posterior a la fecha actual"
|
|
51
|
+
)
|
|
52
|
+
if empleado.fecha_alta > periodo_fin:
|
|
53
|
+
result.add_error(
|
|
54
|
+
f"Empleado {empleado.codigo_empleado}: fecha de ingreso ({empleado.fecha_alta}) "
|
|
55
|
+
f"es posterior al período a procesar ({periodo_fin})"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
result.add_error(f"Empleado {empleado.codigo_empleado} no tiene fecha de ingreso definida")
|
|
59
|
+
|
|
60
|
+
if empleado.fecha_baja and empleado.fecha_baja < periodo_inicio:
|
|
61
|
+
result.add_error(
|
|
62
|
+
f"Empleado {empleado.codigo_empleado}: fecha de salida ({empleado.fecha_baja}) "
|
|
63
|
+
f"es anterior al inicio del período ({periodo_inicio})"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not empleado.identificacion_personal:
|
|
67
|
+
result.add_error(f"Empleado {empleado.codigo_empleado} no tiene identificación personal")
|
|
68
|
+
|
|
69
|
+
if empleado.salario_base <= Decimal("0.00"):
|
|
70
|
+
result.add_error(
|
|
71
|
+
f"Empleado {empleado.codigo_empleado} tiene salario base inválido ({empleado.salario_base})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not empleado.empresa_id:
|
|
75
|
+
result.add_error(f"Empleado {empleado.codigo_empleado} no está asignado a ninguna empresa")
|
|
76
|
+
|
|
77
|
+
if planilla_empresa_id and empleado.empresa_id:
|
|
78
|
+
if empleado.empresa_id != planilla_empresa_id:
|
|
79
|
+
result.add_error(
|
|
80
|
+
f"Empleado {empleado.codigo_empleado} pertenece a empresa diferente a la planilla. "
|
|
81
|
+
f"Empleado empresa_id={empleado.empresa_id}, Planilla empresa_id={planilla_empresa_id}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not empleado.moneda_id:
|
|
85
|
+
result.add_error(f"Empleado {empleado.codigo_empleado} no tiene moneda definida")
|
|
86
|
+
|
|
87
|
+
return result
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
"""Validator for payroll period."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from ..domain.payroll_context import PayrollContext
|
|
20
|
+
from ..results.validation_result import ValidationResult
|
|
21
|
+
from ..validators.base_validator import BaseValidator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PeriodValidator(BaseValidator):
|
|
25
|
+
"""Validates payroll period."""
|
|
26
|
+
|
|
27
|
+
def validate(self, context: PayrollContext) -> ValidationResult:
|
|
28
|
+
"""Validate period."""
|
|
29
|
+
result = ValidationResult()
|
|
30
|
+
|
|
31
|
+
if context.periodo_inicio > context.periodo_fin:
|
|
32
|
+
result.add_error(
|
|
33
|
+
f"Período inválido: inicio ({context.periodo_inicio}) posterior a fin ({context.periodo_fin})"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
dias_periodo = (context.periodo_fin - context.periodo_inicio).days + 1
|
|
37
|
+
if dias_periodo <= 0:
|
|
38
|
+
result.add_error("El período debe tener al menos un día.")
|
|
39
|
+
elif dias_periodo > 366:
|
|
40
|
+
result.add_error(
|
|
41
|
+
f"Período excesivamente largo: {dias_periodo} días. Los períodos no deben exceder 366 días."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return result
|