coati-payroll 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of coati-payroll might be problematic. Click here for more details.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|