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.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,423 @@
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
+ """Initial benefit balance loading views."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from datetime import datetime
20
+
21
+ from flask import Blueprint, flash, redirect, render_template, request, url_for, Response
22
+ from flask_login import current_user
23
+ from sqlalchemy import and_
24
+
25
+ from coati_payroll.forms import CargaInicialPrestacionForm
26
+ from coati_payroll.i18n import _
27
+ from coati_payroll.model import (
28
+ CargaInicialPrestacion,
29
+ Empleado,
30
+ Moneda,
31
+ Prestacion,
32
+ PrestacionAcumulada,
33
+ db,
34
+ )
35
+ from coati_payroll.rbac import require_read_access, require_write_access
36
+ from coati_payroll.vistas.constants import PER_PAGE
37
+
38
+ carga_inicial_prestacion_bp = Blueprint("carga_inicial_prestacion", __name__, url_prefix="/carga-inicial-prestaciones")
39
+
40
+
41
+ @carga_inicial_prestacion_bp.route("/")
42
+ @require_read_access()
43
+ def index():
44
+ """List all initial benefit balance loads."""
45
+ page = request.args.get("page", 1, type=int)
46
+ estado_filter = request.args.get("estado", "")
47
+
48
+ query = CargaInicialPrestacion.query
49
+
50
+ # Apply filters
51
+ if estado_filter:
52
+ query = query.filter(CargaInicialPrestacion.estado == estado_filter)
53
+
54
+ # Order by creation date descending
55
+ query = query.order_by(CargaInicialPrestacion.creado.desc())
56
+
57
+ # Paginate
58
+ pagination = query.paginate(page=page, per_page=PER_PAGE, error_out=False)
59
+ cargas = pagination.items
60
+
61
+ return render_template(
62
+ "modules/carga_inicial_prestacion/index.html",
63
+ cargas=cargas,
64
+ pagination=pagination,
65
+ estado_filter=estado_filter,
66
+ )
67
+
68
+
69
+ @carga_inicial_prestacion_bp.route("/nueva", methods=["GET", "POST"])
70
+ @require_write_access()
71
+ def nueva():
72
+ """Create a new initial benefit balance load."""
73
+ form = CargaInicialPrestacionForm()
74
+
75
+ # Populate select field choices
76
+ form.empleado_id.choices = [("", _("-- Seleccionar --"))] + [
77
+ (emp.id, f"{emp.codigo_empleado} - {emp.primer_nombre} {emp.primer_apellido}")
78
+ for emp in Empleado.query.filter_by(activo=True).order_by(Empleado.codigo_empleado).all()
79
+ ]
80
+
81
+ form.prestacion_id.choices = [("", _("-- Seleccionar --"))] + [
82
+ (prest.id, f"{prest.codigo} - {prest.nombre}")
83
+ for prest in Prestacion.query.filter_by(activo=True).order_by(Prestacion.codigo).all()
84
+ ]
85
+
86
+ form.moneda_id.choices = [("", _("-- Seleccionar --"))] + [
87
+ (mon.id, f"{mon.codigo} - {mon.nombre}")
88
+ for mon in Moneda.query.filter_by(activo=True).order_by(Moneda.codigo).all()
89
+ ]
90
+
91
+ if form.validate_on_submit():
92
+ # Check for duplicate
93
+ existing = CargaInicialPrestacion.query.filter(
94
+ and_(
95
+ CargaInicialPrestacion.empleado_id == form.empleado_id.data,
96
+ CargaInicialPrestacion.prestacion_id == form.prestacion_id.data,
97
+ CargaInicialPrestacion.anio_corte == form.anio_corte.data,
98
+ CargaInicialPrestacion.mes_corte == form.mes_corte.data,
99
+ )
100
+ ).first()
101
+
102
+ if existing:
103
+ flash(
104
+ _("Ya existe una carga inicial para este empleado, prestación y periodo."),
105
+ "warning",
106
+ )
107
+ return render_template("modules/carga_inicial_prestacion/form.html", form=form)
108
+
109
+ carga = CargaInicialPrestacion(
110
+ empleado_id=form.empleado_id.data,
111
+ prestacion_id=form.prestacion_id.data,
112
+ anio_corte=form.anio_corte.data,
113
+ mes_corte=form.mes_corte.data,
114
+ moneda_id=form.moneda_id.data,
115
+ saldo_acumulado=form.saldo_acumulado.data if form.saldo_acumulado.data is not None else Decimal("0.00"),
116
+ tipo_cambio=form.tipo_cambio.data if form.tipo_cambio.data is not None else Decimal("1.0"),
117
+ saldo_convertido=form.saldo_convertido.data if form.saldo_convertido.data is not None else Decimal("0.00"),
118
+ observaciones=form.observaciones.data,
119
+ estado="borrador",
120
+ creado_por=current_user.usuario if current_user.is_authenticated else None,
121
+ )
122
+
123
+ db.session.add(carga)
124
+ db.session.commit()
125
+
126
+ flash(_("Carga inicial creada exitosamente en estado borrador."), "success")
127
+ return redirect(url_for("carga_inicial_prestacion.index"))
128
+
129
+ return render_template("modules/carga_inicial_prestacion/form.html", form=form)
130
+
131
+
132
+ @carga_inicial_prestacion_bp.route("/<carga_id>/editar", methods=["GET", "POST"])
133
+ @require_write_access()
134
+ def editar(carga_id):
135
+ """Edit an initial benefit balance load (only if in draft status)."""
136
+ carga = CargaInicialPrestacion.query.get_or_404(carga_id)
137
+
138
+ if carga.estado == "aplicado":
139
+ flash(_("No se puede editar una carga inicial ya aplicada."), "warning")
140
+ return redirect(url_for("carga_inicial_prestacion.index"))
141
+
142
+ form = CargaInicialPrestacionForm(obj=carga)
143
+
144
+ # Populate select field choices
145
+ form.empleado_id.choices = [("", _("-- Seleccionar --"))] + [
146
+ (emp.id, f"{emp.codigo_empleado} - {emp.primer_nombre} {emp.primer_apellido}")
147
+ for emp in Empleado.query.filter_by(activo=True).order_by(Empleado.codigo_empleado).all()
148
+ ]
149
+
150
+ form.prestacion_id.choices = [("", _("-- Seleccionar --"))] + [
151
+ (prest.id, f"{prest.codigo} - {prest.nombre}")
152
+ for prest in Prestacion.query.filter_by(activo=True).order_by(Prestacion.codigo).all()
153
+ ]
154
+
155
+ form.moneda_id.choices = [("", _("-- Seleccionar --"))] + [
156
+ (mon.id, f"{mon.codigo} - {mon.nombre}")
157
+ for mon in Moneda.query.filter_by(activo=True).order_by(Moneda.codigo).all()
158
+ ]
159
+
160
+ if form.validate_on_submit():
161
+ carga.empleado_id = form.empleado_id.data
162
+ carga.prestacion_id = form.prestacion_id.data
163
+ carga.anio_corte = form.anio_corte.data
164
+ carga.mes_corte = form.mes_corte.data
165
+ carga.moneda_id = form.moneda_id.data
166
+ carga.saldo_acumulado = form.saldo_acumulado.data if form.saldo_acumulado.data is not None else Decimal("0.00")
167
+ carga.tipo_cambio = form.tipo_cambio.data if form.tipo_cambio.data is not None else Decimal("1.0")
168
+ carga.saldo_convertido = (
169
+ form.saldo_convertido.data if form.saldo_convertido.data is not None else Decimal("0.00")
170
+ )
171
+ carga.observaciones = form.observaciones.data
172
+ carga.modificado_por = current_user.usuario if current_user.is_authenticated else None
173
+
174
+ db.session.commit()
175
+
176
+ flash(_("Carga inicial actualizada exitosamente."), "success")
177
+ return redirect(url_for("carga_inicial_prestacion.index"))
178
+
179
+ return render_template("modules/carga_inicial_prestacion/form.html", form=form, carga=carga)
180
+
181
+
182
+ @carga_inicial_prestacion_bp.route("/<carga_id>/aplicar", methods=["POST"])
183
+ @require_write_access()
184
+ def aplicar(carga_id):
185
+ """Apply an initial balance load - creates transaction in prestacion_acumulada."""
186
+ carga = CargaInicialPrestacion.query.get_or_404(carga_id)
187
+
188
+ if carga.estado == "aplicado":
189
+ flash(_("Esta carga inicial ya ha sido aplicada."), "warning")
190
+ return redirect(url_for("carga_inicial_prestacion.index"))
191
+
192
+ try:
193
+ # Create transaction in prestacion_acumulada
194
+ transaccion = PrestacionAcumulada(
195
+ empleado_id=carga.empleado_id,
196
+ prestacion_id=carga.prestacion_id,
197
+ fecha_transaccion=datetime.now().date(),
198
+ tipo_transaccion="saldo_inicial",
199
+ anio=carga.anio_corte,
200
+ mes=carga.mes_corte,
201
+ moneda_id=carga.moneda_id,
202
+ monto_transaccion=carga.saldo_convertido,
203
+ saldo_anterior=Decimal("0.00"),
204
+ saldo_nuevo=carga.saldo_convertido,
205
+ carga_inicial_id=carga.id,
206
+ observaciones=f"Carga inicial - {carga.observaciones or ''}",
207
+ procesado_por=current_user.usuario if current_user.is_authenticated else None,
208
+ creado_por=current_user.usuario if current_user.is_authenticated else None,
209
+ )
210
+
211
+ db.session.add(transaccion)
212
+
213
+ # Update carga status
214
+ carga.estado = "aplicado"
215
+ carga.fecha_aplicacion = datetime.now()
216
+ carga.aplicado_por = current_user.usuario if current_user.is_authenticated else None
217
+ carga.modificado_por = current_user.usuario if current_user.is_authenticated else None
218
+
219
+ db.session.commit()
220
+
221
+ flash(_("Carga inicial aplicada exitosamente."), "success")
222
+
223
+ except Exception as e:
224
+ db.session.rollback()
225
+ flash(_("Error al aplicar la carga inicial: %(error)s", error=str(e)), "danger")
226
+
227
+ return redirect(url_for("carga_inicial_prestacion.index"))
228
+
229
+
230
+ @carga_inicial_prestacion_bp.route("/<carga_id>/eliminar", methods=["POST"])
231
+ @require_write_access()
232
+ def eliminar(carga_id):
233
+ """Delete an initial balance load (only if in draft status)."""
234
+ carga = CargaInicialPrestacion.query.get_or_404(carga_id)
235
+
236
+ if carga.estado == "aplicado":
237
+ flash(_("No se puede eliminar una carga inicial ya aplicada."), "warning")
238
+ return redirect(url_for("carga_inicial_prestacion.index"))
239
+
240
+ try:
241
+ db.session.delete(carga)
242
+ db.session.commit()
243
+ flash(_("Carga inicial eliminada exitosamente."), "success")
244
+ except Exception as e:
245
+ db.session.rollback()
246
+ flash(_("Error al eliminar la carga inicial: %(error)s", error=str(e)), "danger")
247
+
248
+ return redirect(url_for("carga_inicial_prestacion.index"))
249
+
250
+
251
+ @carga_inicial_prestacion_bp.route("/reporte")
252
+ @require_read_access()
253
+ def reporte():
254
+ """Generate accumulated benefits report."""
255
+ # Get filter parameters
256
+ empleado_id = request.args.get("empleado_id")
257
+ prestacion_id = request.args.get("prestacion_id")
258
+ fecha_desde = request.args.get("fecha_desde")
259
+ fecha_hasta = request.args.get("fecha_hasta")
260
+
261
+ # Build query
262
+ query = PrestacionAcumulada.query
263
+
264
+ if empleado_id:
265
+ query = query.filter(PrestacionAcumulada.empleado_id == empleado_id)
266
+
267
+ if prestacion_id:
268
+ query = query.filter(PrestacionAcumulada.prestacion_id == prestacion_id)
269
+
270
+ if fecha_desde:
271
+ query = query.filter(PrestacionAcumulada.fecha_transaccion >= fecha_desde)
272
+
273
+ if fecha_hasta:
274
+ query = query.filter(PrestacionAcumulada.fecha_transaccion <= fecha_hasta)
275
+
276
+ # Order by date
277
+ transacciones = query.order_by(
278
+ PrestacionAcumulada.empleado_id, PrestacionAcumulada.prestacion_id, PrestacionAcumulada.fecha_transaccion
279
+ ).all()
280
+
281
+ # Get choices for filters
282
+ empleados = Empleado.query.filter_by(activo=True).order_by(Empleado.codigo_empleado).all()
283
+ prestaciones = Prestacion.query.filter_by(activo=True).order_by(Prestacion.codigo).all()
284
+
285
+ return render_template(
286
+ "modules/carga_inicial_prestacion/reporte.html",
287
+ transacciones=transacciones,
288
+ empleados=empleados,
289
+ prestaciones=prestaciones,
290
+ empleado_id=empleado_id,
291
+ prestacion_id=prestacion_id,
292
+ fecha_desde=fecha_desde,
293
+ fecha_hasta=fecha_hasta,
294
+ )
295
+
296
+
297
+ @carga_inicial_prestacion_bp.route("/reporte/excel")
298
+ @require_read_access()
299
+ def reporte_excel():
300
+ """Export accumulated benefits report to Excel."""
301
+ import io
302
+
303
+ try:
304
+ from openpyxl import Workbook
305
+ from openpyxl.styles import Font, PatternFill
306
+ except ImportError:
307
+ flash(_("La librería openpyxl no está instalada. No se puede generar el reporte Excel."), "danger")
308
+ return redirect(url_for("carga_inicial_prestacion.reporte"))
309
+
310
+ # Get filter parameters (same as reporte)
311
+ empleado_id = request.args.get("empleado_id")
312
+ prestacion_id = request.args.get("prestacion_id")
313
+ fecha_desde = request.args.get("fecha_desde")
314
+ fecha_hasta = request.args.get("fecha_hasta")
315
+
316
+ # Build query
317
+ query = PrestacionAcumulada.query
318
+
319
+ if empleado_id:
320
+ query = query.filter(PrestacionAcumulada.empleado_id == empleado_id)
321
+
322
+ if prestacion_id:
323
+ query = query.filter(PrestacionAcumulada.prestacion_id == prestacion_id)
324
+
325
+ if fecha_desde:
326
+ query = query.filter(PrestacionAcumulada.fecha_transaccion >= fecha_desde)
327
+
328
+ if fecha_hasta:
329
+ query = query.filter(PrestacionAcumulada.fecha_transaccion <= fecha_hasta)
330
+
331
+ transacciones = query.order_by(
332
+ PrestacionAcumulada.empleado_id, PrestacionAcumulada.prestacion_id, PrestacionAcumulada.fecha_transaccion
333
+ ).all()
334
+
335
+ # Create Excel workbook
336
+ wb = Workbook()
337
+ ws = wb.active
338
+ ws.title = "Prestaciones Acumuladas"
339
+
340
+ # Header style
341
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
342
+ header_font = Font(color="FFFFFF", bold=True)
343
+
344
+ # Headers - Enhanced for audit purposes
345
+ headers = [
346
+ "ID Transacción",
347
+ "Fecha Transacción",
348
+ "Código Empleado",
349
+ "Empleado",
350
+ "Código Prestación",
351
+ "Prestación",
352
+ "Tipo Acumulación",
353
+ "Tipo Transacción",
354
+ "Año",
355
+ "Mes",
356
+ "Monto Transacción",
357
+ "Saldo Anterior",
358
+ "Saldo Nuevo",
359
+ "Moneda",
360
+ "Nómina ID",
361
+ "Carga Inicial ID",
362
+ "Procesado Por",
363
+ "Fecha Creación",
364
+ "Creado Por",
365
+ "Observaciones",
366
+ ]
367
+
368
+ for col_num, header in enumerate(headers, 1):
369
+ cell = ws.cell(row=1, column=col_num, value=header)
370
+ cell.fill = header_fill
371
+ cell.font = header_font
372
+
373
+ # Data rows - Enhanced with all audit fields
374
+ for row_num, trans in enumerate(transacciones, 2):
375
+ ws.cell(row=row_num, column=1, value=trans.id)
376
+ ws.cell(row=row_num, column=2, value=trans.fecha_transaccion.strftime("%Y-%m-%d"))
377
+ ws.cell(row=row_num, column=3, value=trans.empleado.codigo_empleado)
378
+ ws.cell(
379
+ row=row_num,
380
+ column=4,
381
+ value=f"{trans.empleado.primer_nombre} {trans.empleado.primer_apellido}",
382
+ )
383
+ ws.cell(row=row_num, column=5, value=trans.prestacion.codigo)
384
+ ws.cell(row=row_num, column=6, value=trans.prestacion.nombre)
385
+ ws.cell(row=row_num, column=7, value=trans.prestacion.tipo_acumulacion)
386
+ ws.cell(row=row_num, column=8, value=trans.tipo_transaccion)
387
+ ws.cell(row=row_num, column=9, value=trans.anio)
388
+ ws.cell(row=row_num, column=10, value=trans.mes)
389
+ ws.cell(row=row_num, column=11, value=float(trans.monto_transaccion))
390
+ ws.cell(row=row_num, column=12, value=float(trans.saldo_anterior))
391
+ ws.cell(row=row_num, column=13, value=float(trans.saldo_nuevo))
392
+ ws.cell(row=row_num, column=14, value=trans.moneda.codigo)
393
+ ws.cell(row=row_num, column=15, value=trans.nomina_id or "")
394
+ ws.cell(row=row_num, column=16, value=trans.carga_inicial_id or "")
395
+ ws.cell(row=row_num, column=17, value=trans.procesado_por or "")
396
+ ws.cell(row=row_num, column=18, value=trans.creado.strftime("%Y-%m-%d"))
397
+ ws.cell(row=row_num, column=19, value=trans.creado_por or "")
398
+ ws.cell(row=row_num, column=20, value=trans.observaciones or "")
399
+
400
+ # Auto-adjust column widths
401
+ for column in ws.columns:
402
+ max_length = 0
403
+ column_letter = column[0].column_letter
404
+ for cell in column:
405
+ try:
406
+ if cell.value is not None and len(str(cell.value)) > max_length:
407
+ max_length = len(cell.value)
408
+ except (TypeError, AttributeError):
409
+ # Skip cells with values that can't be converted to string
410
+ pass
411
+ adjusted_width = min(max_length + 2, 50)
412
+ ws.column_dimensions[column_letter].width = adjusted_width
413
+
414
+ # Save to BytesIO
415
+ output = io.BytesIO()
416
+ wb.save(output)
417
+ output.seek(0)
418
+
419
+ return Response(
420
+ output.read(),
421
+ mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
422
+ headers={"Content-Disposition": "attachment;filename=prestaciones_acumuladas.xlsx"},
423
+ )
@@ -0,0 +1,72 @@
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
+ """Configuración de parámetros de cálculo."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from flask import Blueprint, flash, redirect, render_template, url_for
20
+
21
+ from coati_payroll.forms import ConfiguracionCalculosForm
22
+ from coati_payroll.i18n import _
23
+ from coati_payroll.model import ConfiguracionCalculos, db
24
+ from coati_payroll.rbac import require_write_access
25
+
26
+
27
+ config_calculos_bp = Blueprint("config_calculos", __name__, url_prefix="/config-calculos")
28
+
29
+
30
+ def _get_or_create_global_config() -> ConfiguracionCalculos:
31
+ """Get or create the global (empresa=None, pais=None) calculation config."""
32
+ config = db.session.execute(
33
+ db.select(ConfiguracionCalculos).filter(
34
+ ConfiguracionCalculos.empresa_id.is_(None),
35
+ ConfiguracionCalculos.pais_id.is_(None),
36
+ ConfiguracionCalculos.activo.is_(True),
37
+ )
38
+ ).scalar_one_or_none()
39
+
40
+ if config:
41
+ return config
42
+
43
+ config = ConfiguracionCalculos(
44
+ empresa_id=None,
45
+ pais_id=None,
46
+ activo=True,
47
+ )
48
+ db.session.add(config)
49
+ db.session.commit()
50
+ return config
51
+
52
+
53
+ @config_calculos_bp.route("/", methods=["GET", "POST"])
54
+ @require_write_access()
55
+ def index():
56
+ """Edit global calculation parameters."""
57
+ config = _get_or_create_global_config()
58
+ form = ConfiguracionCalculosForm(obj=config)
59
+
60
+ if form.validate_on_submit():
61
+ config.liquidacion_modo_dias = form.liquidacion_modo_dias.data
62
+ config.liquidacion_factor_calendario = form.liquidacion_factor_calendario.data
63
+ config.liquidacion_factor_laboral = form.liquidacion_factor_laboral.data
64
+ db.session.commit()
65
+ flash(_("Configuración de cálculos actualizada."), "success")
66
+ return redirect(url_for("config_calculos.index"))
67
+
68
+ return render_template(
69
+ "modules/config_calculos/index.html",
70
+ form=form,
71
+ config=config,
72
+ )
@@ -0,0 +1,87 @@
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
+ """Global configuration views."""
15
+
16
+ from __future__ import annotations
17
+
18
+ # <-------------------------------------------------------------------------> #
19
+ # Standard library
20
+ # <-------------------------------------------------------------------------> #
21
+
22
+ # <-------------------------------------------------------------------------> #
23
+ # Third party libraries
24
+ # <-------------------------------------------------------------------------> #
25
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
26
+
27
+ # <-------------------------------------------------------------------------> #
28
+ # Local modules
29
+ # <-------------------------------------------------------------------------> #
30
+ from coati_payroll.i18n import _
31
+ from coati_payroll.rbac import require_write_access
32
+ from coati_payroll.locale_config import (
33
+ SUPPORTED_LANGUAGES,
34
+ get_language_from_db,
35
+ set_language_in_db,
36
+ )
37
+
38
+ configuracion_bp = Blueprint("configuracion", __name__, url_prefix="/configuracion")
39
+
40
+
41
+ @configuracion_bp.route("/")
42
+ @require_write_access()
43
+ def index():
44
+ """Display global configuration page."""
45
+ current_language = get_language_from_db()
46
+
47
+ # Language names for display
48
+ language_names = {"en": "English", "es": "Español"}
49
+
50
+ return render_template(
51
+ "modules/configuracion/index.html",
52
+ current_language=current_language,
53
+ supported_languages=SUPPORTED_LANGUAGES,
54
+ language_names=language_names,
55
+ )
56
+
57
+
58
+ @configuracion_bp.route("/idioma", methods=["POST"])
59
+ @require_write_access()
60
+ def cambiar_idioma():
61
+ """Change the application language."""
62
+ new_language = request.form.get("idioma", "").strip()
63
+
64
+ if not new_language:
65
+ flash(_("Por favor seleccione un idioma."), "warning")
66
+ return redirect(url_for("configuracion.index"))
67
+
68
+ if new_language not in SUPPORTED_LANGUAGES:
69
+ flash(_("Idioma no soportado."), "danger")
70
+ return redirect(url_for("configuracion.index"))
71
+
72
+ try:
73
+ set_language_in_db(new_language)
74
+
75
+ # Message will be shown in the new language after redirect
76
+ language_names = {"en": "English", "es": "Español"}
77
+ flash(
78
+ _(
79
+ "Idioma actualizado a %(language)s.",
80
+ language=language_names[new_language],
81
+ ),
82
+ "success",
83
+ )
84
+ except Exception as e:
85
+ flash(_("Error al actualizar el idioma: %(error)s", error=str(e)), "danger")
86
+
87
+ return redirect(url_for("configuracion.index"))
@@ -0,0 +1,17 @@
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
+ """Constants for the vistas module."""
15
+
16
+ # Number of items per page for pagination
17
+ PER_PAGE = 10