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,238 @@
|
|
|
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 managing planilla associations."""
|
|
15
|
+
|
|
16
|
+
from datetime import date
|
|
17
|
+
from flask import flash, redirect, request, url_for
|
|
18
|
+
from flask_login import current_user
|
|
19
|
+
|
|
20
|
+
from coati_payroll.model import (
|
|
21
|
+
db,
|
|
22
|
+
Planilla,
|
|
23
|
+
Empleado,
|
|
24
|
+
PlanillaEmpleado,
|
|
25
|
+
PlanillaIngreso,
|
|
26
|
+
PlanillaDeduccion,
|
|
27
|
+
PlanillaPrestacion,
|
|
28
|
+
PlanillaReglaCalculo,
|
|
29
|
+
)
|
|
30
|
+
from coati_payroll.i18n import _
|
|
31
|
+
from coati_payroll.rbac import require_write_access
|
|
32
|
+
from coati_payroll.vistas.planilla import planilla_bp
|
|
33
|
+
from coati_payroll.vistas.planilla.helpers.association_helpers import agregar_asociacion
|
|
34
|
+
from coati_payroll.vistas.planilla.validators.planilla_validators import PlanillaValidator
|
|
35
|
+
|
|
36
|
+
# Constants
|
|
37
|
+
ROUTE_CONFIG_EMPLEADOS = "planilla.config_empleados"
|
|
38
|
+
ROUTE_CONFIG_PERCEPCIONES = "planilla.config_percepciones"
|
|
39
|
+
ROUTE_CONFIG_DEDUCCIONES = "planilla.config_deducciones"
|
|
40
|
+
ROUTE_CONFIG_PRESTACIONES = "planilla.config_prestaciones"
|
|
41
|
+
ROUTE_CONFIG_REGLAS = "planilla.config_reglas"
|
|
42
|
+
ERROR_NOT_FOUND = "no encontrada"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@planilla_bp.route("/<planilla_id>/empleado/add", methods=["POST"])
|
|
46
|
+
@require_write_access()
|
|
47
|
+
def add_empleado(planilla_id: str):
|
|
48
|
+
"""Add an employee to the planilla."""
|
|
49
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
50
|
+
empleado_id = request.form.get("empleado_id")
|
|
51
|
+
|
|
52
|
+
if not empleado_id:
|
|
53
|
+
flash(_("Debe seleccionar un empleado."), "error")
|
|
54
|
+
return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
|
|
55
|
+
|
|
56
|
+
# Check if already exists
|
|
57
|
+
existing = db.session.execute(
|
|
58
|
+
db.select(PlanillaEmpleado).filter_by(planilla_id=planilla_id, empleado_id=empleado_id)
|
|
59
|
+
).scalar_one_or_none()
|
|
60
|
+
|
|
61
|
+
if existing:
|
|
62
|
+
flash(_("El empleado ya está asignado a esta planilla."), "warning")
|
|
63
|
+
return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
|
|
64
|
+
|
|
65
|
+
# Validate that employee and planilla belong to the same company
|
|
66
|
+
empleado = db.get_or_404(Empleado, empleado_id)
|
|
67
|
+
is_valid, error_message = PlanillaValidator.validar_empresa_empleado(planilla, empleado)
|
|
68
|
+
if not is_valid:
|
|
69
|
+
flash(_(error_message), "error")
|
|
70
|
+
return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
|
|
71
|
+
|
|
72
|
+
association = PlanillaEmpleado(
|
|
73
|
+
planilla_id=planilla_id,
|
|
74
|
+
empleado_id=empleado_id,
|
|
75
|
+
fecha_inicio=date.today(),
|
|
76
|
+
activo=True,
|
|
77
|
+
creado_por=current_user.usuario,
|
|
78
|
+
)
|
|
79
|
+
db.session.add(association)
|
|
80
|
+
db.session.commit()
|
|
81
|
+
flash(_("Empleado agregado exitosamente."), "success")
|
|
82
|
+
return redirect(url_for("planilla.config_empleados", planilla_id=planilla_id))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@planilla_bp.route("/<planilla_id>/empleado/<association_id>/remove", methods=["POST"])
|
|
86
|
+
@require_write_access()
|
|
87
|
+
def remove_empleado(planilla_id: str, association_id: str):
|
|
88
|
+
"""Remove an employee from the planilla."""
|
|
89
|
+
association = db.get_or_404(PlanillaEmpleado, association_id)
|
|
90
|
+
db.session.delete(association)
|
|
91
|
+
db.session.commit()
|
|
92
|
+
flash(_("Empleado removido exitosamente."), "success")
|
|
93
|
+
return redirect(url_for("planilla.config_empleados", planilla_id=planilla_id))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@planilla_bp.route("/<planilla_id>/percepcion/add", methods=["POST"])
|
|
97
|
+
@require_write_access()
|
|
98
|
+
def add_percepcion(planilla_id: str):
|
|
99
|
+
"""Add a perception to the planilla."""
|
|
100
|
+
orden = request.form.get("orden", 0, type=int)
|
|
101
|
+
success, error_message, association_id = agregar_asociacion(
|
|
102
|
+
planilla_id=planilla_id,
|
|
103
|
+
tipo_componente="percepcion",
|
|
104
|
+
componente_id=request.form.get("percepcion_id"),
|
|
105
|
+
datos_extra={"orden": orden},
|
|
106
|
+
usuario=current_user.usuario,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if not success:
|
|
110
|
+
flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
|
|
111
|
+
return redirect(url_for(ROUTE_CONFIG_PERCEPCIONES, planilla_id=planilla_id))
|
|
112
|
+
|
|
113
|
+
flash(_("Percepción agregada exitosamente."), "success")
|
|
114
|
+
return redirect(url_for(ROUTE_CONFIG_PERCEPCIONES, planilla_id=planilla_id))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@planilla_bp.route("/<planilla_id>/percepcion/<association_id>/remove", methods=["POST"])
|
|
118
|
+
@require_write_access()
|
|
119
|
+
def remove_percepcion(planilla_id: str, association_id: str):
|
|
120
|
+
"""Remove a perception from the planilla."""
|
|
121
|
+
association = db.get_or_404(PlanillaIngreso, association_id)
|
|
122
|
+
db.session.delete(association)
|
|
123
|
+
db.session.commit()
|
|
124
|
+
flash(_("Percepción removida exitosamente."), "success")
|
|
125
|
+
return redirect(url_for("planilla.config_percepciones", planilla_id=planilla_id))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@planilla_bp.route("/<planilla_id>/deduccion/add", methods=["POST"])
|
|
129
|
+
@require_write_access()
|
|
130
|
+
def add_deduccion(planilla_id: str):
|
|
131
|
+
"""Add a deduction to the planilla with priority."""
|
|
132
|
+
prioridad = request.form.get("prioridad", 100, type=int)
|
|
133
|
+
es_obligatoria = request.form.get("es_obligatoria") == "on"
|
|
134
|
+
success, error_message, association_id = agregar_asociacion(
|
|
135
|
+
planilla_id=planilla_id,
|
|
136
|
+
tipo_componente="deduccion",
|
|
137
|
+
componente_id=request.form.get("deduccion_id"),
|
|
138
|
+
datos_extra={"prioridad": prioridad, "es_obligatoria": es_obligatoria},
|
|
139
|
+
usuario=current_user.usuario,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not success:
|
|
143
|
+
flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
|
|
144
|
+
return redirect(url_for(ROUTE_CONFIG_DEDUCCIONES, planilla_id=planilla_id))
|
|
145
|
+
|
|
146
|
+
flash(_("Deducción agregada exitosamente."), "success")
|
|
147
|
+
return redirect(url_for(ROUTE_CONFIG_DEDUCCIONES, planilla_id=planilla_id))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@planilla_bp.route("/<planilla_id>/deduccion/<association_id>/remove", methods=["POST"])
|
|
151
|
+
@require_write_access()
|
|
152
|
+
def remove_deduccion(planilla_id: str, association_id: str):
|
|
153
|
+
"""Remove a deduction from the planilla."""
|
|
154
|
+
association = db.get_or_404(PlanillaDeduccion, association_id)
|
|
155
|
+
db.session.delete(association)
|
|
156
|
+
db.session.commit()
|
|
157
|
+
flash(_("Deducción removida exitosamente."), "success")
|
|
158
|
+
return redirect(url_for("planilla.config_deducciones", planilla_id=planilla_id))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@planilla_bp.route("/<planilla_id>/deduccion/<association_id>/update-priority", methods=["POST"])
|
|
162
|
+
@require_write_access()
|
|
163
|
+
def update_deduccion_priority(planilla_id: str, association_id: str):
|
|
164
|
+
"""Update the priority of a deduction."""
|
|
165
|
+
association = db.get_or_404(PlanillaDeduccion, association_id)
|
|
166
|
+
|
|
167
|
+
prioridad = request.form.get("prioridad", type=int)
|
|
168
|
+
if prioridad is not None:
|
|
169
|
+
association.prioridad = prioridad
|
|
170
|
+
association.modificado_por = current_user.usuario
|
|
171
|
+
db.session.commit()
|
|
172
|
+
flash(_("Prioridad actualizada."), "success")
|
|
173
|
+
|
|
174
|
+
return redirect(url_for("planilla.config_deducciones", planilla_id=planilla_id))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@planilla_bp.route("/<planilla_id>/prestacion/add", methods=["POST"])
|
|
178
|
+
@require_write_access()
|
|
179
|
+
def add_prestacion(planilla_id: str):
|
|
180
|
+
"""Add a benefit (prestacion) to the planilla."""
|
|
181
|
+
orden = request.form.get("orden", 0, type=int)
|
|
182
|
+
success, error_message, association_id = agregar_asociacion(
|
|
183
|
+
planilla_id=planilla_id,
|
|
184
|
+
tipo_componente="prestacion",
|
|
185
|
+
componente_id=request.form.get("prestacion_id"),
|
|
186
|
+
datos_extra={"orden": orden},
|
|
187
|
+
usuario=current_user.usuario,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if not success:
|
|
191
|
+
flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
|
|
192
|
+
return redirect(url_for(ROUTE_CONFIG_PRESTACIONES, planilla_id=planilla_id))
|
|
193
|
+
|
|
194
|
+
flash(_("Prestación agregada exitosamente."), "success")
|
|
195
|
+
return redirect(url_for(ROUTE_CONFIG_PRESTACIONES, planilla_id=planilla_id))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@planilla_bp.route("/<planilla_id>/prestacion/<association_id>/remove", methods=["POST"])
|
|
199
|
+
@require_write_access()
|
|
200
|
+
def remove_prestacion(planilla_id: str, association_id: str):
|
|
201
|
+
"""Remove a benefit from the planilla."""
|
|
202
|
+
association = db.get_or_404(PlanillaPrestacion, association_id)
|
|
203
|
+
db.session.delete(association)
|
|
204
|
+
db.session.commit()
|
|
205
|
+
flash(_("Prestación removida exitosamente."), "success")
|
|
206
|
+
return redirect(url_for("planilla.config_prestaciones", planilla_id=planilla_id))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@planilla_bp.route("/<planilla_id>/regla/add", methods=["POST"])
|
|
210
|
+
@require_write_access()
|
|
211
|
+
def add_regla(planilla_id: str):
|
|
212
|
+
"""Add a calculation rule to the planilla."""
|
|
213
|
+
orden = request.form.get("orden", 0, type=int)
|
|
214
|
+
success, error_message, association_id = agregar_asociacion(
|
|
215
|
+
planilla_id=planilla_id,
|
|
216
|
+
tipo_componente="regla",
|
|
217
|
+
componente_id=request.form.get("regla_calculo_id"),
|
|
218
|
+
datos_extra={"orden": orden},
|
|
219
|
+
usuario=current_user.usuario,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if not success:
|
|
223
|
+
flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
|
|
224
|
+
return redirect(url_for(ROUTE_CONFIG_REGLAS, planilla_id=planilla_id))
|
|
225
|
+
|
|
226
|
+
flash(_("Regla de cálculo agregada exitosamente."), "success")
|
|
227
|
+
return redirect(url_for(ROUTE_CONFIG_REGLAS, planilla_id=planilla_id))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@planilla_bp.route("/<planilla_id>/regla/<association_id>/remove", methods=["POST"])
|
|
231
|
+
@require_write_access()
|
|
232
|
+
def remove_regla(planilla_id: str, association_id: str):
|
|
233
|
+
"""Remove a calculation rule from the planilla."""
|
|
234
|
+
association = db.get_or_404(PlanillaReglaCalculo, association_id)
|
|
235
|
+
db.session.delete(association)
|
|
236
|
+
db.session.commit()
|
|
237
|
+
flash(_("Regla de cálculo removida exitosamente."), "success")
|
|
238
|
+
return redirect(url_for("planilla.config_reglas", planilla_id=planilla_id))
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
"""Configuration routes for planilla associations."""
|
|
15
|
+
|
|
16
|
+
from flask import render_template
|
|
17
|
+
|
|
18
|
+
from coati_payroll.model import (
|
|
19
|
+
db,
|
|
20
|
+
Planilla,
|
|
21
|
+
Empleado,
|
|
22
|
+
PlanillaEmpleado,
|
|
23
|
+
PlanillaIngreso,
|
|
24
|
+
PlanillaDeduccion,
|
|
25
|
+
PlanillaPrestacion,
|
|
26
|
+
PlanillaReglaCalculo,
|
|
27
|
+
Percepcion,
|
|
28
|
+
Deduccion,
|
|
29
|
+
Prestacion,
|
|
30
|
+
ReglaCalculo,
|
|
31
|
+
)
|
|
32
|
+
from coati_payroll.rbac import require_read_access, require_write_access
|
|
33
|
+
from coati_payroll.vistas.planilla import planilla_bp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@planilla_bp.route("/<planilla_id>/config/empleados")
|
|
37
|
+
@require_read_access()
|
|
38
|
+
def config_empleados(planilla_id: str):
|
|
39
|
+
"""View employees associated with a planilla."""
|
|
40
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
41
|
+
|
|
42
|
+
empleados_asignados = (
|
|
43
|
+
db.session.execute(db.select(PlanillaEmpleado).filter_by(planilla_id=planilla_id)).scalars().all()
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Filter employees to only show those from the same company as the planilla
|
|
47
|
+
# CRITICAL: Only employees with matching empresa_id can be added to this planilla
|
|
48
|
+
query = db.select(Empleado).filter_by(activo=True)
|
|
49
|
+
if planilla.empresa_id:
|
|
50
|
+
# Only show employees from the same company (exact match required)
|
|
51
|
+
query = query.filter(Empleado.empresa_id == planilla.empresa_id)
|
|
52
|
+
else:
|
|
53
|
+
# If planilla has no company, show no employees (planilla must have empresa)
|
|
54
|
+
query = query.filter(db.false())
|
|
55
|
+
empleados_disponibles = db.session.execute(query.order_by(Empleado.primer_apellido)).scalars().all()
|
|
56
|
+
|
|
57
|
+
return render_template(
|
|
58
|
+
"modules/planilla/config_empleados.html",
|
|
59
|
+
planilla=planilla,
|
|
60
|
+
empleados_asignados=empleados_asignados,
|
|
61
|
+
empleados_disponibles=empleados_disponibles,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@planilla_bp.route("/<planilla_id>/config/percepciones")
|
|
66
|
+
@require_read_access()
|
|
67
|
+
def config_percepciones(planilla_id: str):
|
|
68
|
+
"""View perceptions associated with a planilla."""
|
|
69
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
70
|
+
|
|
71
|
+
percepciones_asignadas = (
|
|
72
|
+
db.session.execute(db.select(PlanillaIngreso).filter_by(planilla_id=planilla_id)).scalars().all()
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
percepciones_disponibles = (
|
|
76
|
+
db.session.execute(db.select(Percepcion).filter_by(activo=True).order_by(Percepcion.nombre)).scalars().all()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return render_template(
|
|
80
|
+
"modules/planilla/config_percepciones.html",
|
|
81
|
+
planilla=planilla,
|
|
82
|
+
percepciones_asignadas=percepciones_asignadas,
|
|
83
|
+
percepciones_disponibles=percepciones_disponibles,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@planilla_bp.route("/<planilla_id>/config/deducciones")
|
|
88
|
+
@require_read_access()
|
|
89
|
+
def config_deducciones(planilla_id: str):
|
|
90
|
+
"""View deductions associated with a planilla."""
|
|
91
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
92
|
+
|
|
93
|
+
deducciones_asignadas = (
|
|
94
|
+
db.session.execute(
|
|
95
|
+
db.select(PlanillaDeduccion).filter_by(planilla_id=planilla_id).order_by(PlanillaDeduccion.prioridad)
|
|
96
|
+
)
|
|
97
|
+
.scalars()
|
|
98
|
+
.all()
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
deducciones_disponibles = (
|
|
102
|
+
db.session.execute(db.select(Deduccion).filter_by(activo=True).order_by(Deduccion.nombre)).scalars().all()
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return render_template(
|
|
106
|
+
"modules/planilla/config_deducciones.html",
|
|
107
|
+
planilla=planilla,
|
|
108
|
+
deducciones_asignadas=deducciones_asignadas,
|
|
109
|
+
deducciones_disponibles=deducciones_disponibles,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@planilla_bp.route("/<planilla_id>/config/prestaciones")
|
|
114
|
+
@require_write_access()
|
|
115
|
+
def config_prestaciones(planilla_id: str):
|
|
116
|
+
"""Manage benefits associated with a planilla."""
|
|
117
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
118
|
+
|
|
119
|
+
prestaciones_asignadas = (
|
|
120
|
+
db.session.execute(db.select(PlanillaPrestacion).filter_by(planilla_id=planilla_id)).scalars().all()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
prestaciones_disponibles = (
|
|
124
|
+
db.session.execute(db.select(Prestacion).filter_by(activo=True).order_by(Prestacion.nombre)).scalars().all()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return render_template(
|
|
128
|
+
"modules/planilla/config_prestaciones.html",
|
|
129
|
+
planilla=planilla,
|
|
130
|
+
prestaciones_asignadas=prestaciones_asignadas,
|
|
131
|
+
prestaciones_disponibles=prestaciones_disponibles,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@planilla_bp.route("/<planilla_id>/config/reglas")
|
|
136
|
+
@require_read_access()
|
|
137
|
+
def config_reglas(planilla_id: str):
|
|
138
|
+
"""View calculation rules associated with a planilla."""
|
|
139
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
140
|
+
|
|
141
|
+
reglas_asignadas = (
|
|
142
|
+
db.session.execute(
|
|
143
|
+
db.select(PlanillaReglaCalculo).filter_by(planilla_id=planilla_id).order_by(PlanillaReglaCalculo.orden)
|
|
144
|
+
)
|
|
145
|
+
.scalars()
|
|
146
|
+
.all()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
reglas_disponibles = (
|
|
150
|
+
db.session.execute(db.select(ReglaCalculo).filter_by(activo=True).order_by(ReglaCalculo.nombre)).scalars().all()
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return render_template(
|
|
154
|
+
"modules/planilla/config_reglas.html",
|
|
155
|
+
planilla=planilla,
|
|
156
|
+
reglas_asignadas=reglas_asignadas,
|
|
157
|
+
reglas_disponibles=reglas_disponibles,
|
|
158
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
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 Excel export operations."""
|
|
15
|
+
|
|
16
|
+
from flask import flash, redirect, send_file, url_for
|
|
17
|
+
from flask_login import login_required
|
|
18
|
+
|
|
19
|
+
from coati_payroll.model import db, Planilla, Nomina
|
|
20
|
+
from coati_payroll.i18n import _
|
|
21
|
+
from coati_payroll.rbac import require_read_access
|
|
22
|
+
from coati_payroll.vistas.planilla import planilla_bp
|
|
23
|
+
from coati_payroll.vistas.planilla.helpers import check_openpyxl_available
|
|
24
|
+
from coati_payroll.vistas.planilla.services import ExportService
|
|
25
|
+
|
|
26
|
+
# Constants
|
|
27
|
+
ROUTE_VER_NOMINA = "planilla.ver_nomina"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-excel")
|
|
31
|
+
@login_required
|
|
32
|
+
@require_read_access()
|
|
33
|
+
def exportar_nomina_excel(planilla_id: str, nomina_id: str):
|
|
34
|
+
"""Export nomina to Excel with employee details and calculations."""
|
|
35
|
+
if not check_openpyxl_available():
|
|
36
|
+
flash(_("Excel export no disponible. Instale openpyxl."), "warning")
|
|
37
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
38
|
+
|
|
39
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
40
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
41
|
+
|
|
42
|
+
if nomina.planilla_id != planilla_id:
|
|
43
|
+
flash(_("La nómina no pertenece a esta planilla."), "error")
|
|
44
|
+
return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
output, filename = ExportService.exportar_nomina_excel(planilla, nomina)
|
|
48
|
+
return send_file(
|
|
49
|
+
output,
|
|
50
|
+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
51
|
+
as_attachment=True,
|
|
52
|
+
download_name=filename,
|
|
53
|
+
)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
flash(_("Error al exportar nómina: {}").format(str(e)), "error")
|
|
56
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-prestaciones-excel")
|
|
60
|
+
@login_required
|
|
61
|
+
@require_read_access()
|
|
62
|
+
def exportar_prestaciones_excel(planilla_id: str, nomina_id: str):
|
|
63
|
+
"""Export benefits (prestaciones) to Excel separately."""
|
|
64
|
+
if not check_openpyxl_available():
|
|
65
|
+
flash(_("Excel export no disponible. Instale openpyxl."), "warning")
|
|
66
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
67
|
+
|
|
68
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
69
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
70
|
+
|
|
71
|
+
if nomina.planilla_id != planilla_id:
|
|
72
|
+
flash(_("La nómina no pertenece a esta planilla."), "error")
|
|
73
|
+
return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
output, filename = ExportService.exportar_prestaciones_excel(planilla, nomina)
|
|
77
|
+
return send_file(
|
|
78
|
+
output,
|
|
79
|
+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
80
|
+
as_attachment=True,
|
|
81
|
+
download_name=filename,
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
flash(_("Error al exportar prestaciones: {}").format(str(e)), "error")
|
|
85
|
+
return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-comprobante-excel")
|
|
89
|
+
@login_required
|
|
90
|
+
@require_read_access()
|
|
91
|
+
def exportar_comprobante_excel(planilla_id: str, nomina_id: str):
|
|
92
|
+
"""Export summarized accounting voucher (comprobante contable) to Excel."""
|
|
93
|
+
if not check_openpyxl_available():
|
|
94
|
+
flash(_("Excel export no disponible. Instale openpyxl."), "warning")
|
|
95
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
96
|
+
|
|
97
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
98
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
99
|
+
|
|
100
|
+
if nomina.planilla_id != planilla_id:
|
|
101
|
+
flash(_("La nómina no pertenece a esta planilla."), "error")
|
|
102
|
+
return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
|
|
103
|
+
|
|
104
|
+
# Check if comprobante exists
|
|
105
|
+
from coati_payroll.model import ComprobanteContable
|
|
106
|
+
|
|
107
|
+
comprobante = db.session.execute(db.select(ComprobanteContable).filter_by(nomina_id=nomina_id)).scalar_one_or_none()
|
|
108
|
+
|
|
109
|
+
if not comprobante:
|
|
110
|
+
flash(_("No existe comprobante contable para esta nómina. Debe recalcular la nómina."), "error")
|
|
111
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
112
|
+
|
|
113
|
+
# Check for configuration warnings
|
|
114
|
+
if comprobante.advertencias:
|
|
115
|
+
flash(
|
|
116
|
+
_("ADVERTENCIA: La configuración contable está incompleta. Revise las advertencias en el log."),
|
|
117
|
+
"warning",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
output, filename = ExportService.exportar_comprobante_excel(planilla, nomina)
|
|
122
|
+
return send_file(
|
|
123
|
+
output,
|
|
124
|
+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
125
|
+
as_attachment=True,
|
|
126
|
+
download_name=filename,
|
|
127
|
+
)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
flash(_("Error al exportar comprobante: {}").format(str(e)), "error")
|
|
130
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-comprobante-detallado-excel")
|
|
134
|
+
@login_required
|
|
135
|
+
@require_read_access()
|
|
136
|
+
def exportar_comprobante_detallado_excel(planilla_id: str, nomina_id: str):
|
|
137
|
+
"""Export detailed accounting voucher per employee to Excel."""
|
|
138
|
+
if not check_openpyxl_available():
|
|
139
|
+
flash(_("Excel export no disponible. Instale openpyxl."), "warning")
|
|
140
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
141
|
+
|
|
142
|
+
planilla = db.get_or_404(Planilla, planilla_id)
|
|
143
|
+
nomina = db.get_or_404(Nomina, nomina_id)
|
|
144
|
+
|
|
145
|
+
if nomina.planilla_id != planilla_id:
|
|
146
|
+
flash(_("La nómina no pertenece a esta planilla."), "error")
|
|
147
|
+
return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
|
|
148
|
+
|
|
149
|
+
# Check if comprobante exists
|
|
150
|
+
from coati_payroll.model import ComprobanteContable
|
|
151
|
+
|
|
152
|
+
comprobante = db.session.execute(db.select(ComprobanteContable).filter_by(nomina_id=nomina_id)).scalar_one_or_none()
|
|
153
|
+
|
|
154
|
+
if not comprobante:
|
|
155
|
+
flash(_("No existe comprobante contable para esta nómina. Debe recalcular la nómina."), "error")
|
|
156
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
157
|
+
|
|
158
|
+
# Check for configuration warnings
|
|
159
|
+
if comprobante.advertencias:
|
|
160
|
+
flash(
|
|
161
|
+
_("ADVERTENCIA: La configuración contable está incompleta. Revise las advertencias en el log."),
|
|
162
|
+
"warning",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
output, filename = ExportService.exportar_comprobante_detallado_excel(planilla, nomina)
|
|
167
|
+
return send_file(
|
|
168
|
+
output,
|
|
169
|
+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
170
|
+
as_attachment=True,
|
|
171
|
+
download_name=filename,
|
|
172
|
+
)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
flash(_("Error al exportar comprobante detallado: {}").format(str(e)), "error")
|
|
175
|
+
return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"""Helper functions for planilla views."""
|
|
15
|
+
|
|
16
|
+
from coati_payroll.vistas.planilla.helpers.form_helpers import (
|
|
17
|
+
populate_form_choices,
|
|
18
|
+
populate_novedad_form_choices,
|
|
19
|
+
get_concepto_ids_from_form,
|
|
20
|
+
)
|
|
21
|
+
from coati_payroll.vistas.planilla.helpers.excel_helpers import check_openpyxl_available
|
|
22
|
+
from coati_payroll.vistas.planilla.helpers.association_helpers import (
|
|
23
|
+
agregar_asociacion,
|
|
24
|
+
get_planilla_component_counts,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"populate_form_choices",
|
|
29
|
+
"populate_novedad_form_choices",
|
|
30
|
+
"get_concepto_ids_from_form",
|
|
31
|
+
"check_openpyxl_available",
|
|
32
|
+
"agregar_asociacion",
|
|
33
|
+
"get_planilla_component_counts",
|
|
34
|
+
]
|