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,488 @@
|
|
|
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
|
+
"""Routes for nomina execution and management."""
|
|
15
|
+
|
|
16
|
+
from datetime import date
|
|
17
|
+
from flask import flash, jsonify, redirect, render_template, request, url_for
|
|
18
|
+
from flask_login import current_user, login_required
|
|
19
|
+
|
|
20
|
+
from coati_payroll.model import db, Planilla, Nomina, NominaEmpleado, NominaDetalle, NominaNovedad
|
|
21
|
+
from coati_payroll.enums import NominaEstado, NovedadEstado
|
|
22
|
+
from coati_payroll.i18n import _
|
|
23
|
+
from coati_payroll.rbac import require_read_access, require_write_access
|
|
24
|
+
from coati_payroll.vistas.planilla import planilla_bp
|
|
25
|
+
from coati_payroll.vistas.planilla.services import NominaService
|
|
26
|
+
from coati_payroll.queue.tasks import retry_failed_nomina
|
|
27
|
+
|
|
28
|
+
# Constants
|
|
29
|
+
ROUTE_EJECUTAR_NOMINA = "planilla.ejecutar_nomina"
|
|
30
|
+
ROUTE_VER_NOMINA = "planilla.ver_nomina"
|
|
31
|
+
ROUTE_LISTAR_NOMINAS = "planilla.listar_nominas"
|
|
32
|
+
ERROR_NOMINA_NO_PERTENECE = "La nómina no pertenece a esta planilla."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@planilla_bp.route("/<planilla_id>/ejecutar", methods=["GET", "POST"])
|
|
36
|
+
@require_write_access()
|
|
37
|
+
def ejecutar_nomina(planilla_id: str):
|
|
38
|
+
"""Execute a payroll run for a planilla."""
|
|
39
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
40
|
+
|
|
41
|
+
if request.method == "POST":
|
|
42
|
+
periodo_inicio = request.form.get("periodo_inicio")
|
|
43
|
+
periodo_fin = request.form.get("periodo_fin")
|
|
44
|
+
fecha_calculo = request.form.get("fecha_calculo")
|
|
45
|
+
|
|
46
|
+
if not periodo_inicio or not periodo_fin:
|
|
47
|
+
flash(_("Debe especificar el período de la nómina."), "error")
|
|
48
|
+
return redirect(url_for(ROUTE_EJECUTAR_NOMINA, planilla_id=planilla_id))
|
|
49
|
+
|
|
50
|
+
# Parse dates
|
|
51
|
+
try:
|
|
52
|
+
periodo_inicio = date.fromisoformat(periodo_inicio)
|
|
53
|
+
periodo_fin = date.fromisoformat(periodo_fin)
|
|
54
|
+
fecha_calculo = date.fromisoformat(fecha_calculo) if fecha_calculo else date.today()
|
|
55
|
+
except ValueError:
|
|
56
|
+
flash(_("Formato de fecha inválido."), "error")
|
|
57
|
+
return redirect(url_for(ROUTE_EJECUTAR_NOMINA, planilla_id=planilla_id))
|
|
58
|
+
|
|
59
|
+
nomina, errors, warnings = NominaService.ejecutar_nomina(
|
|
60
|
+
planilla=planilla,
|
|
61
|
+
periodo_inicio=periodo_inicio,
|
|
62
|
+
periodo_fin=periodo_fin,
|
|
63
|
+
fecha_calculo=fecha_calculo,
|
|
64
|
+
usuario=current_user.usuario,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if errors:
|
|
68
|
+
for error in errors:
|
|
69
|
+
flash(error, "error")
|
|
70
|
+
|
|
71
|
+
if warnings:
|
|
72
|
+
for warning in warnings:
|
|
73
|
+
flash(warning, "warning")
|
|
74
|
+
|
|
75
|
+
if nomina:
|
|
76
|
+
if nomina.procesamiento_en_background:
|
|
77
|
+
num_empleados = nomina.total_empleados or 0
|
|
78
|
+
flash(
|
|
79
|
+
_(
|
|
80
|
+
"La nómina está siendo calculada en segundo plano. "
|
|
81
|
+
"Se procesarán %(num)d empleados. "
|
|
82
|
+
"Por favor, revise el progreso en unos momentos.",
|
|
83
|
+
num=num_empleados,
|
|
84
|
+
),
|
|
85
|
+
"info",
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
flash(_("Nómina generada exitosamente."), "success")
|
|
89
|
+
return redirect(
|
|
90
|
+
url_for(
|
|
91
|
+
ROUTE_VER_NOMINA,
|
|
92
|
+
planilla_id=planilla_id,
|
|
93
|
+
nomina_id=nomina.id,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
return redirect(url_for(ROUTE_EJECUTAR_NOMINA, planilla_id=planilla_id))
|
|
98
|
+
|
|
99
|
+
# GET - show execution form
|
|
100
|
+
periodo_inicio, periodo_fin = NominaService.calcular_periodo_sugerido(planilla)
|
|
101
|
+
hoy = date.today()
|
|
102
|
+
|
|
103
|
+
# Get last nomina for reference
|
|
104
|
+
ultima_nomina = db.session.execute(
|
|
105
|
+
db.select(Nomina).filter_by(planilla_id=planilla_id).order_by(Nomina.periodo_fin.desc())
|
|
106
|
+
).scalar_one_or_none()
|
|
107
|
+
|
|
108
|
+
return render_template(
|
|
109
|
+
"modules/planilla/ejecutar_nomina.html",
|
|
110
|
+
planilla=planilla,
|
|
111
|
+
periodo_inicio=periodo_inicio,
|
|
112
|
+
periodo_fin=periodo_fin,
|
|
113
|
+
fecha_calculo=hoy,
|
|
114
|
+
ultima_nomina=ultima_nomina,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@planilla_bp.route("/<planilla_id>/nominas")
|
|
119
|
+
@require_read_access()
|
|
120
|
+
def listar_nominas(planilla_id: str):
|
|
121
|
+
"""List all nominas for a planilla."""
|
|
122
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
123
|
+
nominas = (
|
|
124
|
+
db.session.execute(db.select(Nomina).filter_by(planilla_id=planilla_id).order_by(Nomina.periodo_fin.desc()))
|
|
125
|
+
.scalars()
|
|
126
|
+
.all()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return render_template(
|
|
130
|
+
"modules/planilla/listar_nominas.html",
|
|
131
|
+
planilla=planilla,
|
|
132
|
+
nominas=nominas,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>")
|
|
137
|
+
@require_read_access()
|
|
138
|
+
def ver_nomina(planilla_id: str, nomina_id: str):
|
|
139
|
+
"""View details of a specific nomina."""
|
|
140
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
141
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
142
|
+
|
|
143
|
+
if nomina.planilla_id != planilla_id:
|
|
144
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
145
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
146
|
+
|
|
147
|
+
nomina_empleados = db.session.execute(db.select(NominaEmpleado).filter_by(nomina_id=nomina_id)).scalars().all()
|
|
148
|
+
|
|
149
|
+
# Check for errors and warnings in the processing log
|
|
150
|
+
has_errors = _nomina_has_errors(nomina)
|
|
151
|
+
has_warnings = _nomina_has_warnings(nomina)
|
|
152
|
+
|
|
153
|
+
# Get error and warning messages for display
|
|
154
|
+
error_messages = []
|
|
155
|
+
warning_messages = []
|
|
156
|
+
if nomina.log_procesamiento:
|
|
157
|
+
for entry in nomina.log_procesamiento:
|
|
158
|
+
status = entry.get("status") or entry.get("tipo")
|
|
159
|
+
message = entry.get("message") or entry.get("mensaje") or ""
|
|
160
|
+
if status == "error":
|
|
161
|
+
error_messages.append(message)
|
|
162
|
+
elif status in ("warning", "advertencia_contabilidad"):
|
|
163
|
+
warning_messages.append(message)
|
|
164
|
+
|
|
165
|
+
return render_template(
|
|
166
|
+
"modules/planilla/ver_nomina.html",
|
|
167
|
+
planilla=planilla,
|
|
168
|
+
nomina=nomina,
|
|
169
|
+
nomina_empleados=nomina_empleados,
|
|
170
|
+
has_errors=has_errors,
|
|
171
|
+
has_warnings=has_warnings,
|
|
172
|
+
error_messages=error_messages,
|
|
173
|
+
warning_messages=warning_messages,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/empleado/<nomina_empleado_id>")
|
|
178
|
+
@require_read_access()
|
|
179
|
+
def ver_nomina_empleado(planilla_id: str, nomina_id: str, nomina_empleado_id: str):
|
|
180
|
+
"""View details of an employee's payroll."""
|
|
181
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
182
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
183
|
+
nomina_empleado = db.get_or_404(NominaEmpleado, nomina_empleado_id)
|
|
184
|
+
|
|
185
|
+
if nomina_empleado.nomina_id != nomina_id:
|
|
186
|
+
flash(_("El detalle no pertenece a esta nómina."), "error")
|
|
187
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
188
|
+
|
|
189
|
+
detalles = (
|
|
190
|
+
db.session.execute(
|
|
191
|
+
db.select(NominaDetalle).filter_by(nomina_empleado_id=nomina_empleado_id).order_by(NominaDetalle.orden)
|
|
192
|
+
)
|
|
193
|
+
.scalars()
|
|
194
|
+
.all()
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Separate by type
|
|
198
|
+
percepciones = [d for d in detalles if d.tipo == "ingreso"]
|
|
199
|
+
deducciones = [d for d in detalles if d.tipo == "deduccion"]
|
|
200
|
+
prestaciones = [d for d in detalles if d.tipo == "prestacion"]
|
|
201
|
+
|
|
202
|
+
return render_template(
|
|
203
|
+
"modules/planilla/ver_nomina_empleado.html",
|
|
204
|
+
planilla=planilla,
|
|
205
|
+
nomina=nomina,
|
|
206
|
+
nomina_empleado=nomina_empleado,
|
|
207
|
+
percepciones=percepciones,
|
|
208
|
+
deducciones=deducciones,
|
|
209
|
+
prestaciones=prestaciones,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/progreso")
|
|
214
|
+
@require_read_access()
|
|
215
|
+
def progreso_nomina(planilla_id: str, nomina_id: str):
|
|
216
|
+
"""API endpoint to check calculation progress of a nomina."""
|
|
217
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
218
|
+
|
|
219
|
+
if nomina.planilla_id != planilla_id:
|
|
220
|
+
return jsonify({"error": "Nomina does not belong to this planilla"}), 404
|
|
221
|
+
|
|
222
|
+
return jsonify(
|
|
223
|
+
{
|
|
224
|
+
"estado": nomina.estado,
|
|
225
|
+
"total_empleados": nomina.total_empleados or 0,
|
|
226
|
+
"empleados_procesados": nomina.empleados_procesados or 0,
|
|
227
|
+
"empleados_con_error": nomina.empleados_con_error or 0,
|
|
228
|
+
"progreso_porcentaje": (
|
|
229
|
+
int((nomina.empleados_procesados / nomina.total_empleados) * 100)
|
|
230
|
+
if nomina.total_empleados and nomina.total_empleados > 0
|
|
231
|
+
else 0
|
|
232
|
+
),
|
|
233
|
+
"errores_calculo": nomina.errores_calculo or {},
|
|
234
|
+
"procesamiento_en_background": nomina.procesamiento_en_background,
|
|
235
|
+
"empleado_actual": nomina.empleado_actual or "",
|
|
236
|
+
"log_procesamiento": nomina.log_procesamiento or [],
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _nomina_has_errors(nomina: Nomina) -> bool:
|
|
242
|
+
"""Check if a nomina has errors in its processing log."""
|
|
243
|
+
if not nomina.log_procesamiento:
|
|
244
|
+
return False
|
|
245
|
+
for entry in nomina.log_procesamiento:
|
|
246
|
+
status = entry.get("status") or entry.get("tipo")
|
|
247
|
+
if status == "error":
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _nomina_has_warnings(nomina: Nomina) -> bool:
|
|
253
|
+
"""Check if a nomina has warnings in its processing log."""
|
|
254
|
+
if not nomina.log_procesamiento:
|
|
255
|
+
return False
|
|
256
|
+
for entry in nomina.log_procesamiento:
|
|
257
|
+
status = entry.get("status") or entry.get("tipo")
|
|
258
|
+
if status in ("warning", "advertencia_contabilidad"):
|
|
259
|
+
return True
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/aprobar", methods=["POST"])
|
|
264
|
+
@require_write_access()
|
|
265
|
+
def aprobar_nomina(planilla_id: str, nomina_id: str):
|
|
266
|
+
"""Approve a nomina for payment."""
|
|
267
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
268
|
+
|
|
269
|
+
if nomina.planilla_id != planilla_id:
|
|
270
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
271
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
272
|
+
|
|
273
|
+
if nomina.estado != "generado":
|
|
274
|
+
flash(_("Solo se pueden aprobar nóminas en estado 'generado'."), "error")
|
|
275
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
276
|
+
|
|
277
|
+
# Check for errors in the processing log - cannot approve with errors
|
|
278
|
+
if _nomina_has_errors(nomina):
|
|
279
|
+
flash(
|
|
280
|
+
_(
|
|
281
|
+
"No se puede aprobar una nómina con errores de procesamiento. "
|
|
282
|
+
"Corrija los errores y recalcule la nómina antes de aprobar."
|
|
283
|
+
),
|
|
284
|
+
"error",
|
|
285
|
+
)
|
|
286
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
287
|
+
|
|
288
|
+
nomina.estado = "aprobado"
|
|
289
|
+
nomina.modificado_por = current_user.usuario
|
|
290
|
+
db.session.commit()
|
|
291
|
+
|
|
292
|
+
flash(_("Nómina aprobada exitosamente."), "success")
|
|
293
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/aplicar", methods=["POST"])
|
|
297
|
+
@require_write_access()
|
|
298
|
+
def aplicar_nomina(planilla_id: str, nomina_id: str):
|
|
299
|
+
"""Mark a nomina as applied (paid)."""
|
|
300
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
301
|
+
|
|
302
|
+
if nomina.planilla_id != planilla_id:
|
|
303
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
304
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
305
|
+
|
|
306
|
+
if nomina.estado != "aprobado":
|
|
307
|
+
flash(_("Solo se pueden aplicar nóminas en estado 'aprobado'."), "error")
|
|
308
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
309
|
+
|
|
310
|
+
nomina.estado = "aplicado"
|
|
311
|
+
nomina.modificado_por = current_user.usuario
|
|
312
|
+
|
|
313
|
+
# Actualizar estado de todas las novedades asociadas a "ejecutada"
|
|
314
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
315
|
+
empleado_ids = [pe.empleado_id for pe in planilla.planilla_empleados if pe.activo]
|
|
316
|
+
|
|
317
|
+
# Actualizar novedades que corresponden a este período
|
|
318
|
+
novedades = (
|
|
319
|
+
db.session.execute(
|
|
320
|
+
db.select(NominaNovedad).filter(
|
|
321
|
+
NominaNovedad.empleado_id.in_(empleado_ids),
|
|
322
|
+
NominaNovedad.fecha_novedad >= nomina.periodo_inicio,
|
|
323
|
+
NominaNovedad.fecha_novedad <= nomina.periodo_fin,
|
|
324
|
+
NominaNovedad.estado == NovedadEstado.PENDIENTE,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
.scalars()
|
|
328
|
+
.all()
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
for novedad in novedades:
|
|
332
|
+
novedad.estado = NovedadEstado.EJECUTADA
|
|
333
|
+
novedad.modificado_por = current_user.usuario
|
|
334
|
+
|
|
335
|
+
db.session.commit()
|
|
336
|
+
|
|
337
|
+
flash(
|
|
338
|
+
_("Nómina aplicada exitosamente. {} novedad(es) marcadas como ejecutadas.").format(len(novedades)),
|
|
339
|
+
"success",
|
|
340
|
+
)
|
|
341
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/reintentar", methods=["POST"])
|
|
345
|
+
@require_write_access()
|
|
346
|
+
def reintentar_nomina(planilla_id: str, nomina_id: str):
|
|
347
|
+
"""Retry processing a failed nomina."""
|
|
348
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
349
|
+
|
|
350
|
+
if nomina.planilla_id != planilla_id:
|
|
351
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
352
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
353
|
+
|
|
354
|
+
if nomina.estado != NominaEstado.ERROR:
|
|
355
|
+
flash(_("Solo se pueden reintentar nóminas en estado 'error'."), "error")
|
|
356
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
357
|
+
|
|
358
|
+
# Call the retry function
|
|
359
|
+
result = retry_failed_nomina(nomina_id, current_user.usuario)
|
|
360
|
+
|
|
361
|
+
if result.get("success"):
|
|
362
|
+
flash(
|
|
363
|
+
_("Reintento de nómina iniciado exitosamente. El procesamiento se realizará en segundo plano."), "success"
|
|
364
|
+
)
|
|
365
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
366
|
+
else:
|
|
367
|
+
flash(_("Error al reintentar la nómina: {}").format(result.get("error", "Error desconocido")), "error")
|
|
368
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/recalcular", methods=["POST"])
|
|
372
|
+
@require_write_access()
|
|
373
|
+
def recalcular_nomina(planilla_id: str, nomina_id: str):
|
|
374
|
+
"""Recalculate an existing nomina."""
|
|
375
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
376
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
377
|
+
|
|
378
|
+
if nomina.planilla_id != planilla_id:
|
|
379
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
380
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
381
|
+
|
|
382
|
+
if nomina.estado == "aplicado":
|
|
383
|
+
flash(_("No se puede recalcular una nómina en estado 'aplicado' (pagada)."), "error")
|
|
384
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
385
|
+
|
|
386
|
+
new_nomina, errors, warnings = NominaService.recalcular_nomina(nomina, planilla, current_user.usuario)
|
|
387
|
+
|
|
388
|
+
if errors:
|
|
389
|
+
for error in errors:
|
|
390
|
+
flash(error, "error")
|
|
391
|
+
|
|
392
|
+
if warnings:
|
|
393
|
+
for warning in warnings:
|
|
394
|
+
flash(warning, "warning")
|
|
395
|
+
|
|
396
|
+
if new_nomina:
|
|
397
|
+
flash(_("Nómina recalculada exitosamente."), "success")
|
|
398
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=new_nomina.id))
|
|
399
|
+
else:
|
|
400
|
+
flash(_("Error al recalcular la nómina."), "error")
|
|
401
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/log")
|
|
405
|
+
@require_read_access()
|
|
406
|
+
def ver_log_nomina(planilla_id: str, nomina_id: str):
|
|
407
|
+
"""View execution log for a nomina including warnings and errors."""
|
|
408
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
409
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
410
|
+
|
|
411
|
+
if nomina.planilla_id != planilla_id:
|
|
412
|
+
flash(_(ERROR_NOMINA_NO_PERTENECE), "error")
|
|
413
|
+
return redirect(url_for(ROUTE_LISTAR_NOMINAS, planilla_id=planilla_id))
|
|
414
|
+
|
|
415
|
+
# Get log entries
|
|
416
|
+
log_entries = nomina.log_procesamiento or []
|
|
417
|
+
|
|
418
|
+
# Get comprobante warnings if exists
|
|
419
|
+
from coati_payroll.model import ComprobanteContable
|
|
420
|
+
|
|
421
|
+
comprobante = db.session.execute(db.select(ComprobanteContable).filter_by(nomina_id=nomina_id)).scalar_one_or_none()
|
|
422
|
+
|
|
423
|
+
comprobante_warnings = comprobante.advertencias if comprobante else []
|
|
424
|
+
|
|
425
|
+
return render_template(
|
|
426
|
+
"modules/planilla/log_nomina.html",
|
|
427
|
+
planilla=planilla,
|
|
428
|
+
nomina=nomina,
|
|
429
|
+
log_entries=log_entries,
|
|
430
|
+
comprobante_warnings=comprobante_warnings,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/regenerar-comprobante", methods=["POST"])
|
|
435
|
+
@login_required
|
|
436
|
+
@require_write_access()
|
|
437
|
+
def regenerar_comprobante_contable(planilla_id: str, nomina_id: str):
|
|
438
|
+
"""Regenerate accounting voucher for an applied/paid nomina without recalculating.
|
|
439
|
+
|
|
440
|
+
This is useful when accounting configuration was incomplete at the time of calculation
|
|
441
|
+
and has been updated afterwards. Only regenerates the accounting entries based on
|
|
442
|
+
existing payroll calculations.
|
|
443
|
+
"""
|
|
444
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
445
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
446
|
+
|
|
447
|
+
if nomina.planilla_id != planilla_id:
|
|
448
|
+
flash(_("La nómina no pertenece a esta planilla."), "error")
|
|
449
|
+
return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
|
|
450
|
+
|
|
451
|
+
# Only allow for applied or paid nominas
|
|
452
|
+
from coati_payroll.enums import NominaEstado
|
|
453
|
+
|
|
454
|
+
if nomina.estado not in (NominaEstado.APLICADO, NominaEstado.PAGADO):
|
|
455
|
+
flash(
|
|
456
|
+
_(
|
|
457
|
+
"Solo se puede regenerar el comprobante contable para nóminas en estado 'aplicado' o 'pagado'. "
|
|
458
|
+
"Para nóminas en otros estados, use 'recalcular'."
|
|
459
|
+
),
|
|
460
|
+
"error",
|
|
461
|
+
)
|
|
462
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
from coati_payroll.nomina_engine.services.accounting_voucher_service import AccountingVoucherService
|
|
466
|
+
from flask_login import current_user
|
|
467
|
+
|
|
468
|
+
accounting_service = AccountingVoucherService(db.session)
|
|
469
|
+
|
|
470
|
+
# Regenerate voucher using existing nomina data
|
|
471
|
+
fecha_calculo = nomina.fecha_calculo_original or nomina.periodo_fin
|
|
472
|
+
usuario = current_user.nombre_usuario if current_user and current_user.is_authenticated else None
|
|
473
|
+
comprobante = accounting_service.generate_accounting_voucher(nomina, planilla, fecha_calculo, usuario)
|
|
474
|
+
|
|
475
|
+
db.session.commit()
|
|
476
|
+
|
|
477
|
+
flash(_("Comprobante contable regenerado exitosamente."), "success")
|
|
478
|
+
|
|
479
|
+
# Show warnings if configuration is still incomplete
|
|
480
|
+
if comprobante.advertencias:
|
|
481
|
+
for warning in comprobante.advertencias:
|
|
482
|
+
flash(warning, "warning")
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
db.session.rollback()
|
|
486
|
+
flash(_("Error al regenerar comprobante contable: {}").format(str(e)), "error")
|
|
487
|
+
|
|
488
|
+
return redirect(url_for("planilla.ver_log_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|