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,267 @@
|
|
|
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
|
+
|
|
15
|
+
"""Liquidación engine orchestrator."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import date, timedelta
|
|
21
|
+
from decimal import Decimal
|
|
22
|
+
|
|
23
|
+
from sqlalchemy import select
|
|
24
|
+
|
|
25
|
+
from coati_payroll.enums import NominaEstado
|
|
26
|
+
from coati_payroll.model import (
|
|
27
|
+
ConfiguracionCalculos,
|
|
28
|
+
Empleado,
|
|
29
|
+
Liquidacion,
|
|
30
|
+
LiquidacionDetalle,
|
|
31
|
+
Nomina,
|
|
32
|
+
NominaEmpleado,
|
|
33
|
+
AdelantoAbono,
|
|
34
|
+
Adelanto,
|
|
35
|
+
db,
|
|
36
|
+
)
|
|
37
|
+
from coati_payroll.nomina_engine.repositories.config_repository import ConfigRepository
|
|
38
|
+
from coati_payroll.nomina_engine.processors.loan_processor import LoanProcessor
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LiquidacionResult:
|
|
43
|
+
liquidacion: Liquidacion | None
|
|
44
|
+
errors: list[str]
|
|
45
|
+
warnings: list[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LiquidacionEngine:
|
|
49
|
+
"""Engine for calculating employee termination settlements (liquidaciones)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, empleado: Empleado, fecha_calculo: date | None = None, usuario: str | None = None):
|
|
52
|
+
self.empleado = empleado
|
|
53
|
+
self.fecha_calculo = fecha_calculo or date.today()
|
|
54
|
+
self.usuario = usuario
|
|
55
|
+
self.errors: list[str] = []
|
|
56
|
+
self.warnings: list[str] = []
|
|
57
|
+
|
|
58
|
+
self._config_repo = ConfigRepository(db.session)
|
|
59
|
+
|
|
60
|
+
def _get_config(self) -> ConfiguracionCalculos:
|
|
61
|
+
return self._config_repo.get_for_empresa(self.empleado.empresa_id)
|
|
62
|
+
|
|
63
|
+
def determinar_ultimo_dia_pagado(self) -> date:
|
|
64
|
+
"""Get the last day covered by the employee's last applied/paid payroll."""
|
|
65
|
+
stmt = (
|
|
66
|
+
select(Nomina.periodo_fin)
|
|
67
|
+
.join(NominaEmpleado, NominaEmpleado.nomina_id == Nomina.id)
|
|
68
|
+
.where(
|
|
69
|
+
NominaEmpleado.empleado_id == self.empleado.id,
|
|
70
|
+
Nomina.estado.in_([NominaEstado.APLICADO, NominaEstado.PAGADO]),
|
|
71
|
+
)
|
|
72
|
+
.order_by(Nomina.periodo_fin.desc())
|
|
73
|
+
.limit(1)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
ultimo = db.session.execute(stmt).scalar_one_or_none()
|
|
77
|
+
if ultimo:
|
|
78
|
+
return ultimo
|
|
79
|
+
|
|
80
|
+
fecha_alta = self.empleado.fecha_alta
|
|
81
|
+
if not fecha_alta:
|
|
82
|
+
self.warnings.append("Empleado sin fecha de alta; usando fecha de cálculo como referencia.")
|
|
83
|
+
return self.fecha_calculo
|
|
84
|
+
|
|
85
|
+
return fecha_alta - timedelta(days=1)
|
|
86
|
+
|
|
87
|
+
def _get_factor_dias(self, config: ConfiguracionCalculos) -> int:
|
|
88
|
+
modo = (config.liquidacion_modo_dias or "calendario").strip().lower()
|
|
89
|
+
if modo == "laboral":
|
|
90
|
+
return int(config.liquidacion_factor_laboral or 28)
|
|
91
|
+
return int(config.liquidacion_factor_calendario or 30)
|
|
92
|
+
|
|
93
|
+
def calcular(self, liquidacion: Liquidacion) -> Liquidacion | None:
|
|
94
|
+
"""Calculate a liquidacion record in-place."""
|
|
95
|
+
config = self._get_config()
|
|
96
|
+
|
|
97
|
+
ultimo_dia_pagado = self.determinar_ultimo_dia_pagado()
|
|
98
|
+
liquidacion.ultimo_dia_pagado = ultimo_dia_pagado
|
|
99
|
+
liquidacion.fecha_calculo = self.fecha_calculo
|
|
100
|
+
|
|
101
|
+
if self.fecha_calculo <= ultimo_dia_pagado:
|
|
102
|
+
liquidacion.dias_por_pagar = 0
|
|
103
|
+
self.warnings.append("La fecha de cálculo es menor o igual al último día pagado.")
|
|
104
|
+
else:
|
|
105
|
+
liquidacion.dias_por_pagar = (self.fecha_calculo - ultimo_dia_pagado).days
|
|
106
|
+
|
|
107
|
+
# Clear previous details (support recalculation)
|
|
108
|
+
liquidacion.detalles.clear()
|
|
109
|
+
|
|
110
|
+
# Income for pending days
|
|
111
|
+
factor_dias = self._get_factor_dias(config)
|
|
112
|
+
salario_mensual = Decimal(str(self.empleado.salario_base or 0))
|
|
113
|
+
if factor_dias <= 0:
|
|
114
|
+
self.errors.append("Factor de días inválido en configuración.")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
tasa_dia = (salario_mensual / Decimal(str(factor_dias))).quantize(Decimal("0.01"))
|
|
118
|
+
monto_dias = (tasa_dia * Decimal(str(liquidacion.dias_por_pagar))).quantize(Decimal("0.01"))
|
|
119
|
+
|
|
120
|
+
if liquidacion.dias_por_pagar > 0 and monto_dias > 0:
|
|
121
|
+
liquidacion.detalles.append(
|
|
122
|
+
LiquidacionDetalle(
|
|
123
|
+
tipo="ingreso",
|
|
124
|
+
codigo="DIAS_POR_PAGAR",
|
|
125
|
+
descripcion="Días por pagar",
|
|
126
|
+
monto=monto_dias,
|
|
127
|
+
orden=1,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Apply pending loans/advances as deductions
|
|
132
|
+
saldo_disponible = monto_dias
|
|
133
|
+
loan_processor = LoanProcessor(
|
|
134
|
+
nomina=None,
|
|
135
|
+
fecha_calculo=self.fecha_calculo,
|
|
136
|
+
periodo_inicio=ultimo_dia_pagado,
|
|
137
|
+
periodo_fin=self.fecha_calculo,
|
|
138
|
+
liquidacion=liquidacion,
|
|
139
|
+
calcular_interes=False,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Defaults match Planilla priorities; liquidation uses fixed priorities for now
|
|
143
|
+
deducciones = []
|
|
144
|
+
deducciones.extend(
|
|
145
|
+
loan_processor.process_loans(
|
|
146
|
+
empleado_id=self.empleado.id,
|
|
147
|
+
saldo_disponible=saldo_disponible,
|
|
148
|
+
aplicar_prestamos=True,
|
|
149
|
+
prioridad_prestamos=250,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
for d in deducciones:
|
|
153
|
+
saldo_disponible -= d.monto
|
|
154
|
+
|
|
155
|
+
deducciones_adv = loan_processor.process_advances(
|
|
156
|
+
empleado_id=self.empleado.id,
|
|
157
|
+
saldo_disponible=saldo_disponible,
|
|
158
|
+
aplicar_adelantos=True,
|
|
159
|
+
prioridad_adelantos=251,
|
|
160
|
+
)
|
|
161
|
+
deducciones.extend(deducciones_adv)
|
|
162
|
+
|
|
163
|
+
orden = 1
|
|
164
|
+
total_deducciones = Decimal("0.00")
|
|
165
|
+
for item in deducciones:
|
|
166
|
+
orden += 1
|
|
167
|
+
total_deducciones += item.monto
|
|
168
|
+
liquidacion.detalles.append(
|
|
169
|
+
LiquidacionDetalle(
|
|
170
|
+
tipo="deduccion",
|
|
171
|
+
codigo=item.codigo,
|
|
172
|
+
descripcion=item.nombre,
|
|
173
|
+
monto=item.monto,
|
|
174
|
+
orden=orden,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
total_bruto = monto_dias
|
|
179
|
+
total_neto = (total_bruto - total_deducciones).quantize(Decimal("0.01"))
|
|
180
|
+
liquidacion.total_bruto = total_bruto
|
|
181
|
+
liquidacion.total_deducciones = total_deducciones
|
|
182
|
+
liquidacion.total_neto = total_neto
|
|
183
|
+
|
|
184
|
+
liquidacion.errores_calculo = {"errors": self.errors} if self.errors else {}
|
|
185
|
+
liquidacion.advertencias_calculo = list(self.warnings)
|
|
186
|
+
|
|
187
|
+
return liquidacion
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def recalcular_liquidacion(liquidacion_id: str, fecha_calculo: date | None = None, usuario: str | None = None):
|
|
191
|
+
"""Recalculate an existing liquidacion.
|
|
192
|
+
|
|
193
|
+
- Removes existing details
|
|
194
|
+
- Reverts any AdelantoAbono records created by this liquidation
|
|
195
|
+
- Re-runs calculation
|
|
196
|
+
"""
|
|
197
|
+
liquidacion = db.session.get(Liquidacion, liquidacion_id)
|
|
198
|
+
if not liquidacion:
|
|
199
|
+
return None, ["Liquidación no encontrada."], []
|
|
200
|
+
|
|
201
|
+
if liquidacion.estado != "borrador":
|
|
202
|
+
return None, ["Solo se pueden recalcular liquidaciones en borrador."], []
|
|
203
|
+
|
|
204
|
+
empleado = db.session.get(Empleado, liquidacion.empleado_id)
|
|
205
|
+
if not empleado:
|
|
206
|
+
return None, ["Empleado no encontrado."], []
|
|
207
|
+
|
|
208
|
+
# Revert loan/advance payments applied by this liquidation
|
|
209
|
+
abonos = (
|
|
210
|
+
db.session.execute(select(AdelantoAbono).where(AdelantoAbono.liquidacion_id == liquidacion.id)).scalars().all()
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
for abono in abonos:
|
|
214
|
+
adelanto = db.session.get(Adelanto, abono.adelanto_id)
|
|
215
|
+
if adelanto:
|
|
216
|
+
# Undo the payment (add back to saldo)
|
|
217
|
+
adelanto.saldo_pendiente = (
|
|
218
|
+
Decimal(str(adelanto.saldo_pendiente)) + Decimal(str(abono.monto_abonado))
|
|
219
|
+
).quantize(Decimal("0.01"))
|
|
220
|
+
if adelanto.saldo_pendiente > 0 and adelanto.estado == "pagado":
|
|
221
|
+
adelanto.estado = "aprobado"
|
|
222
|
+
db.session.delete(abono)
|
|
223
|
+
|
|
224
|
+
# Remove existing details
|
|
225
|
+
liquidacion.detalles.clear()
|
|
226
|
+
|
|
227
|
+
engine = LiquidacionEngine(
|
|
228
|
+
empleado=empleado, fecha_calculo=fecha_calculo or liquidacion.fecha_calculo, usuario=usuario
|
|
229
|
+
)
|
|
230
|
+
calculated = engine.calcular(liquidacion)
|
|
231
|
+
if not calculated:
|
|
232
|
+
db.session.rollback()
|
|
233
|
+
return None, engine.errors, engine.warnings
|
|
234
|
+
|
|
235
|
+
db.session.commit()
|
|
236
|
+
return calculated, engine.errors, engine.warnings
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def ejecutar_liquidacion(
|
|
240
|
+
empleado_id: str,
|
|
241
|
+
concepto_id: str | None,
|
|
242
|
+
fecha_calculo: date | None = None,
|
|
243
|
+
usuario: str | None = None,
|
|
244
|
+
) -> tuple[Liquidacion | None, list[str], list[str]]:
|
|
245
|
+
"""Convenience function to create and calculate a liquidacion."""
|
|
246
|
+
empleado = db.session.get(Empleado, empleado_id)
|
|
247
|
+
if not empleado:
|
|
248
|
+
return None, ["Empleado no encontrado."], []
|
|
249
|
+
|
|
250
|
+
liquidacion = Liquidacion(
|
|
251
|
+
empleado_id=empleado.id,
|
|
252
|
+
concepto_id=concepto_id,
|
|
253
|
+
fecha_calculo=fecha_calculo or date.today(),
|
|
254
|
+
estado="borrador",
|
|
255
|
+
)
|
|
256
|
+
db.session.add(liquidacion)
|
|
257
|
+
db.session.flush()
|
|
258
|
+
|
|
259
|
+
engine = LiquidacionEngine(empleado=empleado, fecha_calculo=fecha_calculo, usuario=usuario)
|
|
260
|
+
calculated = engine.calcular(liquidacion)
|
|
261
|
+
|
|
262
|
+
if not calculated:
|
|
263
|
+
db.session.rollback()
|
|
264
|
+
return None, engine.errors, engine.warnings
|
|
265
|
+
|
|
266
|
+
db.session.commit()
|
|
267
|
+
return calculated, engine.errors, engine.warnings
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
"""Language configuration and caching for internationalization."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
# <-------------------------------------------------------------------------> #
|
|
19
|
+
# Standard library
|
|
20
|
+
# <-------------------------------------------------------------------------> #
|
|
21
|
+
from os import environ
|
|
22
|
+
from threading import Lock
|
|
23
|
+
|
|
24
|
+
# <-------------------------------------------------------------------------> #
|
|
25
|
+
# Third party libraries
|
|
26
|
+
# <-------------------------------------------------------------------------> #
|
|
27
|
+
from flask import current_app
|
|
28
|
+
|
|
29
|
+
# <-------------------------------------------------------------------------> #
|
|
30
|
+
# Local modules
|
|
31
|
+
# <-------------------------------------------------------------------------> #
|
|
32
|
+
from coati_payroll.log import log
|
|
33
|
+
|
|
34
|
+
# Supported languages
|
|
35
|
+
SUPPORTED_LANGUAGES = ["en", "es"]
|
|
36
|
+
DEFAULT_LANGUAGE = "en"
|
|
37
|
+
|
|
38
|
+
# Cache for language setting with thread-safe access
|
|
39
|
+
_language_cache = None
|
|
40
|
+
_cache_lock = Lock()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_language_from_db() -> str:
|
|
44
|
+
"""Get the configured language from the database.
|
|
45
|
+
|
|
46
|
+
Returns the language code ('en' or 'es') from the global configuration table.
|
|
47
|
+
If no configuration exists, returns the default language.
|
|
48
|
+
Uses caching to avoid repeated database queries.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: Language code ('en' or 'es')
|
|
52
|
+
"""
|
|
53
|
+
global _language_cache
|
|
54
|
+
|
|
55
|
+
# Check cache first (thread-safe)
|
|
56
|
+
with _cache_lock:
|
|
57
|
+
if _language_cache is not None:
|
|
58
|
+
return _language_cache
|
|
59
|
+
|
|
60
|
+
# Cache miss - query database
|
|
61
|
+
try:
|
|
62
|
+
from coati_payroll.model import ConfiguracionGlobal, db
|
|
63
|
+
|
|
64
|
+
with current_app.app_context():
|
|
65
|
+
config = db.session.execute(db.select(ConfiguracionGlobal)).scalar_one_or_none()
|
|
66
|
+
|
|
67
|
+
if config and config.idioma in SUPPORTED_LANGUAGES:
|
|
68
|
+
language = config.idioma
|
|
69
|
+
else:
|
|
70
|
+
language = DEFAULT_LANGUAGE
|
|
71
|
+
|
|
72
|
+
# Update cache (thread-safe)
|
|
73
|
+
with _cache_lock:
|
|
74
|
+
_language_cache = language
|
|
75
|
+
|
|
76
|
+
log.trace(f"Language loaded from database: {language}")
|
|
77
|
+
return language
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
log.warning(f"Error reading language from database: {e}")
|
|
81
|
+
return DEFAULT_LANGUAGE
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_language_in_db(language: str) -> None:
|
|
85
|
+
"""Set the configured language in the database.
|
|
86
|
+
|
|
87
|
+
Updates the language setting in the global configuration table and
|
|
88
|
+
invalidates the cache to ensure the change is immediately reflected.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
language: Language code ('en' or 'es')
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If language is not supported
|
|
95
|
+
"""
|
|
96
|
+
if language not in SUPPORTED_LANGUAGES:
|
|
97
|
+
raise ValueError(f"Unsupported language: {language}. Must be one of {SUPPORTED_LANGUAGES}")
|
|
98
|
+
|
|
99
|
+
from coati_payroll.model import ConfiguracionGlobal, db
|
|
100
|
+
|
|
101
|
+
with current_app.app_context():
|
|
102
|
+
config = db.session.execute(db.select(ConfiguracionGlobal)).scalar_one_or_none()
|
|
103
|
+
|
|
104
|
+
if config:
|
|
105
|
+
config.idioma = language
|
|
106
|
+
else:
|
|
107
|
+
# Create new configuration record
|
|
108
|
+
config = ConfiguracionGlobal()
|
|
109
|
+
config.idioma = language
|
|
110
|
+
db.session.add(config)
|
|
111
|
+
|
|
112
|
+
db.session.commit()
|
|
113
|
+
|
|
114
|
+
# Invalidate cache to force reload on next access
|
|
115
|
+
invalidate_language_cache()
|
|
116
|
+
|
|
117
|
+
log.info(f"Language updated to: {language}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def invalidate_language_cache() -> None:
|
|
121
|
+
"""Invalidate the language cache.
|
|
122
|
+
|
|
123
|
+
Forces the next call to get_language_from_db() to query the database.
|
|
124
|
+
Call this after updating the language setting.
|
|
125
|
+
"""
|
|
126
|
+
global _language_cache
|
|
127
|
+
|
|
128
|
+
with _cache_lock:
|
|
129
|
+
_language_cache = None
|
|
130
|
+
|
|
131
|
+
log.trace("Language cache invalidated")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def initialize_language_from_env() -> None:
|
|
135
|
+
"""Initialize language from COATI_LANG environment variable.
|
|
136
|
+
|
|
137
|
+
Called during application startup to set the initial language from
|
|
138
|
+
the environment variable if provided. Only updates the database if
|
|
139
|
+
no configuration exists yet.
|
|
140
|
+
"""
|
|
141
|
+
env_lang = environ.get("COATI_LANG", "").strip().lower()
|
|
142
|
+
|
|
143
|
+
if not env_lang:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if env_lang not in SUPPORTED_LANGUAGES:
|
|
147
|
+
log.warning(f"Invalid COATI_LANG value: {env_lang}. " f"Must be one of {SUPPORTED_LANGUAGES}. Using default.")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
from coati_payroll.model import ConfiguracionGlobal, db
|
|
152
|
+
|
|
153
|
+
with current_app.app_context():
|
|
154
|
+
config = db.session.execute(db.select(ConfiguracionGlobal)).scalar_one_or_none()
|
|
155
|
+
|
|
156
|
+
# Only set from environment if no config exists yet
|
|
157
|
+
if not config:
|
|
158
|
+
config = ConfiguracionGlobal()
|
|
159
|
+
config.idioma = env_lang
|
|
160
|
+
db.session.add(config)
|
|
161
|
+
db.session.commit()
|
|
162
|
+
log.info(f"Language initialized from COATI_LANG: {env_lang}")
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log.warning(f"Error initializing language from environment: {e}")
|
coati_payroll/log.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright 2022 - 2024 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
|
+
#
|
|
15
|
+
"""Configuración de logs."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# <-------------------------------------------------------------------------> #
|
|
20
|
+
# Standard library
|
|
21
|
+
# <-------------------------------------------------------------------------> #
|
|
22
|
+
import logging
|
|
23
|
+
from os import environ
|
|
24
|
+
from sys import stdout
|
|
25
|
+
|
|
26
|
+
# <-------------------------------------------------------------------------> #
|
|
27
|
+
# Third-party libraries
|
|
28
|
+
# <-------------------------------------------------------------------------> #
|
|
29
|
+
|
|
30
|
+
# <-------------------------------------------------------------------------> #
|
|
31
|
+
# Local modules
|
|
32
|
+
# <-------------------------------------------------------------------------> #
|
|
33
|
+
|
|
34
|
+
# Definir nivel TRACE
|
|
35
|
+
TRACE_LEVEL_NUM = 5
|
|
36
|
+
logging.addLevelName(TRACE_LEVEL_NUM, "TRACE")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Método adicional para usar logger.trace(...)
|
|
40
|
+
def trace(self, message, *args, **kwargs):
|
|
41
|
+
"""Log a message with TRACE level."""
|
|
42
|
+
if self.isEnabledFor(TRACE_LEVEL_NUM):
|
|
43
|
+
self._log(TRACE_LEVEL_NUM, message, args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
logging.Logger.trace = trace
|
|
47
|
+
|
|
48
|
+
# Configurar nivel desde variable de entorno (default: INFO)
|
|
49
|
+
log_level_str = environ.get("LOG_LEVEL", "INFO").upper()
|
|
50
|
+
|
|
51
|
+
# Soporte de niveles personalizados
|
|
52
|
+
custom_levels = {
|
|
53
|
+
"TRACE": TRACE_LEVEL_NUM,
|
|
54
|
+
"DEBUG": logging.DEBUG,
|
|
55
|
+
"INFO": logging.INFO,
|
|
56
|
+
"WARNING": logging.WARNING,
|
|
57
|
+
"ERROR": logging.ERROR,
|
|
58
|
+
"CRITICAL": logging.CRITICAL,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Nivel numérico, default INFO si no coincide
|
|
62
|
+
numeric_level = custom_levels.get(log_level_str, logging.INFO)
|
|
63
|
+
|
|
64
|
+
# Configurar logger raíz
|
|
65
|
+
root_logger = logging.getLogger("now_lms")
|
|
66
|
+
root_logger.setLevel(numeric_level)
|
|
67
|
+
|
|
68
|
+
# Handler solo para stdout
|
|
69
|
+
console_handler = logging.StreamHandler(stdout)
|
|
70
|
+
console_handler.setLevel(numeric_level)
|
|
71
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s: %(message)s")
|
|
72
|
+
console_handler.setFormatter(formatter)
|
|
73
|
+
root_logger.addHandler(console_handler)
|
|
74
|
+
|
|
75
|
+
# Configurar logger de Flask y Werkzeug al mismo nivel
|
|
76
|
+
logging.getLogger("flask").setLevel(numeric_level)
|
|
77
|
+
logging.getLogger("werkzeug").setLevel(numeric_level)
|
|
78
|
+
|
|
79
|
+
# Configurar logger de SQLAlchemy al nivel WARNING
|
|
80
|
+
logging.getLogger("sqlalchemy").setLevel(logging.WARNING)
|
|
81
|
+
|
|
82
|
+
LOG_LEVEL = root_logger.getEffectiveLevel()
|
|
83
|
+
|
|
84
|
+
log = root_logger
|
|
85
|
+
logger = root_logger
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Cached helper to avoid repeated debug/level checks on every trace call
|
|
89
|
+
_TRACE_ACTIVE: bool | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _compute_trace_active(debug_flag: bool | None = None) -> bool:
|
|
93
|
+
"""Compute whether TRACE logging should be emitted.
|
|
94
|
+
|
|
95
|
+
Prefers an explicit debug_flag, then Flask's current_app.debug (if available),
|
|
96
|
+
then FLASK_DEBUG/FLASK_ENV environment hints. Also verifies the logger is
|
|
97
|
+
actually enabled for TRACE level.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# Determine debug flag
|
|
101
|
+
if debug_flag is None:
|
|
102
|
+
try:
|
|
103
|
+
from flask import current_app
|
|
104
|
+
|
|
105
|
+
debug_flag = bool(getattr(current_app, "debug", False))
|
|
106
|
+
except Exception:
|
|
107
|
+
debug_flag = False
|
|
108
|
+
|
|
109
|
+
if not debug_flag:
|
|
110
|
+
debug_env = environ.get("FLASK_DEBUG") or environ.get("FLASK_ENV")
|
|
111
|
+
if debug_env:
|
|
112
|
+
debug_flag = str(debug_env).lower() in {
|
|
113
|
+
"1",
|
|
114
|
+
"true",
|
|
115
|
+
"yes",
|
|
116
|
+
"on",
|
|
117
|
+
"development",
|
|
118
|
+
"dev",
|
|
119
|
+
"debug",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return bool(debug_flag) and log.isEnabledFor(TRACE_LEVEL_NUM)
|
|
124
|
+
except Exception:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_trace_enabled(*, force_refresh: bool = False, debug_flag: bool | None = None) -> bool:
|
|
129
|
+
"""Return cached TRACE-enabled flag, computing once unless refreshed.
|
|
130
|
+
|
|
131
|
+
This keeps per-log-call overhead minimal while allowing an explicit refresh
|
|
132
|
+
if runtime configuration changes.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
global _TRACE_ACTIVE
|
|
136
|
+
if force_refresh or _TRACE_ACTIVE is None:
|
|
137
|
+
_TRACE_ACTIVE = _compute_trace_active(debug_flag)
|
|
138
|
+
return _TRACE_ACTIVE
|