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,272 @@
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
+ """Prestacion (Benefits) module views.
15
+
16
+ This module provides views for managing benefits including a dashboard
17
+ and bulk loading capabilities similar to the vacation module.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from decimal import Decimal
23
+
24
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
25
+ from flask_login import current_user, login_required
26
+ from sqlalchemy import func
27
+
28
+ from coati_payroll.enums import TipoUsuario
29
+ from coati_payroll.i18n import _
30
+ from coati_payroll.model import (
31
+ db,
32
+ Prestacion,
33
+ PrestacionAcumulada,
34
+ CargaInicialPrestacion,
35
+ Empleado,
36
+ Moneda,
37
+ )
38
+ from coati_payroll.rbac import require_role
39
+
40
+ # Constants
41
+ MAX_DISPLAYED_ERRORS = 10 # Maximum number of errors to display in bulk upload results
42
+
43
+ prestacion_management_bp = Blueprint("prestacion_management", __name__, url_prefix="/prestacion-management")
44
+
45
+
46
+ # ============================================================================
47
+ # Prestacion Dashboard
48
+ # ============================================================================
49
+
50
+
51
+ @prestacion_management_bp.route("/")
52
+ @login_required
53
+ def dashboard():
54
+ """Prestacion management dashboard."""
55
+ # Statistics
56
+ total_prestaciones = (
57
+ db.session.execute(db.select(func.count(Prestacion.id)).filter(Prestacion.activo.is_(True))).scalar() or 0
58
+ )
59
+
60
+ # Count employees with benefit balances
61
+ total_accounts = (
62
+ db.session.execute(db.select(func.count(func.distinct(PrestacionAcumulada.empleado_id)))).scalar() or 0
63
+ )
64
+
65
+ # Count pending initial loads
66
+ pending_loads = (
67
+ db.session.execute(
68
+ db.select(func.count(CargaInicialPrestacion.id)).filter(CargaInicialPrestacion.estado == "borrador")
69
+ ).scalar()
70
+ or 0
71
+ )
72
+
73
+ # Recent activity - latest transactions
74
+ recent_transactions = (
75
+ db.session.execute(
76
+ db.select(PrestacionAcumulada)
77
+ .join(PrestacionAcumulada.empleado)
78
+ .order_by(PrestacionAcumulada.fecha_transaccion.desc())
79
+ .limit(10)
80
+ )
81
+ .scalars()
82
+ .all()
83
+ )
84
+
85
+ return render_template(
86
+ "modules/prestacion_management/dashboard.html",
87
+ total_prestaciones=total_prestaciones,
88
+ total_accounts=total_accounts,
89
+ pending_loads=pending_loads,
90
+ recent_transactions=recent_transactions,
91
+ )
92
+
93
+
94
+ # ============================================================================
95
+ # Initial Balance Loading (for System Implementation)
96
+ # ============================================================================
97
+
98
+
99
+ @prestacion_management_bp.route("/initial-balance/bulk", methods=["GET", "POST"])
100
+ @require_role(TipoUsuario.ADMIN)
101
+ def initial_balance_bulk():
102
+ """Bulk load initial prestacion balances from Excel.
103
+
104
+ Used during system implementation for companies with many employees.
105
+ Allows uploading an Excel file with initial prestacion balances for multiple
106
+ employees at once.
107
+
108
+ Expected Excel format (without headers, data starts on row 1):
109
+ - Column A: Código de Empleado
110
+ - Column B: Código de Prestación
111
+ - Column C: Año de Corte
112
+ - Column D: Mes de Corte
113
+ - Column E: Código de Moneda
114
+ - Column F: Saldo Acumulado
115
+ - Column G: Tipo de Cambio (opcional, default 1.0)
116
+ - Column H: Observaciones (opcional)
117
+ """
118
+ if request.method == "POST":
119
+ # Check if file was uploaded
120
+ if "file" not in request.files:
121
+ flash(_("No se seleccionó ningún archivo."), "warning")
122
+ return redirect(url_for("prestacion_management.initial_balance_bulk"))
123
+
124
+ file = request.files["file"]
125
+
126
+ if file.filename == "":
127
+ flash(_("No se seleccionó ningún archivo."), "warning")
128
+ return redirect(url_for("prestacion_management.initial_balance_bulk"))
129
+
130
+ if not file.filename.endswith((".xlsx", ".xls")):
131
+ flash(_("Por favor, suba un archivo Excel (.xlsx o .xls)."), "warning")
132
+ return redirect(url_for("prestacion_management.initial_balance_bulk"))
133
+
134
+ try:
135
+ import openpyxl
136
+
137
+ # Load Excel file
138
+ workbook = openpyxl.load_workbook(file, data_only=True)
139
+ sheet = workbook.active
140
+
141
+ success_count = 0
142
+ error_count = 0
143
+ errors = []
144
+
145
+ # Process each row (data starts at row 1, no headers expected)
146
+ for row_num, row in enumerate(sheet.iter_rows(min_row=1, values_only=True), start=1):
147
+ codigo_empleado = row[0]
148
+ codigo_prestacion = row[1]
149
+ anio_corte = row[2]
150
+ mes_corte = row[3]
151
+ codigo_moneda = row[4]
152
+ saldo_acumulado = row[5]
153
+ tipo_cambio = row[6] if len(row) > 6 and row[6] is not None else Decimal("1.0")
154
+ observaciones = row[7] if len(row) > 7 else "Carga masiva de saldo inicial"
155
+
156
+ # Validate required fields
157
+ if not all(
158
+ [
159
+ codigo_empleado,
160
+ codigo_prestacion,
161
+ anio_corte,
162
+ mes_corte,
163
+ codigo_moneda,
164
+ saldo_acumulado is not None,
165
+ ]
166
+ ):
167
+ errors.append(f"Fila {row_num}: Faltan campos requeridos")
168
+ error_count += 1
169
+ continue
170
+
171
+ # Find employee
172
+ empleado = db.session.execute(
173
+ db.select(Empleado).filter(Empleado.codigo_empleado == codigo_empleado, Empleado.activo.is_(True))
174
+ ).scalar_one_or_none()
175
+
176
+ if not empleado:
177
+ errors.append(f"Fila {row_num}: Empleado {codigo_empleado} no encontrado")
178
+ error_count += 1
179
+ continue
180
+
181
+ # Find prestacion
182
+ prestacion = db.session.execute(
183
+ db.select(Prestacion).filter(Prestacion.codigo == codigo_prestacion, Prestacion.activo.is_(True))
184
+ ).scalar_one_or_none()
185
+
186
+ if not prestacion:
187
+ errors.append(f"Fila {row_num}: Prestación {codigo_prestacion} no encontrada")
188
+ error_count += 1
189
+ continue
190
+
191
+ # Find moneda
192
+ moneda = db.session.execute(
193
+ db.select(Moneda).filter(Moneda.codigo == codigo_moneda, Moneda.activo.is_(True))
194
+ ).scalar_one_or_none()
195
+
196
+ if not moneda:
197
+ errors.append(f"Fila {row_num}: Moneda {codigo_moneda} no encontrada")
198
+ error_count += 1
199
+ continue
200
+
201
+ # Check for duplicate
202
+ existing = db.session.execute(
203
+ db.select(CargaInicialPrestacion).filter(
204
+ CargaInicialPrestacion.empleado_id == empleado.id,
205
+ CargaInicialPrestacion.prestacion_id == prestacion.id,
206
+ CargaInicialPrestacion.anio_corte == anio_corte,
207
+ CargaInicialPrestacion.mes_corte == mes_corte,
208
+ )
209
+ ).scalar_one_or_none()
210
+
211
+ if existing:
212
+ errors.append(
213
+ f"Fila {row_num}: Duplicado {codigo_empleado}, {codigo_prestacion}, {mes_corte}/{anio_corte}"
214
+ )
215
+ error_count += 1
216
+ continue
217
+
218
+ try:
219
+ # Calculate saldo_convertido
220
+ saldo_convertido = Decimal(str(saldo_acumulado)) * Decimal(str(tipo_cambio))
221
+
222
+ # Create CargaInicialPrestacion
223
+ carga = CargaInicialPrestacion(
224
+ empleado_id=empleado.id,
225
+ prestacion_id=prestacion.id,
226
+ anio_corte=anio_corte,
227
+ mes_corte=mes_corte,
228
+ moneda_id=moneda.id,
229
+ saldo_acumulado=Decimal(str(saldo_acumulado)),
230
+ tipo_cambio=Decimal(str(tipo_cambio)),
231
+ saldo_convertido=saldo_convertido,
232
+ observaciones=str(observaciones) if observaciones else "Carga masiva de saldo inicial",
233
+ estado="borrador",
234
+ creado_por=current_user.usuario if current_user.is_authenticated else None,
235
+ )
236
+
237
+ db.session.add(carga)
238
+ success_count += 1
239
+
240
+ except Exception as e:
241
+ errors.append(f"Fila {row_num}: Error al procesar {codigo_empleado}: {str(e)}")
242
+ error_count += 1
243
+ continue
244
+
245
+ # Commit all changes
246
+ try:
247
+ db.session.commit()
248
+ flash(
249
+ _("Carga completada: {} registros exitosos en estado borrador, {} errores.").format(
250
+ success_count, error_count
251
+ ),
252
+ "success" if error_count == 0 else "warning",
253
+ )
254
+
255
+ if errors:
256
+ error_details = "<br>".join(errors[:MAX_DISPLAYED_ERRORS])
257
+ if len(errors) > MAX_DISPLAYED_ERRORS:
258
+ error_details += f"<br>...y {len(errors) - MAX_DISPLAYED_ERRORS} errores más"
259
+ flash(error_details, "warning")
260
+
261
+ except Exception as e:
262
+ db.session.rollback()
263
+ flash(_("Error al guardar los cambios: {}").format(str(e)), "danger")
264
+
265
+ except ImportError:
266
+ flash(_("Error: La librería openpyxl no está instalada. Contacte al administrador."), "danger")
267
+ except Exception as e:
268
+ flash(_("Error al procesar el archivo Excel: {}").format(str(e)), "danger")
269
+
270
+ return redirect(url_for("prestacion_management.initial_balance_bulk"))
271
+
272
+ return render_template("modules/prestacion_management/initial_balance_bulk.html")