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,451 @@
|
|
|
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
|
+
"""Vacation service for integration with payroll engine.
|
|
15
|
+
|
|
16
|
+
This module provides the service layer for vacation accrual and usage
|
|
17
|
+
during payroll execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# <-------------------------------------------------------------------------> #
|
|
23
|
+
# Standard library
|
|
24
|
+
# <-------------------------------------------------------------------------> #
|
|
25
|
+
from datetime import date
|
|
26
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
# <-------------------------------------------------------------------------> #
|
|
30
|
+
# Third party libraries
|
|
31
|
+
# <-------------------------------------------------------------------------> #
|
|
32
|
+
|
|
33
|
+
# <-------------------------------------------------------------------------> #
|
|
34
|
+
# Local modules
|
|
35
|
+
# <-------------------------------------------------------------------------> #
|
|
36
|
+
from coati_payroll.enums import VacationLedgerType, AccrualMethod, AccrualFrequency
|
|
37
|
+
from coati_payroll.log import log
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from coati_payroll.model import (
|
|
41
|
+
Empleado,
|
|
42
|
+
Planilla,
|
|
43
|
+
VacationPolicy,
|
|
44
|
+
VacationAccount,
|
|
45
|
+
NominaEmpleado,
|
|
46
|
+
ConfiguracionCalculos,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VacationService:
|
|
51
|
+
"""Service for vacation accrual and usage during payroll execution."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, planilla: Planilla, periodo_inicio: date, periodo_fin: date):
|
|
54
|
+
"""Initialize vacation service.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
planilla: The payroll being executed
|
|
58
|
+
periodo_inicio: Start date of payroll period
|
|
59
|
+
periodo_fin: End date of payroll period
|
|
60
|
+
"""
|
|
61
|
+
self.planilla = planilla
|
|
62
|
+
self.periodo_inicio = periodo_inicio
|
|
63
|
+
self.periodo_fin = periodo_fin
|
|
64
|
+
|
|
65
|
+
def _obtener_config_calculos(self) -> ConfiguracionCalculos:
|
|
66
|
+
"""Get calculation configuration for the current planilla.
|
|
67
|
+
|
|
68
|
+
Returns configuration specific to the planilla's company, or global defaults.
|
|
69
|
+
Always returns a valid configuration object with defaults if none exists.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ConfiguracionCalculos instance with appropriate values
|
|
73
|
+
"""
|
|
74
|
+
from coati_payroll.model import db, ConfiguracionCalculos
|
|
75
|
+
|
|
76
|
+
empresa_id = self.planilla.empresa_id if self.planilla else None
|
|
77
|
+
|
|
78
|
+
# Try to find company-specific configuration
|
|
79
|
+
if empresa_id:
|
|
80
|
+
config = (
|
|
81
|
+
db.session.execute(
|
|
82
|
+
db.select(ConfiguracionCalculos).filter(
|
|
83
|
+
ConfiguracionCalculos.empresa_id == empresa_id,
|
|
84
|
+
ConfiguracionCalculos.activo.is_(True),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
.scalars()
|
|
88
|
+
.first()
|
|
89
|
+
)
|
|
90
|
+
if config:
|
|
91
|
+
return config
|
|
92
|
+
|
|
93
|
+
# Try to find global default (no empresa_id, no pais_id)
|
|
94
|
+
config = (
|
|
95
|
+
db.session.execute(
|
|
96
|
+
db.select(ConfiguracionCalculos).filter(
|
|
97
|
+
ConfiguracionCalculos.empresa_id.is_(None),
|
|
98
|
+
ConfiguracionCalculos.pais_id.is_(None),
|
|
99
|
+
ConfiguracionCalculos.activo.is_(True),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
.scalars()
|
|
103
|
+
.first()
|
|
104
|
+
)
|
|
105
|
+
if config:
|
|
106
|
+
return config
|
|
107
|
+
|
|
108
|
+
# If no configuration exists, return a default instance (not saved to DB)
|
|
109
|
+
# This ensures backward compatibility with existing tests
|
|
110
|
+
from decimal import Decimal
|
|
111
|
+
|
|
112
|
+
return ConfiguracionCalculos(
|
|
113
|
+
empresa_id=None,
|
|
114
|
+
pais_id=None,
|
|
115
|
+
dias_mes_nomina=30,
|
|
116
|
+
dias_anio_nomina=365,
|
|
117
|
+
horas_jornada_diaria=Decimal("8.00"),
|
|
118
|
+
dias_mes_vacaciones=30,
|
|
119
|
+
dias_anio_vacaciones=365,
|
|
120
|
+
considerar_bisiesto_vacaciones=True,
|
|
121
|
+
dias_anio_financiero=365,
|
|
122
|
+
meses_anio_financiero=12,
|
|
123
|
+
dias_quincena=15,
|
|
124
|
+
dias_mes_antiguedad=30,
|
|
125
|
+
dias_anio_antiguedad=365,
|
|
126
|
+
activo=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def acumular_vacaciones_empleado(
|
|
130
|
+
self, empleado: Empleado, nomina_empleado: NominaEmpleado, usuario: str | None = None
|
|
131
|
+
) -> Decimal:
|
|
132
|
+
"""Accumulate vacation for an employee during payroll execution.
|
|
133
|
+
|
|
134
|
+
This method is called during payroll processing to automatically
|
|
135
|
+
accrue vacation time based on the employee's vacation policy.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
empleado: The employee to accrue vacation for
|
|
139
|
+
nomina_empleado: The payroll record for this employee
|
|
140
|
+
usuario: Username executing the payroll
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The amount of vacation accrued
|
|
144
|
+
"""
|
|
145
|
+
from coati_payroll.model import db, VacationAccount, VacationLedger
|
|
146
|
+
|
|
147
|
+
# Get active vacation account for this employee and payroll
|
|
148
|
+
account = db.session.execute(
|
|
149
|
+
db.select(VacationAccount)
|
|
150
|
+
.filter(
|
|
151
|
+
VacationAccount.empleado_id == empleado.id,
|
|
152
|
+
VacationAccount.activo.is_(True),
|
|
153
|
+
)
|
|
154
|
+
.join(VacationAccount.policy)
|
|
155
|
+
.filter(
|
|
156
|
+
(VacationAccount.policy.has(planilla_id=self.planilla.id))
|
|
157
|
+
| (VacationAccount.policy.has(empresa_id=self.planilla.empresa_id))
|
|
158
|
+
| ((VacationAccount.policy.has(planilla_id=None)) & (VacationAccount.policy.has(empresa_id=None)))
|
|
159
|
+
)
|
|
160
|
+
).scalar_one_or_none()
|
|
161
|
+
|
|
162
|
+
if not account:
|
|
163
|
+
log.debug(
|
|
164
|
+
f"No active vacation account found for employee {empleado.codigo_empleado} "
|
|
165
|
+
f"in payroll {self.planilla.nombre}"
|
|
166
|
+
)
|
|
167
|
+
return Decimal("0.00")
|
|
168
|
+
|
|
169
|
+
policy = account.policy
|
|
170
|
+
|
|
171
|
+
# Check if employee meets minimum service requirement
|
|
172
|
+
if empleado.fecha_alta:
|
|
173
|
+
dias_servicio = (self.periodo_fin - empleado.fecha_alta).days
|
|
174
|
+
if dias_servicio < policy.min_service_days:
|
|
175
|
+
log.debug(
|
|
176
|
+
f"Employee {empleado.codigo_empleado} has not met minimum service days "
|
|
177
|
+
f"({dias_servicio} < {policy.min_service_days})"
|
|
178
|
+
)
|
|
179
|
+
return Decimal("0.00")
|
|
180
|
+
|
|
181
|
+
# Calculate accrual amount based on policy
|
|
182
|
+
accrual_amount = self._calcular_acumulacion(empleado, account, nomina_empleado)
|
|
183
|
+
|
|
184
|
+
if accrual_amount <= 0:
|
|
185
|
+
return Decimal("0.00")
|
|
186
|
+
|
|
187
|
+
# Check max balance limit
|
|
188
|
+
if policy.max_balance:
|
|
189
|
+
if account.current_balance + accrual_amount > policy.max_balance:
|
|
190
|
+
# Cap at max balance
|
|
191
|
+
accrual_amount = policy.max_balance - account.current_balance
|
|
192
|
+
if accrual_amount <= 0:
|
|
193
|
+
log.debug(
|
|
194
|
+
f"Employee {empleado.codigo_empleado} has reached max vacation balance "
|
|
195
|
+
f"({account.current_balance} >= {policy.max_balance})"
|
|
196
|
+
)
|
|
197
|
+
return Decimal("0.00")
|
|
198
|
+
|
|
199
|
+
# Create ledger entry for accrual
|
|
200
|
+
ledger_entry = VacationLedger(
|
|
201
|
+
account_id=account.id,
|
|
202
|
+
empleado_id=empleado.id,
|
|
203
|
+
fecha=self.periodo_fin,
|
|
204
|
+
entry_type=VacationLedgerType.ACCRUAL,
|
|
205
|
+
quantity=accrual_amount,
|
|
206
|
+
source="payroll",
|
|
207
|
+
reference_id=nomina_empleado.id,
|
|
208
|
+
reference_type="nomina_empleado",
|
|
209
|
+
observaciones=f"Acumulación automática en nómina del {self.periodo_inicio} al {self.periodo_fin}",
|
|
210
|
+
creado_por=usuario,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Update account balance
|
|
214
|
+
account.current_balance = account.current_balance + accrual_amount
|
|
215
|
+
account.last_accrual_date = self.periodo_fin
|
|
216
|
+
account.modificado_por = usuario
|
|
217
|
+
|
|
218
|
+
ledger_entry.balance_after = account.current_balance
|
|
219
|
+
|
|
220
|
+
db.session.add(ledger_entry)
|
|
221
|
+
db.session.flush()
|
|
222
|
+
|
|
223
|
+
log.info(
|
|
224
|
+
f"Accrued {accrual_amount} {policy.unit_type} vacation for employee "
|
|
225
|
+
f"{empleado.codigo_empleado} (new balance: {account.current_balance})"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return accrual_amount
|
|
229
|
+
|
|
230
|
+
def _calcular_acumulacion(
|
|
231
|
+
self, empleado: Empleado, account: VacationAccount, nomina_empleado: NominaEmpleado
|
|
232
|
+
) -> Decimal:
|
|
233
|
+
"""Calculate vacation accrual amount based on policy.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
empleado: The employee
|
|
237
|
+
account: The vacation account
|
|
238
|
+
nomina_empleado: The payroll record
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Amount to accrue
|
|
242
|
+
"""
|
|
243
|
+
policy = account.policy
|
|
244
|
+
|
|
245
|
+
if policy.accrual_method == AccrualMethod.PERIODIC:
|
|
246
|
+
return self._calcular_acumulacion_periodica(policy)
|
|
247
|
+
elif policy.accrual_method == AccrualMethod.PROPORTIONAL:
|
|
248
|
+
return self._calcular_acumulacion_proporcional(empleado, policy, nomina_empleado)
|
|
249
|
+
elif policy.accrual_method == AccrualMethod.SENIORITY:
|
|
250
|
+
return self._calcular_acumulacion_antiguedad(empleado, policy)
|
|
251
|
+
else:
|
|
252
|
+
log.warning(f"Unknown accrual method: {policy.accrual_method}")
|
|
253
|
+
return Decimal("0.00")
|
|
254
|
+
|
|
255
|
+
def _calcular_acumulacion_periodica(self, policy: VacationPolicy) -> Decimal:
|
|
256
|
+
"""Calculate periodic accrual (fixed amount per period).
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
policy: The vacation policy
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Accrual amount
|
|
263
|
+
"""
|
|
264
|
+
# For periodic accrual, the rate is the amount per configured frequency
|
|
265
|
+
# If frequency matches payroll period, use the rate directly
|
|
266
|
+
# Otherwise, prorate based on actual days in period
|
|
267
|
+
|
|
268
|
+
dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
|
|
269
|
+
|
|
270
|
+
# Get configuration for vacation calculations
|
|
271
|
+
config = self._obtener_config_calculos()
|
|
272
|
+
|
|
273
|
+
# Determine expected days for frequency using configuration
|
|
274
|
+
if policy.accrual_frequency == AccrualFrequency.MONTHLY:
|
|
275
|
+
dias_esperados = config.dias_mes_vacaciones
|
|
276
|
+
elif policy.accrual_frequency == AccrualFrequency.BIWEEKLY:
|
|
277
|
+
dias_esperados = config.dias_quincena
|
|
278
|
+
elif policy.accrual_frequency == AccrualFrequency.ANNUAL:
|
|
279
|
+
dias_esperados = config.dias_anio_vacaciones
|
|
280
|
+
else:
|
|
281
|
+
dias_esperados = config.dias_mes_vacaciones
|
|
282
|
+
|
|
283
|
+
# Prorate if period doesn't match frequency
|
|
284
|
+
if dias_periodo == dias_esperados:
|
|
285
|
+
return policy.accrual_rate
|
|
286
|
+
else:
|
|
287
|
+
# Prorate based on days
|
|
288
|
+
return (policy.accrual_rate * Decimal(dias_periodo) / Decimal(dias_esperados)).quantize(
|
|
289
|
+
Decimal("0.0001"), rounding=ROUND_HALF_UP
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _calcular_acumulacion_proporcional(
|
|
293
|
+
self, empleado: Empleado, policy: VacationPolicy, nomina_empleado: NominaEmpleado
|
|
294
|
+
) -> Decimal:
|
|
295
|
+
"""Calculate proportional accrual (based on worked days/hours).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
empleado: The employee
|
|
299
|
+
policy: The vacation policy
|
|
300
|
+
nomina_empleado: The payroll record
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Accrual amount
|
|
304
|
+
"""
|
|
305
|
+
# For proportional accrual, calculate based on actual worked days/hours
|
|
306
|
+
# This requires tracking in the payroll record
|
|
307
|
+
|
|
308
|
+
dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
|
|
309
|
+
|
|
310
|
+
if policy.accrual_basis == "days_worked":
|
|
311
|
+
# Assume full days worked for now (could be enhanced to track absences)
|
|
312
|
+
dias_trabajados = Decimal(dias_periodo)
|
|
313
|
+
return (policy.accrual_rate * dias_trabajados).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
|
314
|
+
elif policy.accrual_basis == "hours_worked":
|
|
315
|
+
# Calculate based on hours (would need hours tracking in payroll)
|
|
316
|
+
# For now, estimate based on standard hours from configuration
|
|
317
|
+
config = self._obtener_config_calculos()
|
|
318
|
+
horas_estandar = Decimal(str(config.horas_jornada_diaria)) * Decimal(dias_periodo)
|
|
319
|
+
return (policy.accrual_rate * horas_estandar).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
|
320
|
+
else:
|
|
321
|
+
return Decimal("0.00")
|
|
322
|
+
|
|
323
|
+
def _calcular_acumulacion_antiguedad(self, empleado: Empleado, policy: VacationPolicy) -> Decimal:
|
|
324
|
+
"""Calculate seniority-based accrual (tiered by years of service).
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
empleado: The employee
|
|
328
|
+
policy: The vacation policy
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Accrual amount
|
|
332
|
+
"""
|
|
333
|
+
if not empleado.fecha_alta or not policy.seniority_tiers:
|
|
334
|
+
return Decimal("0.00")
|
|
335
|
+
|
|
336
|
+
# Get configuration for seniority calculations
|
|
337
|
+
config = self._obtener_config_calculos()
|
|
338
|
+
|
|
339
|
+
# Calculate years of service
|
|
340
|
+
# Use configured days per year, with leap year consideration if enabled
|
|
341
|
+
dias_anio = Decimal(str(config.dias_anio_antiguedad))
|
|
342
|
+
if config.considerar_bisiesto_vacaciones:
|
|
343
|
+
# Use 365.25 to account for leap years
|
|
344
|
+
dias_anio = Decimal("365.25")
|
|
345
|
+
anos_servicio = (self.periodo_fin - empleado.fecha_alta).days / float(dias_anio)
|
|
346
|
+
|
|
347
|
+
# Find applicable tier
|
|
348
|
+
rate = Decimal("0.00")
|
|
349
|
+
for tier in sorted(policy.seniority_tiers, key=lambda t: t.get("years", 0), reverse=True):
|
|
350
|
+
if anos_servicio >= tier.get("years", 0):
|
|
351
|
+
rate = Decimal(str(tier.get("rate", 0)))
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
if rate == 0:
|
|
355
|
+
return Decimal("0.00")
|
|
356
|
+
|
|
357
|
+
# For seniority, rate is typically annual, so prorate for period
|
|
358
|
+
if policy.accrual_frequency == AccrualFrequency.ANNUAL:
|
|
359
|
+
dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
|
|
360
|
+
dias_anio = Decimal(str(config.dias_anio_vacaciones))
|
|
361
|
+
return (rate * Decimal(dias_periodo) / dias_anio).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
|
362
|
+
else:
|
|
363
|
+
# If frequency is monthly/biweekly, divide rate accordingly
|
|
364
|
+
meses_anio = Decimal(str(config.meses_anio_financiero))
|
|
365
|
+
return (rate / meses_anio).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
|
366
|
+
|
|
367
|
+
def procesar_novedades_vacaciones(
|
|
368
|
+
self, empleado: Empleado, novedades: dict | list, usuario: str | None = None
|
|
369
|
+
) -> Decimal:
|
|
370
|
+
"""Process vacation novelties (leave taken) during payroll execution.
|
|
371
|
+
|
|
372
|
+
This method processes vacation leave novelties that have been approved
|
|
373
|
+
and creates ledger entries to reduce the vacation balance.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
empleado: The employee
|
|
377
|
+
novedades: Dictionary or list of novelties to process
|
|
378
|
+
usuario: Username executing the payroll
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Total vacation days/hours used
|
|
382
|
+
"""
|
|
383
|
+
from coati_payroll.model import db, VacationNovelty, VacationLedger, NominaNovedad
|
|
384
|
+
|
|
385
|
+
total_usado = Decimal("0.00")
|
|
386
|
+
|
|
387
|
+
# Query vacation-related novedades for this employee in this period
|
|
388
|
+
nomina_novedades = (
|
|
389
|
+
db.session.execute(
|
|
390
|
+
db.select(NominaNovedad).filter(
|
|
391
|
+
NominaNovedad.empleado_id == empleado.id,
|
|
392
|
+
NominaNovedad.es_descanso_vacaciones.is_(True),
|
|
393
|
+
NominaNovedad.fecha_novedad >= self.periodo_inicio,
|
|
394
|
+
NominaNovedad.fecha_novedad <= self.periodo_fin,
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
.scalars()
|
|
398
|
+
.all()
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
for nomina_novedad in nomina_novedades:
|
|
402
|
+
# Get the associated vacation novelty
|
|
403
|
+
if not nomina_novedad.vacation_novelty_id:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
vac_novelty = db.session.get(VacationNovelty, nomina_novedad.vacation_novelty_id)
|
|
407
|
+
|
|
408
|
+
if not vac_novelty or vac_novelty.estado != "aprobado":
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
# Skip if already processed (has ledger entry)
|
|
412
|
+
if vac_novelty.ledger_entry_id:
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
account = vac_novelty.account
|
|
416
|
+
|
|
417
|
+
# Create ledger entry for usage
|
|
418
|
+
ledger_entry = VacationLedger(
|
|
419
|
+
account_id=account.id,
|
|
420
|
+
empleado_id=empleado.id,
|
|
421
|
+
fecha=self.periodo_fin,
|
|
422
|
+
entry_type=VacationLedgerType.USAGE,
|
|
423
|
+
quantity=-abs(vac_novelty.units), # Negative for usage
|
|
424
|
+
source="novelty",
|
|
425
|
+
reference_id=vac_novelty.id,
|
|
426
|
+
reference_type="vacation_novelty",
|
|
427
|
+
observaciones=f"Vacaciones del {vac_novelty.start_date} al {vac_novelty.end_date}",
|
|
428
|
+
creado_por=usuario,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Update account balance
|
|
432
|
+
account.current_balance = account.current_balance - abs(vac_novelty.units)
|
|
433
|
+
account.modificado_por = usuario
|
|
434
|
+
|
|
435
|
+
ledger_entry.balance_after = account.current_balance
|
|
436
|
+
|
|
437
|
+
# Link ledger entry to novelty
|
|
438
|
+
vac_novelty.ledger_entry_id = ledger_entry.id
|
|
439
|
+
vac_novelty.estado = "disfrutado"
|
|
440
|
+
|
|
441
|
+
db.session.add(ledger_entry)
|
|
442
|
+
db.session.flush()
|
|
443
|
+
|
|
444
|
+
total_usado = total_usado + abs(vac_novelty.units)
|
|
445
|
+
|
|
446
|
+
log.info(
|
|
447
|
+
f"Processed vacation usage of {abs(vac_novelty.units)} for employee "
|
|
448
|
+
f"{empleado.codigo_empleado} (new balance: {account.current_balance})"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return total_usado
|
coati_payroll/version.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
Data model for the payroll module.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = "0.0.2"
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
"""Vistas module for the payroll application."""
|
|
15
|
+
|
|
16
|
+
from coati_payroll.vistas.user import user_bp
|
|
17
|
+
from coati_payroll.vistas.currency import currency_bp
|
|
18
|
+
from coati_payroll.vistas.exchange_rate import exchange_rate_bp
|
|
19
|
+
from coati_payroll.vistas.employee import employee_bp
|
|
20
|
+
from coati_payroll.vistas.custom_field import custom_field_bp
|
|
21
|
+
from coati_payroll.vistas.calculation_rule import calculation_rule_bp
|
|
22
|
+
from coati_payroll.vistas.payroll_concepts import (
|
|
23
|
+
percepcion_bp,
|
|
24
|
+
deduccion_bp,
|
|
25
|
+
prestacion_bp,
|
|
26
|
+
)
|
|
27
|
+
from coati_payroll.vistas.planilla import planilla_bp
|
|
28
|
+
from coati_payroll.vistas.tipo_planilla import tipo_planilla_bp
|
|
29
|
+
from coati_payroll.vistas.prestamo import prestamo_bp
|
|
30
|
+
from coati_payroll.vistas.empresa import empresa_bp
|
|
31
|
+
from coati_payroll.vistas.configuracion import configuracion_bp
|
|
32
|
+
from coati_payroll.vistas.carga_inicial_prestacion import carga_inicial_prestacion_bp
|
|
33
|
+
from coati_payroll.vistas.vacation import vacation_bp
|
|
34
|
+
from coati_payroll.vistas.prestacion import prestacion_management_bp
|
|
35
|
+
from coati_payroll.vistas.report import report_bp
|
|
36
|
+
from coati_payroll.vistas.settings import settings_bp
|
|
37
|
+
from coati_payroll.vistas.config_calculos import config_calculos_bp
|
|
38
|
+
from coati_payroll.vistas.liquidacion import liquidacion_bp
|
|
39
|
+
from coati_payroll.vistas.plugins import plugins_bp
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"user_bp",
|
|
43
|
+
"currency_bp",
|
|
44
|
+
"exchange_rate_bp",
|
|
45
|
+
"employee_bp",
|
|
46
|
+
"custom_field_bp",
|
|
47
|
+
"calculation_rule_bp",
|
|
48
|
+
"percepcion_bp",
|
|
49
|
+
"deduccion_bp",
|
|
50
|
+
"prestacion_bp",
|
|
51
|
+
"planilla_bp",
|
|
52
|
+
"tipo_planilla_bp",
|
|
53
|
+
"prestamo_bp",
|
|
54
|
+
"empresa_bp",
|
|
55
|
+
"configuracion_bp",
|
|
56
|
+
"carga_inicial_prestacion_bp",
|
|
57
|
+
"vacation_bp",
|
|
58
|
+
"prestacion_management_bp",
|
|
59
|
+
"report_bp",
|
|
60
|
+
"settings_bp",
|
|
61
|
+
"plugins_bp",
|
|
62
|
+
"config_calculos_bp",
|
|
63
|
+
"liquidacion_bp",
|
|
64
|
+
]
|