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,687 @@
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
+ """Service for Excel export operations."""
15
+
16
+ from io import BytesIO
17
+ from coati_payroll.model import (
18
+ db,
19
+ Planilla,
20
+ Nomina,
21
+ NominaEmpleado,
22
+ NominaDetalle,
23
+ Liquidacion,
24
+ LiquidacionDetalle,
25
+ ComprobanteContable,
26
+ )
27
+ from coati_payroll.vistas.planilla.helpers.excel_helpers import check_openpyxl_available
28
+ from coati_payroll.nomina_engine.services.accounting_voucher_service import AccountingVoucherService
29
+
30
+ # Constants
31
+ ERROR_OPENPYXL_NOT_AVAILABLE = "openpyxl no está disponible"
32
+
33
+
34
+ class ExportService:
35
+ """Service for Excel export operations."""
36
+
37
+ @staticmethod
38
+ def exportar_nomina_excel(planilla: Planilla, nomina: Nomina) -> tuple[BytesIO, str]:
39
+ """Export nomina to Excel with employee details and calculations.
40
+
41
+ Args:
42
+ planilla: The planilla
43
+ nomina: The nomina to export
44
+
45
+ Returns:
46
+ Tuple of (BytesIO file object, filename)
47
+ """
48
+ openpyxl_classes = check_openpyxl_available()
49
+ if not openpyxl_classes:
50
+ raise ImportError(ERROR_OPENPYXL_NOT_AVAILABLE)
51
+
52
+ Workbook, Font, Alignment, PatternFill, Border, Side = openpyxl_classes
53
+
54
+ # Get all nomina employees
55
+ nomina_empleados = db.session.execute(db.select(NominaEmpleado).filter_by(nomina_id=nomina.id)).scalars().all()
56
+
57
+ # Create workbook
58
+ wb = Workbook()
59
+ ws = wb.active
60
+ ws.title = "Nómina"
61
+
62
+ # Define styles
63
+ header_font = Font(bold=True, size=14, color="FFFFFF")
64
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
65
+ subheader_font = Font(bold=True, size=11)
66
+ subheader_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
67
+ border = Border(
68
+ left=Side(style="thin"),
69
+ right=Side(style="thin"),
70
+ top=Side(style="thin"),
71
+ bottom=Side(style="thin"),
72
+ )
73
+
74
+ # Title
75
+ ws.merge_cells("A1:P1")
76
+ title_cell = ws["A1"]
77
+ title_cell.value = f"NÓMINA - {planilla.nombre}"
78
+ title_cell.font = header_font
79
+ title_cell.fill = header_fill
80
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
81
+
82
+ # Nomina info
83
+ row = 3
84
+ if planilla.empresa_id and planilla.empresa:
85
+ ws[f"A{row}"] = "Empresa:"
86
+ ws[f"B{row}"] = planilla.empresa.razon_social
87
+ row += 1
88
+ if planilla.empresa.ruc:
89
+ ws[f"A{row}"] = "RUC:"
90
+ ws[f"B{row}"] = planilla.empresa.ruc
91
+ row += 1
92
+ ws[f"A{row}"] = "Período:"
93
+ ws[f"B{row}"] = f"{nomina.periodo_inicio.strftime('%d/%m/%Y')} - {nomina.periodo_fin.strftime('%d/%m/%Y')}"
94
+ row += 1
95
+ ws[f"A{row}"] = "Estado:"
96
+ ws[f"B{row}"] = nomina.estado
97
+ row += 1
98
+ ws[f"A{row}"] = "Generado por:"
99
+ ws[f"B{row}"] = nomina.generado_por or ""
100
+ row += 2
101
+
102
+ # Table headers
103
+ headers = [
104
+ "Cód. Empleado",
105
+ "Identificación",
106
+ "No. Seg. Social",
107
+ "ID Fiscal",
108
+ "Nombres",
109
+ "Apellidos",
110
+ "Cargo",
111
+ "Salario Base",
112
+ "Total Percepciones",
113
+ "Total Deducciones",
114
+ "Salario Neto",
115
+ ]
116
+
117
+ for col, header in enumerate(headers, start=1):
118
+ cell = ws.cell(row=row, column=col, value=header)
119
+ cell.font = subheader_font
120
+ cell.fill = subheader_fill
121
+ cell.alignment = Alignment(horizontal="center", vertical="center")
122
+ cell.border = border
123
+
124
+ # Data rows
125
+ for ne in nomina_empleados:
126
+ row += 1
127
+ emp = ne.empleado
128
+
129
+ ws.cell(row=row, column=1, value=emp.codigo_empleado).border = border
130
+ ws.cell(row=row, column=2, value=emp.identificacion_personal).border = border
131
+ ws.cell(row=row, column=3, value=emp.id_seguridad_social or "").border = border
132
+ ws.cell(row=row, column=4, value=emp.id_fiscal or "").border = border
133
+ ws.cell(row=row, column=5, value=f"{emp.primer_nombre} {emp.segundo_nombre or ''}".strip()).border = border
134
+ ws.cell(row=row, column=6, value=f"{emp.primer_apellido} {emp.segundo_apellido or ''}".strip()).border = (
135
+ border
136
+ )
137
+ ws.cell(row=row, column=7, value=ne.cargo_snapshot or emp.cargo or "").border = border
138
+ ws.cell(row=row, column=8, value=float(ne.sueldo_base_historico)).border = border
139
+ ws.cell(row=row, column=9, value=float(ne.total_ingresos)).border = border
140
+ ws.cell(row=row, column=10, value=float(ne.total_deducciones)).border = border
141
+ ws.cell(row=row, column=11, value=float(ne.salario_neto)).border = border
142
+
143
+ # Auto-adjust column widths
144
+ for col in range(1, 12):
145
+ ws.column_dimensions[chr(64 + col)].width = 15
146
+
147
+ # Save to BytesIO
148
+ output = BytesIO()
149
+ wb.save(output)
150
+ output.seek(0)
151
+
152
+ filename = f"nomina_{planilla.nombre}_{nomina.periodo_inicio.strftime('%Y%m%d')}_{nomina.id[:8]}.xlsx"
153
+ return output, filename
154
+
155
+ @staticmethod
156
+ def exportar_prestaciones_excel(planilla: Planilla, nomina: Nomina) -> tuple[BytesIO, str]:
157
+ """Export benefits (prestaciones) to Excel separately.
158
+
159
+ Args:
160
+ planilla: The planilla
161
+ nomina: The nomina to export
162
+
163
+ Returns:
164
+ Tuple of (BytesIO file object, filename)
165
+ """
166
+ openpyxl_classes = check_openpyxl_available()
167
+ if not openpyxl_classes:
168
+ raise ImportError(ERROR_OPENPYXL_NOT_AVAILABLE)
169
+
170
+ Workbook, Font, Alignment, PatternFill, Border, Side = openpyxl_classes
171
+
172
+ # Get all nomina employees
173
+ nomina_empleados = db.session.execute(db.select(NominaEmpleado).filter_by(nomina_id=nomina.id)).scalars().all()
174
+
175
+ # Create workbook
176
+ wb = Workbook()
177
+ ws = wb.active
178
+ ws.title = "Prestaciones"
179
+
180
+ # Define styles
181
+ header_font = Font(bold=True, size=14, color="FFFFFF")
182
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
183
+ subheader_font = Font(bold=True, size=11)
184
+ subheader_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
185
+ border = Border(
186
+ left=Side(style="thin"),
187
+ right=Side(style="thin"),
188
+ top=Side(style="thin"),
189
+ bottom=Side(style="thin"),
190
+ )
191
+
192
+ # Title
193
+ ws.merge_cells("A1:F1")
194
+ title_cell = ws["A1"]
195
+ title_cell.value = f"PRESTACIONES LABORALES - {planilla.nombre}"
196
+ title_cell.font = header_font
197
+ title_cell.fill = header_fill
198
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
199
+
200
+ # Nomina info
201
+ row = 3
202
+ if planilla.empresa_id and planilla.empresa:
203
+ ws[f"A{row}"] = "Empresa:"
204
+ ws[f"B{row}"] = planilla.empresa.razon_social
205
+ row += 1
206
+ if planilla.empresa.ruc:
207
+ ws[f"A{row}"] = "RUC:"
208
+ ws[f"B{row}"] = planilla.empresa.ruc
209
+ row += 1
210
+ ws[f"A{row}"] = "Período:"
211
+ ws[f"B{row}"] = f"{nomina.periodo_inicio.strftime('%d/%m/%Y')} - {nomina.periodo_fin.strftime('%d/%m/%Y')}"
212
+ row += 2
213
+
214
+ # Table headers
215
+ headers = ["Cód. Empleado", "Nombres", "Apellidos"]
216
+
217
+ # Get all unique prestaciones
218
+ prestaciones_set = set()
219
+ for ne in nomina_empleados:
220
+ detalles = (
221
+ db.session.execute(db.select(NominaDetalle).filter_by(nomina_empleado_id=ne.id, tipo="prestacion"))
222
+ .scalars()
223
+ .all()
224
+ )
225
+ for d in detalles:
226
+ prestaciones_set.add((d.codigo, d.descripcion))
227
+
228
+ prestaciones_list = sorted(prestaciones_set, key=lambda x: x[0])
229
+ headers.extend([p[1] or p[0] for p in prestaciones_list])
230
+
231
+ for col, header in enumerate(headers, start=1):
232
+ cell = ws.cell(row=row, column=col, value=header)
233
+ cell.font = subheader_font
234
+ cell.fill = subheader_fill
235
+ cell.alignment = Alignment(horizontal="center", vertical="center")
236
+ cell.border = border
237
+
238
+ # Data rows
239
+ for ne in nomina_empleados:
240
+ row += 1
241
+ emp = ne.empleado
242
+
243
+ ws.cell(row=row, column=1, value=emp.codigo_empleado).border = border
244
+ ws.cell(row=row, column=2, value=f"{emp.primer_nombre} {emp.segundo_nombre or ''}".strip()).border = border
245
+ ws.cell(row=row, column=3, value=f"{emp.primer_apellido} {emp.segundo_apellido or ''}".strip()).border = (
246
+ border
247
+ )
248
+
249
+ # Get prestaciones for this employee
250
+ detalles = (
251
+ db.session.execute(
252
+ db.select(NominaDetalle)
253
+ .filter_by(nomina_empleado_id=ne.id, tipo="prestacion")
254
+ .order_by(NominaDetalle.orden)
255
+ )
256
+ .scalars()
257
+ .all()
258
+ )
259
+
260
+ prestaciones_dict = {d.codigo: float(d.monto) for d in detalles}
261
+
262
+ # Fill prestacion amounts
263
+ for col_idx, (codigo, _nombre) in enumerate(prestaciones_list, start=4):
264
+ cell = ws.cell(row=row, column=col_idx, value=prestaciones_dict.get(codigo, 0.0))
265
+ cell.border = border
266
+
267
+ # Auto-adjust column widths
268
+ for col in range(1, min(len(headers) + 1, 27)):
269
+ ws.column_dimensions[chr(64 + col)].width = 15
270
+
271
+ # Save to BytesIO
272
+ output = BytesIO()
273
+ wb.save(output)
274
+ output.seek(0)
275
+
276
+ filename = f"prestaciones_{planilla.nombre}_{nomina.periodo_inicio.strftime('%Y%m%d')}_{nomina.id[:8]}.xlsx"
277
+ return output, filename
278
+
279
+ @staticmethod
280
+ def exportar_liquidacion_excel(liquidacion: Liquidacion) -> tuple[BytesIO, str]:
281
+ openpyxl_classes = check_openpyxl_available()
282
+ if not openpyxl_classes:
283
+ raise ImportError(ERROR_OPENPYXL_NOT_AVAILABLE)
284
+
285
+ Workbook, Font, Alignment, PatternFill, Border, Side = openpyxl_classes
286
+
287
+ liquidacion = db.session.merge(liquidacion)
288
+ empleado = liquidacion.empleado
289
+
290
+ detalles = (
291
+ db.session.execute(
292
+ db.select(LiquidacionDetalle)
293
+ .filter_by(liquidacion_id=liquidacion.id)
294
+ .order_by(LiquidacionDetalle.orden)
295
+ )
296
+ .scalars()
297
+ .all()
298
+ )
299
+
300
+ wb = Workbook()
301
+ ws = wb.active
302
+ ws.title = "Liquidación"
303
+
304
+ header_font = Font(bold=True, size=14, color="FFFFFF")
305
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
306
+ subheader_font = Font(bold=True, size=11)
307
+ subheader_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
308
+ border = Border(
309
+ left=Side(style="thin"),
310
+ right=Side(style="thin"),
311
+ top=Side(style="thin"),
312
+ bottom=Side(style="thin"),
313
+ )
314
+
315
+ ws.merge_cells("A1:F1")
316
+ title_cell = ws["A1"]
317
+ title_cell.value = "LIQUIDACIÓN"
318
+ title_cell.font = header_font
319
+ title_cell.fill = header_fill
320
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
321
+
322
+ row = 3
323
+ ws[f"A{row}"] = "Empleado:"
324
+ ws[f"B{row}"] = (
325
+ f"{empleado.codigo_empleado} - {empleado.primer_nombre} {empleado.primer_apellido}" if empleado else ""
326
+ )
327
+ row += 1
328
+ ws[f"A{row}"] = "Fecha cálculo:"
329
+ ws[f"B{row}"] = str(liquidacion.fecha_calculo)
330
+ row += 1
331
+ ws[f"A{row}"] = "Último día pagado:"
332
+ ws[f"B{row}"] = str(liquidacion.ultimo_dia_pagado or "")
333
+ row += 1
334
+ ws[f"A{row}"] = "Días por pagar:"
335
+ ws[f"B{row}"] = liquidacion.dias_por_pagar
336
+ row += 1
337
+ ws[f"A{row}"] = "Estado:"
338
+ ws[f"B{row}"] = liquidacion.estado
339
+ row += 2
340
+
341
+ headers = ["Tipo", "Código", "Descripción", "Monto"]
342
+ for col, header in enumerate(headers, start=1):
343
+ cell = ws.cell(row=row, column=col, value=header)
344
+ cell.font = subheader_font
345
+ cell.fill = subheader_fill
346
+ cell.alignment = Alignment(horizontal="center", vertical="center")
347
+ cell.border = border
348
+
349
+ for d in detalles:
350
+ row += 1
351
+ ws.cell(row=row, column=1, value=d.tipo).border = border
352
+ ws.cell(row=row, column=2, value=d.codigo).border = border
353
+ ws.cell(row=row, column=3, value=d.descripcion or "").border = border
354
+ ws.cell(row=row, column=4, value=float(d.monto)).border = border
355
+
356
+ row += 2
357
+ ws[f"A{row}"] = "Total bruto:"
358
+ ws[f"B{row}"] = float(liquidacion.total_bruto or 0)
359
+ row += 1
360
+ ws[f"A{row}"] = "Total deducciones:"
361
+ ws[f"B{row}"] = float(liquidacion.total_deducciones or 0)
362
+ row += 1
363
+ ws[f"A{row}"] = "Total neto:"
364
+ ws[f"B{row}"] = float(liquidacion.total_neto or 0)
365
+
366
+ ws.column_dimensions["A"].width = 18
367
+ ws.column_dimensions["B"].width = 25
368
+ ws.column_dimensions["C"].width = 45
369
+ ws.column_dimensions["D"].width = 15
370
+
371
+ output = BytesIO()
372
+ wb.save(output)
373
+ output.seek(0)
374
+
375
+ emp_code = empleado.codigo_empleado if empleado else "empleado"
376
+ filename = f"liquidacion_{emp_code}_{liquidacion.fecha_calculo.strftime('%Y%m%d')}_{liquidacion.id[:8]}.xlsx"
377
+ return output, filename
378
+
379
+ @staticmethod
380
+ def exportar_comprobante_excel(planilla: Planilla, nomina: Nomina) -> tuple[BytesIO, str]:
381
+ """Export summarized accounting voucher (comprobante contable) to Excel.
382
+
383
+ Exports the accounting voucher grouped by account and cost center with netted amounts.
384
+
385
+ Raises ValueError if accounting configuration is incomplete.
386
+
387
+ Args:
388
+ planilla: The planilla
389
+ nomina: The nomina to export
390
+
391
+ Returns:
392
+ Tuple of (BytesIO file object, filename)
393
+
394
+ Raises:
395
+ ValueError: If comprobante doesn't exist or has incomplete accounting configuration
396
+ """
397
+ openpyxl_classes = check_openpyxl_available()
398
+ if not openpyxl_classes:
399
+ raise ImportError("openpyxl no está disponible")
400
+
401
+ Workbook, Font, Alignment, PatternFill, Border, Side = openpyxl_classes
402
+
403
+ # Get comprobante
404
+ comprobante = db.session.execute(
405
+ db.select(ComprobanteContable).filter_by(nomina_id=nomina.id)
406
+ ).scalar_one_or_none()
407
+
408
+ if not comprobante:
409
+ raise ValueError("No existe comprobante contable para esta nómina")
410
+
411
+ # Get summarized entries - will raise ValueError if accounts are NULL
412
+ accounting_service = AccountingVoucherService(db.session)
413
+ try:
414
+ summarized_entries = accounting_service.summarize_voucher(comprobante)
415
+ except ValueError as e:
416
+ # Re-raise with clear message about incomplete configuration
417
+ raise ValueError(
418
+ f"No se puede exportar comprobante sumarizado: {str(e)} "
419
+ "Utilice la exportación detallada para auditoría."
420
+ ) from e
421
+
422
+ # Create workbook
423
+ wb = Workbook()
424
+ ws = wb.active
425
+ ws.title = "Comprobante Contable"
426
+
427
+ # Define styles
428
+ header_font = Font(bold=True, size=14, color="FFFFFF")
429
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
430
+ subheader_font = Font(bold=True, size=11)
431
+ subheader_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
432
+ total_font = Font(bold=True, size=11)
433
+ total_fill = PatternFill(start_color="D9D9D9", end_color="D9D9D9", fill_type="solid")
434
+ border = Border(
435
+ left=Side(style="thin"),
436
+ right=Side(style="thin"),
437
+ top=Side(style="thin"),
438
+ bottom=Side(style="thin"),
439
+ )
440
+
441
+ # Title
442
+ ws.merge_cells("A1:F1")
443
+ title_cell = ws["A1"]
444
+ title_cell.value = f"COMPROBANTE CONTABLE - {planilla.nombre}"
445
+ title_cell.font = header_font
446
+ title_cell.fill = header_fill
447
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
448
+
449
+ # Comprobante info
450
+ row = 3
451
+ if planilla.empresa_id and planilla.empresa:
452
+ ws[f"A{row}"] = "Empresa:"
453
+ ws[f"B{row}"] = planilla.empresa.razon_social
454
+ row += 1
455
+
456
+ ws[f"A{row}"] = "Concepto:"
457
+ ws[f"B{row}"] = comprobante.concepto or ""
458
+ row += 1
459
+
460
+ ws[f"A{row}"] = "Fecha de Cálculo:"
461
+ ws[f"B{row}"] = comprobante.fecha_calculo.strftime("%d/%m/%Y")
462
+ row += 1
463
+
464
+ ws[f"A{row}"] = "Período:"
465
+ ws[f"B{row}"] = f"{nomina.periodo_inicio.strftime('%d/%m/%Y')} - {nomina.periodo_fin.strftime('%d/%m/%Y')}"
466
+ row += 1
467
+
468
+ if comprobante.moneda:
469
+ ws[f"A{row}"] = "Moneda:"
470
+ ws[f"B{row}"] = f"{comprobante.moneda.codigo} - {comprobante.moneda.nombre}"
471
+ row += 1
472
+
473
+ # Audit trail information
474
+ if comprobante.aplicado_por:
475
+ ws[f"A{row}"] = "Aplicado por:"
476
+ ws[f"B{row}"] = comprobante.aplicado_por
477
+ row += 1
478
+
479
+ if comprobante.fecha_aplicacion:
480
+ ws[f"A{row}"] = "Fecha aplicación:"
481
+ ws[f"B{row}"] = comprobante.fecha_aplicacion.strftime("%d/%m/%Y %H:%M")
482
+ row += 1
483
+
484
+ if comprobante.veces_modificado > 0:
485
+ ws[f"A{row}"] = "Modificado:"
486
+ ws[f"B{row}"] = f"{comprobante.veces_modificado} vez/veces"
487
+ row += 1
488
+
489
+ if comprobante.modificado_por:
490
+ ws[f"A{row}"] = "Última modificación por:"
491
+ ws[f"B{row}"] = comprobante.modificado_por
492
+ row += 1
493
+
494
+ if comprobante.fecha_modificacion:
495
+ ws[f"A{row}"] = "Fecha última modificación:"
496
+ ws[f"B{row}"] = comprobante.fecha_modificacion.strftime("%d/%m/%Y %H:%M")
497
+ row += 1
498
+
499
+ row += 1
500
+
501
+ # Warnings if any
502
+ if comprobante.advertencias:
503
+ ws[f"A{row}"] = "ADVERTENCIAS:"
504
+ ws[f"A{row}"].font = Font(bold=True, color="FF0000")
505
+ row += 1
506
+ for warning in comprobante.advertencias:
507
+ ws[f"A{row}"] = f"• {warning}"
508
+ ws[f"A{row}"].font = Font(color="FF0000")
509
+ row += 1
510
+ row += 1
511
+
512
+ # Table headers
513
+ headers = ["Código Cuenta", "Descripción", "Centro de Costos", "Débito", "Crédito"]
514
+
515
+ for col, header in enumerate(headers, start=1):
516
+ cell = ws.cell(row=row, column=col, value=header)
517
+ cell.font = subheader_font
518
+ cell.fill = subheader_fill
519
+ cell.alignment = Alignment(horizontal="center", vertical="center")
520
+ cell.border = border
521
+
522
+ # Data rows
523
+ for entry in summarized_entries:
524
+ row += 1
525
+ ws.cell(row=row, column=1, value=entry["codigo_cuenta"]).border = border
526
+ ws.cell(row=row, column=2, value=entry["descripcion"]).border = border
527
+ ws.cell(row=row, column=3, value=entry["centro_costos"] or "").border = border
528
+ ws.cell(row=row, column=4, value=float(entry["debito"])).border = border
529
+ ws.cell(row=row, column=5, value=float(entry["credito"])).border = border
530
+
531
+ # Totals row
532
+ row += 1
533
+ ws.cell(row=row, column=1, value="TOTALES").font = total_font
534
+ ws.cell(row=row, column=1).fill = total_fill
535
+ ws.cell(row=row, column=1).border = border
536
+ ws.cell(row=row, column=2).border = border
537
+ ws.cell(row=row, column=3).border = border
538
+
539
+ cell_debito = ws.cell(row=row, column=4, value=float(comprobante.total_debitos))
540
+ cell_debito.font = total_font
541
+ cell_debito.fill = total_fill
542
+ cell_debito.border = border
543
+
544
+ cell_credito = ws.cell(row=row, column=5, value=float(comprobante.total_creditos))
545
+ cell_credito.font = total_font
546
+ cell_credito.fill = total_fill
547
+ cell_credito.border = border
548
+
549
+ # Balance check
550
+ row += 2
551
+ ws[f"A{row}"] = "Balance (debe ser 0):"
552
+ ws[f"B{row}"] = float(comprobante.balance)
553
+ if comprobante.balance != 0:
554
+ ws[f"B{row}"].font = Font(bold=True, color="FF0000")
555
+
556
+ # Auto-adjust column widths
557
+ ws.column_dimensions["A"].width = 18
558
+ ws.column_dimensions["B"].width = 40
559
+ ws.column_dimensions["C"].width = 20
560
+ ws.column_dimensions["D"].width = 15
561
+ ws.column_dimensions["E"].width = 15
562
+
563
+ # Save to BytesIO
564
+ output = BytesIO()
565
+ wb.save(output)
566
+ output.seek(0)
567
+
568
+ filename = f"comprobante_{planilla.nombre}_{nomina.periodo_inicio.strftime('%Y%m%d')}_{nomina.id[:8]}.xlsx"
569
+ return output, filename
570
+
571
+ @staticmethod
572
+ def exportar_comprobante_detallado_excel(planilla: Planilla, nomina: Nomina) -> tuple[BytesIO, str]:
573
+ """Export detailed accounting voucher per employee to Excel.
574
+
575
+ Exports the full accounting voucher with all lines per employee for audit purposes.
576
+ This export works even with incomplete accounting configuration, showing NULL for
577
+ missing account fields.
578
+
579
+ Args:
580
+ planilla: The planilla
581
+ nomina: The nomina to export
582
+
583
+ Returns:
584
+ Tuple of (BytesIO file object, filename)
585
+ """
586
+ openpyxl_classes = check_openpyxl_available()
587
+ if not openpyxl_classes:
588
+ raise ImportError("openpyxl no está disponible")
589
+
590
+ Workbook, Font, Alignment, PatternFill, Border, Side = openpyxl_classes
591
+
592
+ # Get comprobante
593
+ comprobante = db.session.execute(
594
+ db.select(ComprobanteContable).filter_by(nomina_id=nomina.id)
595
+ ).scalar_one_or_none()
596
+
597
+ if not comprobante:
598
+ raise ValueError("No existe comprobante contable para esta nómina")
599
+
600
+ # Get detailed entries
601
+ accounting_service = AccountingVoucherService(db.session)
602
+ detailed_entries = accounting_service.get_detailed_voucher_by_employee(comprobante)
603
+
604
+ # Create workbook
605
+ wb = Workbook()
606
+ ws = wb.active
607
+ ws.title = "Comprobante Detallado"
608
+
609
+ # Define styles
610
+ header_font = Font(bold=True, size=14, color="FFFFFF")
611
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
612
+ subheader_font = Font(bold=True, size=11)
613
+ subheader_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
614
+ border = Border(
615
+ left=Side(style="thin"),
616
+ right=Side(style="thin"),
617
+ top=Side(style="thin"),
618
+ bottom=Side(style="thin"),
619
+ )
620
+
621
+ # Title
622
+ ws.merge_cells("A1:G1")
623
+ title_cell = ws["A1"]
624
+ title_cell.value = f"COMPROBANTE CONTABLE DETALLADO - {planilla.nombre}"
625
+ title_cell.font = header_font
626
+ title_cell.fill = header_fill
627
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
628
+
629
+ # Comprobante info
630
+ row = 3
631
+ if planilla.empresa_id and planilla.empresa:
632
+ ws[f"A{row}"] = "Empresa:"
633
+ ws[f"B{row}"] = planilla.empresa.razon_social
634
+ row += 1
635
+
636
+ ws[f"A{row}"] = "Concepto:"
637
+ ws[f"B{row}"] = comprobante.concepto or ""
638
+ row += 1
639
+
640
+ ws[f"A{row}"] = "Fecha de Cálculo:"
641
+ ws[f"B{row}"] = comprobante.fecha_calculo.strftime("%d/%m/%Y")
642
+ row += 1
643
+
644
+ ws[f"A{row}"] = "Período:"
645
+ ws[f"B{row}"] = f"{nomina.periodo_inicio.strftime('%d/%m/%Y')} - {nomina.periodo_fin.strftime('%d/%m/%Y')}"
646
+ row += 2
647
+
648
+ # Table headers
649
+ headers = ["Código Empleado", "Empleado", "Concepto", "Código Cuenta", "Descripción", "Débito", "Crédito"]
650
+
651
+ for col, header in enumerate(headers, start=1):
652
+ cell = ws.cell(row=row, column=col, value=header)
653
+ cell.font = subheader_font
654
+ cell.fill = subheader_fill
655
+ cell.alignment = Alignment(horizontal="center", vertical="center")
656
+ cell.border = border
657
+
658
+ # Data rows - per employee
659
+ for employee_entry in detailed_entries:
660
+ for linea in employee_entry["lineas"]:
661
+ row += 1
662
+ ws.cell(row=row, column=1, value=employee_entry["empleado_codigo"]).border = border
663
+ ws.cell(row=row, column=2, value=employee_entry["empleado_nombre"]).border = border
664
+ ws.cell(row=row, column=3, value=linea["concepto"]).border = border
665
+ ws.cell(row=row, column=4, value=linea["codigo_cuenta"]).border = border
666
+ ws.cell(row=row, column=5, value=linea["descripcion_cuenta"]).border = border
667
+ ws.cell(row=row, column=6, value=float(linea["debito"])).border = border
668
+ ws.cell(row=row, column=7, value=float(linea["credito"])).border = border
669
+
670
+ # Auto-adjust column widths
671
+ ws.column_dimensions["A"].width = 18
672
+ ws.column_dimensions["B"].width = 30
673
+ ws.column_dimensions["C"].width = 30
674
+ ws.column_dimensions["D"].width = 18
675
+ ws.column_dimensions["E"].width = 35
676
+ ws.column_dimensions["F"].width = 15
677
+ ws.column_dimensions["G"].width = 15
678
+
679
+ # Save to BytesIO
680
+ output = BytesIO()
681
+ wb.save(output)
682
+ output.seek(0)
683
+
684
+ filename = (
685
+ f"comprobante_detallado_{planilla.nombre}_{nomina.periodo_inicio.strftime('%Y%m%d')}_{nomina.id[:8]}.xlsx"
686
+ )
687
+ return output, filename