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,341 @@
|
|
|
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
|
+
"""Exchange Rate CRUD routes."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import date, datetime
|
|
19
|
+
from decimal import Decimal
|
|
20
|
+
|
|
21
|
+
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|
22
|
+
from flask_login import current_user
|
|
23
|
+
from openpyxl import load_workbook
|
|
24
|
+
|
|
25
|
+
from coati_payroll.forms import ExchangeRateForm
|
|
26
|
+
from coati_payroll.i18n import _
|
|
27
|
+
from coati_payroll.rbac import require_read_access, require_write_access
|
|
28
|
+
from coati_payroll.model import Moneda, TipoCambio, db
|
|
29
|
+
from coati_payroll.vistas.constants import PER_PAGE
|
|
30
|
+
|
|
31
|
+
exchange_rate_bp = Blueprint("exchange_rate", __name__, url_prefix="/exchange_rate")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_currency_choices():
|
|
35
|
+
"""Get list of currencies for select fields."""
|
|
36
|
+
currencies = db.session.execute(db.select(Moneda).filter_by(activo=True).order_by(Moneda.codigo)).scalars().all()
|
|
37
|
+
return [(c.id, f"{c.codigo} - {c.nombre}") for c in currencies]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@exchange_rate_bp.route("/")
|
|
41
|
+
@require_read_access()
|
|
42
|
+
def index():
|
|
43
|
+
"""List all exchange rates with pagination and filters."""
|
|
44
|
+
page = request.args.get("page", 1, type=int)
|
|
45
|
+
|
|
46
|
+
# Get filter parameters
|
|
47
|
+
fecha_desde = request.args.get("fecha_desde", type=str)
|
|
48
|
+
fecha_hasta = request.args.get("fecha_hasta", type=str)
|
|
49
|
+
moneda_origen_id = request.args.get("moneda_origen_id", type=str) if request.args.get("moneda_origen_id") else None
|
|
50
|
+
moneda_destino_id = (
|
|
51
|
+
request.args.get("moneda_destino_id", type=str) if request.args.get("moneda_destino_id") else None
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Build query with filters
|
|
55
|
+
query = db.select(TipoCambio)
|
|
56
|
+
|
|
57
|
+
if fecha_desde:
|
|
58
|
+
query = query.filter(TipoCambio.fecha >= fecha_desde)
|
|
59
|
+
if fecha_hasta:
|
|
60
|
+
query = query.filter(TipoCambio.fecha <= fecha_hasta)
|
|
61
|
+
if moneda_origen_id:
|
|
62
|
+
query = query.filter(TipoCambio.moneda_origen_id == moneda_origen_id)
|
|
63
|
+
if moneda_destino_id:
|
|
64
|
+
query = query.filter(TipoCambio.moneda_destino_id == moneda_destino_id)
|
|
65
|
+
|
|
66
|
+
query = query.order_by(TipoCambio.fecha.desc())
|
|
67
|
+
|
|
68
|
+
pagination = db.paginate(
|
|
69
|
+
query,
|
|
70
|
+
page=page,
|
|
71
|
+
per_page=PER_PAGE,
|
|
72
|
+
error_out=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Get currencies for filter dropdowns
|
|
76
|
+
currencies = get_currency_choices()
|
|
77
|
+
|
|
78
|
+
return render_template(
|
|
79
|
+
"modules/exchange_rate/index.html",
|
|
80
|
+
exchange_rates=pagination.items,
|
|
81
|
+
pagination=pagination,
|
|
82
|
+
currencies=currencies,
|
|
83
|
+
fecha_desde=fecha_desde,
|
|
84
|
+
fecha_hasta=fecha_hasta,
|
|
85
|
+
moneda_origen_id=moneda_origen_id,
|
|
86
|
+
moneda_destino_id=moneda_destino_id,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@exchange_rate_bp.route("/new", methods=["GET", "POST"])
|
|
91
|
+
@require_write_access()
|
|
92
|
+
def new():
|
|
93
|
+
"""Create a new exchange rate."""
|
|
94
|
+
form = ExchangeRateForm()
|
|
95
|
+
form.moneda_origen_id.choices = [("", _("Seleccionar..."))] + get_currency_choices()
|
|
96
|
+
form.moneda_destino_id.choices = [("", _("Seleccionar..."))] + get_currency_choices()
|
|
97
|
+
|
|
98
|
+
if form.validate_on_submit():
|
|
99
|
+
exchange_rate = TipoCambio()
|
|
100
|
+
exchange_rate.fecha = form.fecha.data
|
|
101
|
+
exchange_rate.moneda_origen_id = form.moneda_origen_id.data
|
|
102
|
+
exchange_rate.moneda_destino_id = form.moneda_destino_id.data
|
|
103
|
+
exchange_rate.tasa = form.tasa.data
|
|
104
|
+
exchange_rate.creado_por = current_user.usuario
|
|
105
|
+
|
|
106
|
+
db.session.add(exchange_rate)
|
|
107
|
+
db.session.commit()
|
|
108
|
+
flash(_("Tipo de cambio creado exitosamente."), "success")
|
|
109
|
+
return redirect(url_for("exchange_rate.index"))
|
|
110
|
+
|
|
111
|
+
# Default date to today
|
|
112
|
+
if not form.fecha.data:
|
|
113
|
+
form.fecha.data = date.today()
|
|
114
|
+
|
|
115
|
+
return render_template("modules/exchange_rate/form.html", form=form, title=_("Nuevo Tipo de Cambio"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@exchange_rate_bp.route("/edit/<string:id>", methods=["GET", "POST"])
|
|
119
|
+
@require_write_access()
|
|
120
|
+
def edit(id: str):
|
|
121
|
+
"""Edit an existing exchange rate."""
|
|
122
|
+
exchange_rate = db.session.get(TipoCambio, id)
|
|
123
|
+
if not exchange_rate:
|
|
124
|
+
flash(_("Tipo de cambio no encontrado."), "error")
|
|
125
|
+
return redirect(url_for("exchange_rate.index"))
|
|
126
|
+
|
|
127
|
+
form = ExchangeRateForm(obj=exchange_rate)
|
|
128
|
+
form.moneda_origen_id.choices = [("", _("Seleccionar..."))] + get_currency_choices()
|
|
129
|
+
form.moneda_destino_id.choices = [("", _("Seleccionar..."))] + get_currency_choices()
|
|
130
|
+
|
|
131
|
+
if form.validate_on_submit():
|
|
132
|
+
exchange_rate.fecha = form.fecha.data
|
|
133
|
+
exchange_rate.moneda_origen_id = form.moneda_origen_id.data
|
|
134
|
+
exchange_rate.moneda_destino_id = form.moneda_destino_id.data
|
|
135
|
+
exchange_rate.tasa = form.tasa.data
|
|
136
|
+
exchange_rate.modificado_por = current_user.usuario
|
|
137
|
+
|
|
138
|
+
db.session.commit()
|
|
139
|
+
flash(_("Tipo de cambio actualizado exitosamente."), "success")
|
|
140
|
+
return redirect(url_for("exchange_rate.index"))
|
|
141
|
+
|
|
142
|
+
return render_template(
|
|
143
|
+
"modules/exchange_rate/form.html",
|
|
144
|
+
form=form,
|
|
145
|
+
title=_("Editar Tipo de Cambio"),
|
|
146
|
+
exchange_rate=exchange_rate,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@exchange_rate_bp.route("/delete/<string:id>", methods=["POST"])
|
|
151
|
+
@require_write_access()
|
|
152
|
+
def delete(id: str):
|
|
153
|
+
"""Delete an exchange rate."""
|
|
154
|
+
exchange_rate = db.session.get(TipoCambio, id)
|
|
155
|
+
if not exchange_rate:
|
|
156
|
+
flash(_("Tipo de cambio no encontrado."), "error")
|
|
157
|
+
return redirect(url_for("exchange_rate.index"))
|
|
158
|
+
|
|
159
|
+
db.session.delete(exchange_rate)
|
|
160
|
+
db.session.commit()
|
|
161
|
+
flash(_("Tipo de cambio eliminado exitosamente."), "success")
|
|
162
|
+
return redirect(url_for("exchange_rate.index"))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Constants for Excel import
|
|
166
|
+
EXPECTED_COLUMNS = 4
|
|
167
|
+
MAX_ERRORS_DISPLAYED = 10
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@exchange_rate_bp.route("/import", methods=["GET", "POST"])
|
|
171
|
+
@require_write_access()
|
|
172
|
+
def import_excel():
|
|
173
|
+
"""Import exchange rates from Excel file."""
|
|
174
|
+
if request.method == "GET":
|
|
175
|
+
return render_template("modules/exchange_rate/import.html")
|
|
176
|
+
|
|
177
|
+
# Check if file is in request
|
|
178
|
+
if "file" not in request.files:
|
|
179
|
+
flash(_("No se seleccionó ningún archivo."), "error")
|
|
180
|
+
return redirect(url_for("exchange_rate.import_excel"))
|
|
181
|
+
|
|
182
|
+
file = request.files["file"]
|
|
183
|
+
|
|
184
|
+
# Check if file has a name
|
|
185
|
+
if file.filename == "":
|
|
186
|
+
flash(_("No se seleccionó ningún archivo."), "error")
|
|
187
|
+
return redirect(url_for("exchange_rate.import_excel"))
|
|
188
|
+
|
|
189
|
+
# Check if file is Excel
|
|
190
|
+
if not file.filename.lower().endswith((".xlsx", ".xls")):
|
|
191
|
+
flash(_("El archivo debe ser un archivo Excel (.xlsx o .xls)."), "error")
|
|
192
|
+
return redirect(url_for("exchange_rate.import_excel"))
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Load the workbook
|
|
196
|
+
workbook = load_workbook(file, data_only=True)
|
|
197
|
+
sheet = workbook.active
|
|
198
|
+
|
|
199
|
+
# Track statistics
|
|
200
|
+
imported_count = 0
|
|
201
|
+
updated_count = 0
|
|
202
|
+
error_count = 0
|
|
203
|
+
errors = []
|
|
204
|
+
|
|
205
|
+
# Get all active currencies for lookup
|
|
206
|
+
currencies = db.session.execute(db.select(Moneda).filter_by(activo=True)).scalars().all()
|
|
207
|
+
currency_map = {c.codigo.upper(): c for c in currencies}
|
|
208
|
+
|
|
209
|
+
# Process rows (skip header)
|
|
210
|
+
for row_idx, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
|
|
211
|
+
if not row or not any(row): # Skip empty rows
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
# Expected columns: Fecha | Moneda Base | Moneda Destino | Tipo de Cambio
|
|
216
|
+
if len(row) < EXPECTED_COLUMNS:
|
|
217
|
+
errors.append(
|
|
218
|
+
_("Fila {}: formato incorrecto, se esperan {} columnas.").format(row_idx, EXPECTED_COLUMNS)
|
|
219
|
+
)
|
|
220
|
+
error_count += 1
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
fecha_val, moneda_origen_codigo, moneda_destino_codigo, tasa_val = row[0], row[1], row[2], row[3]
|
|
224
|
+
|
|
225
|
+
# Validate fecha
|
|
226
|
+
if isinstance(fecha_val, datetime):
|
|
227
|
+
fecha = fecha_val.date()
|
|
228
|
+
elif isinstance(fecha_val, date):
|
|
229
|
+
fecha = fecha_val
|
|
230
|
+
elif isinstance(fecha_val, str):
|
|
231
|
+
try:
|
|
232
|
+
fecha = datetime.strptime(fecha_val, "%Y-%m-%d").date()
|
|
233
|
+
except ValueError:
|
|
234
|
+
try:
|
|
235
|
+
fecha = datetime.strptime(fecha_val, "%d/%m/%Y").date()
|
|
236
|
+
except ValueError:
|
|
237
|
+
errors.append(_("Fila {}: fecha inválida '{}'.").format(row_idx, fecha_val))
|
|
238
|
+
error_count += 1
|
|
239
|
+
continue
|
|
240
|
+
else:
|
|
241
|
+
errors.append(_("Fila {}: fecha inválida.").format(row_idx))
|
|
242
|
+
error_count += 1
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Validate moneda_origen
|
|
246
|
+
if not moneda_origen_codigo:
|
|
247
|
+
errors.append(_("Fila {}: moneda origen vacía.").format(row_idx))
|
|
248
|
+
error_count += 1
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
moneda_origen_key = str(moneda_origen_codigo).strip().upper()
|
|
252
|
+
moneda_origen = currency_map.get(moneda_origen_key)
|
|
253
|
+
if not moneda_origen:
|
|
254
|
+
errors.append(_("Fila {}: moneda origen '{}' no encontrada.").format(row_idx, moneda_origen_codigo))
|
|
255
|
+
error_count += 1
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
# Validate moneda_destino
|
|
259
|
+
if not moneda_destino_codigo:
|
|
260
|
+
errors.append(_("Fila {}: moneda destino vacía.").format(row_idx))
|
|
261
|
+
error_count += 1
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
moneda_destino_key = str(moneda_destino_codigo).strip().upper()
|
|
265
|
+
moneda_destino = currency_map.get(moneda_destino_key)
|
|
266
|
+
if not moneda_destino:
|
|
267
|
+
errors.append(
|
|
268
|
+
_("Fila {}: moneda destino '{}' no encontrada.").format(row_idx, moneda_destino_codigo)
|
|
269
|
+
)
|
|
270
|
+
error_count += 1
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Validate tasa
|
|
274
|
+
try:
|
|
275
|
+
if isinstance(tasa_val, (int, float)):
|
|
276
|
+
tasa = Decimal(str(tasa_val))
|
|
277
|
+
elif isinstance(tasa_val, str):
|
|
278
|
+
tasa = Decimal(tasa_val.strip())
|
|
279
|
+
else:
|
|
280
|
+
tasa = Decimal(str(tasa_val))
|
|
281
|
+
|
|
282
|
+
if tasa <= 0:
|
|
283
|
+
errors.append(_("Fila {}: tasa debe ser mayor que cero.").format(row_idx))
|
|
284
|
+
error_count += 1
|
|
285
|
+
continue
|
|
286
|
+
except (ValueError, TypeError):
|
|
287
|
+
errors.append(_("Fila {}: tasa inválida '{}'.").format(row_idx, tasa_val))
|
|
288
|
+
error_count += 1
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Check if exchange rate already exists
|
|
292
|
+
existing = db.session.execute(
|
|
293
|
+
db.select(TipoCambio).filter_by(
|
|
294
|
+
fecha=fecha, moneda_origen_id=moneda_origen.id, moneda_destino_id=moneda_destino.id
|
|
295
|
+
)
|
|
296
|
+
).scalar_one_or_none()
|
|
297
|
+
|
|
298
|
+
if existing:
|
|
299
|
+
# Update existing
|
|
300
|
+
existing.tasa = tasa
|
|
301
|
+
existing.modificado_por = current_user.usuario
|
|
302
|
+
updated_count += 1
|
|
303
|
+
else:
|
|
304
|
+
# Create new
|
|
305
|
+
exchange_rate = TipoCambio()
|
|
306
|
+
exchange_rate.fecha = fecha
|
|
307
|
+
exchange_rate.moneda_origen_id = moneda_origen.id
|
|
308
|
+
exchange_rate.moneda_destino_id = moneda_destino.id
|
|
309
|
+
exchange_rate.tasa = tasa
|
|
310
|
+
exchange_rate.creado_por = current_user.usuario
|
|
311
|
+
db.session.add(exchange_rate)
|
|
312
|
+
imported_count += 1
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
errors.append(_("Fila {}: error inesperado - {}.").format(row_idx, str(e)))
|
|
316
|
+
error_count += 1
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Commit all changes
|
|
320
|
+
db.session.commit()
|
|
321
|
+
|
|
322
|
+
# Show results
|
|
323
|
+
if imported_count > 0 or updated_count > 0:
|
|
324
|
+
flash(
|
|
325
|
+
_("Importación completada: {} creados, {} actualizados.").format(imported_count, updated_count),
|
|
326
|
+
"success",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if error_count > 0:
|
|
330
|
+
flash(_("{} errores encontrados durante la importación.").format(error_count), "warning")
|
|
331
|
+
for error in errors[:MAX_ERRORS_DISPLAYED]:
|
|
332
|
+
flash(error, "error")
|
|
333
|
+
if len(errors) > MAX_ERRORS_DISPLAYED:
|
|
334
|
+
flash(_("... y {} errores más.").format(len(errors) - MAX_ERRORS_DISPLAYED), "error")
|
|
335
|
+
|
|
336
|
+
return redirect(url_for("exchange_rate.index"))
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
db.session.rollback()
|
|
340
|
+
flash(_("Error al procesar el archivo: {}.").format(str(e)), "error")
|
|
341
|
+
return redirect(url_for("exchange_rate.import_excel"))
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
"""Liquidaciones module views."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from datetime import date
|
|
20
|
+
|
|
21
|
+
from flask import Blueprint, flash, redirect, render_template, request, send_file, url_for
|
|
22
|
+
from flask_login import login_required, current_user
|
|
23
|
+
|
|
24
|
+
from coati_payroll.i18n import _
|
|
25
|
+
from coati_payroll.model import Liquidacion, LiquidacionConcepto, Empleado, db
|
|
26
|
+
from coati_payroll.model import PlanillaEmpleado
|
|
27
|
+
from coati_payroll.rbac import require_read_access, require_write_access
|
|
28
|
+
from coati_payroll.liquidacion_engine import ejecutar_liquidacion, recalcular_liquidacion
|
|
29
|
+
from coati_payroll.vistas.planilla.helpers import check_openpyxl_available
|
|
30
|
+
from coati_payroll.vistas.planilla.services import ExportService
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
liquidacion_bp = Blueprint("liquidacion", __name__, url_prefix="/liquidaciones")
|
|
34
|
+
|
|
35
|
+
# Constants
|
|
36
|
+
ROUTE_LIQUIDACION_VER = "liquidacion.ver"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@liquidacion_bp.route("/")
|
|
40
|
+
@login_required
|
|
41
|
+
@require_read_access()
|
|
42
|
+
def index():
|
|
43
|
+
"""List liquidaciones."""
|
|
44
|
+
liquidaciones = db.session.execute(db.select(Liquidacion).order_by(Liquidacion.creado.desc())).scalars().all()
|
|
45
|
+
return render_template("modules/liquidacion/index.html", liquidaciones=liquidaciones)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@liquidacion_bp.route("/nueva", methods=["GET", "POST"])
|
|
49
|
+
@login_required
|
|
50
|
+
@require_write_access()
|
|
51
|
+
def nueva():
|
|
52
|
+
"""Create and calculate a new liquidacion."""
|
|
53
|
+
empleados = (
|
|
54
|
+
db.session.execute(db.select(Empleado).filter_by(activo=True).order_by(Empleado.primer_apellido))
|
|
55
|
+
.scalars()
|
|
56
|
+
.all()
|
|
57
|
+
)
|
|
58
|
+
conceptos = (
|
|
59
|
+
db.session.execute(db.select(LiquidacionConcepto).filter_by(activo=True).order_by(LiquidacionConcepto.nombre))
|
|
60
|
+
.scalars()
|
|
61
|
+
.all()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if request.method == "POST":
|
|
65
|
+
empleado_id = request.form.get("empleado_id")
|
|
66
|
+
concepto_id = request.form.get("concepto_id") or None
|
|
67
|
+
fecha_calculo_str = request.form.get("fecha_calculo")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
fecha_calculo = date.fromisoformat(fecha_calculo_str) if fecha_calculo_str else date.today()
|
|
71
|
+
except ValueError:
|
|
72
|
+
flash(_("Formato de fecha inválido."), "error")
|
|
73
|
+
return redirect(url_for("liquidacion.nueva"))
|
|
74
|
+
|
|
75
|
+
liquidacion, errors, warnings = ejecutar_liquidacion(
|
|
76
|
+
empleado_id=empleado_id,
|
|
77
|
+
concepto_id=concepto_id,
|
|
78
|
+
fecha_calculo=fecha_calculo,
|
|
79
|
+
usuario=getattr(current_user, "usuario", None),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
for e in errors:
|
|
83
|
+
flash(e, "error")
|
|
84
|
+
for w in warnings:
|
|
85
|
+
flash(w, "warning")
|
|
86
|
+
|
|
87
|
+
if liquidacion:
|
|
88
|
+
flash(_("Liquidación calculada."), "success")
|
|
89
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
90
|
+
|
|
91
|
+
return redirect(url_for("liquidacion.nueva"))
|
|
92
|
+
|
|
93
|
+
return render_template(
|
|
94
|
+
"modules/liquidacion/nueva.html", empleados=empleados, conceptos=conceptos, fecha_calculo=date.today()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@liquidacion_bp.route("/<liquidacion_id>")
|
|
99
|
+
@login_required
|
|
100
|
+
@require_read_access()
|
|
101
|
+
def ver(liquidacion_id: str):
|
|
102
|
+
"""View liquidacion detail."""
|
|
103
|
+
liquidacion = db.get_or_404(Liquidacion, liquidacion_id)
|
|
104
|
+
return render_template("modules/liquidacion/ver.html", liquidacion=liquidacion)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@liquidacion_bp.route("/<liquidacion_id>/recalcular", methods=["POST"])
|
|
108
|
+
@login_required
|
|
109
|
+
@require_write_access()
|
|
110
|
+
def recalcular(liquidacion_id: str):
|
|
111
|
+
liquidacion = db.get_or_404(Liquidacion, liquidacion_id)
|
|
112
|
+
nueva, errors, warnings = recalcular_liquidacion(
|
|
113
|
+
liquidacion_id=liquidacion.id,
|
|
114
|
+
fecha_calculo=liquidacion.fecha_calculo,
|
|
115
|
+
usuario=getattr(current_user, "usuario", None),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for e in errors:
|
|
119
|
+
flash(e, "error")
|
|
120
|
+
for w in warnings:
|
|
121
|
+
flash(w, "warning")
|
|
122
|
+
|
|
123
|
+
if nueva:
|
|
124
|
+
flash(_("Liquidación recalculada."), "success")
|
|
125
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@liquidacion_bp.route("/<liquidacion_id>/aplicar", methods=["POST"])
|
|
129
|
+
@login_required
|
|
130
|
+
@require_write_access()
|
|
131
|
+
def aplicar(liquidacion_id: str):
|
|
132
|
+
liquidacion = db.get_or_404(Liquidacion, liquidacion_id)
|
|
133
|
+
|
|
134
|
+
if liquidacion.estado != "borrador":
|
|
135
|
+
flash(_("Solo se pueden aplicar liquidaciones en borrador."), "error")
|
|
136
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
137
|
+
|
|
138
|
+
empleado = db.session.get(Empleado, liquidacion.empleado_id)
|
|
139
|
+
if not empleado:
|
|
140
|
+
flash(_("Empleado no encontrado."), "error")
|
|
141
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
142
|
+
|
|
143
|
+
# Mark employee inactive
|
|
144
|
+
empleado.activo = False
|
|
145
|
+
empleado.fecha_baja = liquidacion.fecha_calculo
|
|
146
|
+
|
|
147
|
+
# Deactivate all active planilla associations
|
|
148
|
+
asociaciones = (
|
|
149
|
+
db.session.execute(
|
|
150
|
+
db.select(PlanillaEmpleado).where(
|
|
151
|
+
PlanillaEmpleado.empleado_id == empleado.id,
|
|
152
|
+
PlanillaEmpleado.activo.is_(True),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
.scalars()
|
|
156
|
+
.all()
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for pe in asociaciones:
|
|
160
|
+
pe.activo = False
|
|
161
|
+
pe.fecha_fin = liquidacion.fecha_calculo
|
|
162
|
+
|
|
163
|
+
liquidacion.estado = "aplicada"
|
|
164
|
+
db.session.commit()
|
|
165
|
+
flash(_("Liquidación aplicada. Empleado marcado como inactivo y desvinculado de planillas."), "success")
|
|
166
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@liquidacion_bp.route("/<liquidacion_id>/pagar", methods=["POST"])
|
|
170
|
+
@login_required
|
|
171
|
+
@require_write_access()
|
|
172
|
+
def pagar(liquidacion_id: str):
|
|
173
|
+
liquidacion = db.get_or_404(Liquidacion, liquidacion_id)
|
|
174
|
+
|
|
175
|
+
if liquidacion.estado != "aplicada":
|
|
176
|
+
flash(_("Solo se pueden pagar liquidaciones aplicadas."), "error")
|
|
177
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
178
|
+
|
|
179
|
+
liquidacion.estado = "pagada"
|
|
180
|
+
db.session.commit()
|
|
181
|
+
flash(_("Liquidación marcada como pagada."), "success")
|
|
182
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion.id))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@liquidacion_bp.route("/<liquidacion_id>/exportar-excel")
|
|
186
|
+
@login_required
|
|
187
|
+
@require_read_access()
|
|
188
|
+
def exportar_excel(liquidacion_id: str):
|
|
189
|
+
if not check_openpyxl_available():
|
|
190
|
+
flash(_("Excel export no disponible. Instale openpyxl."), "warning")
|
|
191
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion_id))
|
|
192
|
+
|
|
193
|
+
liquidacion = db.get_or_404(Liquidacion, liquidacion_id)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
output, filename = ExportService.exportar_liquidacion_excel(liquidacion)
|
|
197
|
+
return send_file(
|
|
198
|
+
output,
|
|
199
|
+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
200
|
+
as_attachment=True,
|
|
201
|
+
download_name=filename,
|
|
202
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
flash(_("Error al exportar liquidación: {}").format(str(e)), "error")
|
|
205
|
+
return redirect(url_for(ROUTE_LIQUIDACION_VER, liquidacion_id=liquidacion_id))
|