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,708 @@
|
|
|
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 generating accounting vouchers from payroll calculations."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from decimal import Decimal
|
|
19
|
+
from typing import Any
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from datetime import date
|
|
22
|
+
|
|
23
|
+
from coati_payroll.model import (
|
|
24
|
+
db,
|
|
25
|
+
Nomina,
|
|
26
|
+
NominaEmpleado,
|
|
27
|
+
NominaDetalle,
|
|
28
|
+
Planilla,
|
|
29
|
+
Percepcion,
|
|
30
|
+
Deduccion,
|
|
31
|
+
Prestacion,
|
|
32
|
+
Adelanto,
|
|
33
|
+
ComprobanteContable,
|
|
34
|
+
ComprobanteContableLinea,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AccountingVoucherService:
|
|
39
|
+
"""Service for generating accounting vouchers from payroll calculations."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, session):
|
|
42
|
+
self.session = session
|
|
43
|
+
|
|
44
|
+
def validate_accounting_configuration(self, planilla: Planilla) -> tuple[bool, list[str]]:
|
|
45
|
+
"""Validate that all accounting configuration is complete.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
planilla: The planilla to validate
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (is_valid, list of warnings)
|
|
52
|
+
"""
|
|
53
|
+
warnings = []
|
|
54
|
+
|
|
55
|
+
# Check base salary accounts
|
|
56
|
+
if not planilla.codigo_cuenta_debe_salario:
|
|
57
|
+
warnings.append("Falta configurar la cuenta de débito para salario básico en la planilla")
|
|
58
|
+
if not planilla.codigo_cuenta_haber_salario:
|
|
59
|
+
warnings.append("Falta configurar la cuenta de crédito para salario básico en la planilla")
|
|
60
|
+
|
|
61
|
+
# Check percepciones
|
|
62
|
+
percepciones = (
|
|
63
|
+
self.session.execute(
|
|
64
|
+
db.select(Percepcion)
|
|
65
|
+
.join(Percepcion.planillas)
|
|
66
|
+
.filter(db.text("planilla_ingreso.planilla_id = :planilla_id"))
|
|
67
|
+
.params(planilla_id=planilla.id)
|
|
68
|
+
)
|
|
69
|
+
.scalars()
|
|
70
|
+
.all()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for percepcion in percepciones:
|
|
74
|
+
if percepcion.contabilizable:
|
|
75
|
+
if not percepcion.codigo_cuenta_debe:
|
|
76
|
+
warnings.append(
|
|
77
|
+
f"Percepción '{percepcion.nombre}' ({percepcion.codigo}) no tiene cuenta de débito configurada"
|
|
78
|
+
)
|
|
79
|
+
if not percepcion.codigo_cuenta_haber:
|
|
80
|
+
warnings.append(
|
|
81
|
+
f"Percepción '{percepcion.nombre}' ({percepcion.codigo}) no tiene cuenta de crédito configurada"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Check deducciones
|
|
85
|
+
deducciones = (
|
|
86
|
+
self.session.execute(
|
|
87
|
+
db.select(Deduccion)
|
|
88
|
+
.join(Deduccion.planillas)
|
|
89
|
+
.filter(db.text("planilla_deduccion.planilla_id = :planilla_id"))
|
|
90
|
+
.params(planilla_id=planilla.id)
|
|
91
|
+
)
|
|
92
|
+
.scalars()
|
|
93
|
+
.all()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for deduccion in deducciones:
|
|
97
|
+
if deduccion.contabilizable:
|
|
98
|
+
if not deduccion.codigo_cuenta_debe:
|
|
99
|
+
warnings.append(
|
|
100
|
+
f"Deducción '{deduccion.nombre}' ({deduccion.codigo}) no tiene cuenta de débito configurada"
|
|
101
|
+
)
|
|
102
|
+
if not deduccion.codigo_cuenta_haber:
|
|
103
|
+
warnings.append(
|
|
104
|
+
f"Deducción '{deduccion.nombre}' ({deduccion.codigo}) no tiene cuenta de crédito configurada"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Check prestaciones
|
|
108
|
+
prestaciones = (
|
|
109
|
+
self.session.execute(
|
|
110
|
+
db.select(Prestacion)
|
|
111
|
+
.join(Prestacion.planillas)
|
|
112
|
+
.filter(db.text("planilla_prestacion.planilla_id = :planilla_id"))
|
|
113
|
+
.params(planilla_id=planilla.id)
|
|
114
|
+
)
|
|
115
|
+
.scalars()
|
|
116
|
+
.all()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for prestacion in prestaciones:
|
|
120
|
+
if prestacion.contabilizable:
|
|
121
|
+
if not prestacion.codigo_cuenta_debe:
|
|
122
|
+
warnings.append(
|
|
123
|
+
f"Prestación '{prestacion.nombre}' ({prestacion.codigo}) no tiene cuenta de débito configurada"
|
|
124
|
+
)
|
|
125
|
+
if not prestacion.codigo_cuenta_haber:
|
|
126
|
+
warnings.append(
|
|
127
|
+
f"Prestación '{prestacion.nombre}' ({prestacion.codigo}) no tiene cuenta de crédito configurada"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
is_valid = len(warnings) == 0
|
|
131
|
+
return is_valid, warnings
|
|
132
|
+
|
|
133
|
+
def generate_accounting_voucher(
|
|
134
|
+
self, nomina: Nomina, planilla: Planilla, fecha_calculo: date = None, usuario: str = None
|
|
135
|
+
) -> ComprobanteContable:
|
|
136
|
+
"""Generate accounting voucher for a nomina with individual lines per employee.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
nomina: The nomina to generate voucher for
|
|
140
|
+
planilla: The planilla configuration
|
|
141
|
+
fecha_calculo: Calculation date (defaults to nomina periodo_fin)
|
|
142
|
+
usuario: User generating/regenerating the voucher
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
ComprobanteContable with generated line entries
|
|
146
|
+
"""
|
|
147
|
+
from datetime import datetime, timezone
|
|
148
|
+
|
|
149
|
+
# Validate configuration
|
|
150
|
+
is_valid, warnings = self.validate_accounting_configuration(planilla)
|
|
151
|
+
|
|
152
|
+
# Use nomina's calculation date or periodo_fin
|
|
153
|
+
if fecha_calculo is None:
|
|
154
|
+
fecha_calculo = nomina.fecha_calculo_original or nomina.periodo_fin
|
|
155
|
+
|
|
156
|
+
# Generate voucher concept
|
|
157
|
+
concepto = (
|
|
158
|
+
f"Nómina {planilla.nombre}"
|
|
159
|
+
+ f" - Período {nomina.periodo_inicio.strftime('%d/%m/%Y')} al "
|
|
160
|
+
+ f"{nomina.periodo_fin.strftime('%d/%m/%Y')}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Get or create comprobante
|
|
164
|
+
comprobante = self.session.execute(
|
|
165
|
+
db.select(ComprobanteContable).filter_by(nomina_id=nomina.id)
|
|
166
|
+
).scalar_one_or_none()
|
|
167
|
+
|
|
168
|
+
if comprobante:
|
|
169
|
+
# Regenerating - update modification audit trail
|
|
170
|
+
self.session.execute(
|
|
171
|
+
db.delete(ComprobanteContableLinea).where(ComprobanteContableLinea.comprobante_id == comprobante.id)
|
|
172
|
+
)
|
|
173
|
+
self.session.flush()
|
|
174
|
+
# Update header information
|
|
175
|
+
comprobante.fecha_calculo = fecha_calculo
|
|
176
|
+
comprobante.concepto = concepto
|
|
177
|
+
comprobante.moneda_id = planilla.moneda_id
|
|
178
|
+
comprobante.advertencias = warnings
|
|
179
|
+
# Update modification tracking
|
|
180
|
+
comprobante.modificado_por = usuario or nomina.generado_por
|
|
181
|
+
comprobante.fecha_modificacion = datetime.now(timezone.utc)
|
|
182
|
+
comprobante.veces_modificado += 1
|
|
183
|
+
else:
|
|
184
|
+
# Creating new - set initial audit trail
|
|
185
|
+
# Check if nomina is already applied to set aplicado_por
|
|
186
|
+
from coati_payroll.enums import NominaEstado
|
|
187
|
+
|
|
188
|
+
aplicado_por = None
|
|
189
|
+
fecha_aplicacion = None
|
|
190
|
+
if nomina.estado in (NominaEstado.APLICADO, NominaEstado.PAGADO):
|
|
191
|
+
aplicado_por = nomina.aplicado_por or usuario or nomina.generado_por
|
|
192
|
+
fecha_aplicacion = nomina.aplicado_en or datetime.now(timezone.utc)
|
|
193
|
+
|
|
194
|
+
comprobante = ComprobanteContable(
|
|
195
|
+
nomina_id=nomina.id,
|
|
196
|
+
fecha_calculo=fecha_calculo,
|
|
197
|
+
concepto=concepto,
|
|
198
|
+
moneda_id=planilla.moneda_id,
|
|
199
|
+
total_debitos=Decimal("0.00"),
|
|
200
|
+
total_creditos=Decimal("0.00"),
|
|
201
|
+
balance=Decimal("0.00"),
|
|
202
|
+
advertencias=warnings,
|
|
203
|
+
aplicado_por=aplicado_por,
|
|
204
|
+
fecha_aplicacion=fecha_aplicacion,
|
|
205
|
+
veces_modificado=0,
|
|
206
|
+
)
|
|
207
|
+
self.session.add(comprobante)
|
|
208
|
+
self.session.flush()
|
|
209
|
+
|
|
210
|
+
# Get all nomina employees with their details
|
|
211
|
+
nomina_empleados = (
|
|
212
|
+
self.session.execute(db.select(NominaEmpleado).filter_by(nomina_id=nomina.id)).scalars().all()
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Accumulate totals
|
|
216
|
+
total_debitos = Decimal("0.00")
|
|
217
|
+
total_creditos = Decimal("0.00")
|
|
218
|
+
orden = 0
|
|
219
|
+
|
|
220
|
+
# Process each employee
|
|
221
|
+
for ne in nomina_empleados:
|
|
222
|
+
empleado = ne.empleado
|
|
223
|
+
centro_costos = ne.centro_costos_snapshot or empleado.centro_costos
|
|
224
|
+
empleado_nombre_completo = f"{empleado.primer_nombre} {empleado.primer_apellido}"
|
|
225
|
+
|
|
226
|
+
# 1. Base Salary Accounting
|
|
227
|
+
# Always generate lines even if accounts are missing (use NULL for missing accounts)
|
|
228
|
+
salario_base = ne.sueldo_base_historico
|
|
229
|
+
|
|
230
|
+
# Debit: Salary Expense
|
|
231
|
+
orden += 1
|
|
232
|
+
linea_debe = ComprobanteContableLinea(
|
|
233
|
+
comprobante_id=comprobante.id,
|
|
234
|
+
nomina_empleado_id=ne.id,
|
|
235
|
+
empleado_id=empleado.id,
|
|
236
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
237
|
+
empleado_nombre=empleado_nombre_completo,
|
|
238
|
+
codigo_cuenta=planilla.codigo_cuenta_debe_salario, # Can be None if not configured
|
|
239
|
+
descripcion_cuenta=planilla.descripcion_cuenta_debe_salario
|
|
240
|
+
or ("Gasto por Salario" if planilla.codigo_cuenta_debe_salario else None),
|
|
241
|
+
centro_costos=centro_costos,
|
|
242
|
+
tipo_debito_credito="debito",
|
|
243
|
+
debito=salario_base,
|
|
244
|
+
credito=Decimal("0.00"),
|
|
245
|
+
monto_calculado=salario_base,
|
|
246
|
+
concepto="Salario Base",
|
|
247
|
+
tipo_concepto="salario_base",
|
|
248
|
+
concepto_codigo="SALARIO_BASE",
|
|
249
|
+
orden=orden,
|
|
250
|
+
)
|
|
251
|
+
self.session.add(linea_debe)
|
|
252
|
+
total_debitos += salario_base
|
|
253
|
+
|
|
254
|
+
# Credit: Salary Payable
|
|
255
|
+
orden += 1
|
|
256
|
+
linea_haber = ComprobanteContableLinea(
|
|
257
|
+
comprobante_id=comprobante.id,
|
|
258
|
+
nomina_empleado_id=ne.id,
|
|
259
|
+
empleado_id=empleado.id,
|
|
260
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
261
|
+
empleado_nombre=empleado_nombre_completo,
|
|
262
|
+
codigo_cuenta=planilla.codigo_cuenta_haber_salario, # Can be None if not configured
|
|
263
|
+
descripcion_cuenta=planilla.descripcion_cuenta_haber_salario
|
|
264
|
+
or ("Salario por Pagar" if planilla.codigo_cuenta_haber_salario else None),
|
|
265
|
+
centro_costos=centro_costos,
|
|
266
|
+
tipo_debito_credito="credito",
|
|
267
|
+
debito=Decimal("0.00"),
|
|
268
|
+
credito=salario_base,
|
|
269
|
+
monto_calculado=salario_base,
|
|
270
|
+
concepto="Salario Base",
|
|
271
|
+
tipo_concepto="salario_base",
|
|
272
|
+
concepto_codigo="SALARIO_BASE",
|
|
273
|
+
orden=orden,
|
|
274
|
+
)
|
|
275
|
+
self.session.add(linea_haber)
|
|
276
|
+
total_creditos += salario_base
|
|
277
|
+
|
|
278
|
+
# 2. Process Loans and Advances (special treatment)
|
|
279
|
+
# Loans/advances debit salary payable and credit loan control account
|
|
280
|
+
detalles = (
|
|
281
|
+
self.session.execute(
|
|
282
|
+
db.select(NominaDetalle).filter_by(nomina_empleado_id=ne.id).order_by(NominaDetalle.orden)
|
|
283
|
+
)
|
|
284
|
+
.scalars()
|
|
285
|
+
.all()
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
for detalle in detalles:
|
|
289
|
+
# Check if this is a loan/advance deduction
|
|
290
|
+
is_loan_advance = False
|
|
291
|
+
cuenta_control_prestamo = None
|
|
292
|
+
|
|
293
|
+
if detalle.deduccion_id:
|
|
294
|
+
deduccion = self.session.get(Deduccion, detalle.deduccion_id)
|
|
295
|
+
if deduccion:
|
|
296
|
+
# Check if this deduction is associated with loans/advances
|
|
297
|
+
adelantos = (
|
|
298
|
+
self.session.execute(
|
|
299
|
+
db.select(Adelanto).filter_by(empleado_id=empleado.id, deduccion_id=deduccion.id)
|
|
300
|
+
)
|
|
301
|
+
.scalars()
|
|
302
|
+
.all()
|
|
303
|
+
)
|
|
304
|
+
if adelantos:
|
|
305
|
+
is_loan_advance = True
|
|
306
|
+
# Get loan control account from first active loan
|
|
307
|
+
for adelanto in adelantos:
|
|
308
|
+
if adelanto.estado in ("aprobado", "aplicado"):
|
|
309
|
+
cuenta_control_prestamo = adelanto.cuenta_haber
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
if is_loan_advance:
|
|
313
|
+
# Loan/advance: Debit salary payable, Credit loan control
|
|
314
|
+
# Always create both lines even if accounts are NULL
|
|
315
|
+
|
|
316
|
+
# Debit: Salary Payable (same as base salary credit account, can be NULL)
|
|
317
|
+
orden += 1
|
|
318
|
+
linea_debe = ComprobanteContableLinea(
|
|
319
|
+
comprobante_id=comprobante.id,
|
|
320
|
+
nomina_empleado_id=ne.id,
|
|
321
|
+
empleado_id=empleado.id,
|
|
322
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
323
|
+
empleado_nombre=empleado_nombre_completo,
|
|
324
|
+
codigo_cuenta=planilla.codigo_cuenta_haber_salario, # Can be None
|
|
325
|
+
descripcion_cuenta=(
|
|
326
|
+
(planilla.descripcion_cuenta_haber_salario or "Salario por Pagar")
|
|
327
|
+
if planilla.codigo_cuenta_haber_salario
|
|
328
|
+
else None
|
|
329
|
+
),
|
|
330
|
+
centro_costos=centro_costos,
|
|
331
|
+
tipo_debito_credito="debito",
|
|
332
|
+
debito=detalle.monto,
|
|
333
|
+
credito=Decimal("0.00"),
|
|
334
|
+
monto_calculado=detalle.monto,
|
|
335
|
+
concepto=detalle.descripcion or "Préstamo/Adelanto",
|
|
336
|
+
tipo_concepto="prestamo",
|
|
337
|
+
concepto_codigo=detalle.codigo,
|
|
338
|
+
orden=orden,
|
|
339
|
+
)
|
|
340
|
+
self.session.add(linea_debe)
|
|
341
|
+
total_debitos += detalle.monto
|
|
342
|
+
|
|
343
|
+
# Credit: Loan Control Account (can be NULL)
|
|
344
|
+
orden += 1
|
|
345
|
+
linea_haber = ComprobanteContableLinea(
|
|
346
|
+
comprobante_id=comprobante.id,
|
|
347
|
+
nomina_empleado_id=ne.id,
|
|
348
|
+
empleado_id=empleado.id,
|
|
349
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
350
|
+
empleado_nombre=empleado_nombre_completo,
|
|
351
|
+
codigo_cuenta=cuenta_control_prestamo, # Can be None
|
|
352
|
+
descripcion_cuenta="Cuenta de Control Préstamos/Adelantos" if cuenta_control_prestamo else None,
|
|
353
|
+
centro_costos=centro_costos,
|
|
354
|
+
tipo_debito_credito="credito",
|
|
355
|
+
debito=Decimal("0.00"),
|
|
356
|
+
credito=detalle.monto,
|
|
357
|
+
monto_calculado=detalle.monto,
|
|
358
|
+
concepto=detalle.descripcion or "Préstamo/Adelanto",
|
|
359
|
+
tipo_concepto="prestamo",
|
|
360
|
+
concepto_codigo=detalle.codigo,
|
|
361
|
+
orden=orden,
|
|
362
|
+
)
|
|
363
|
+
self.session.add(linea_haber)
|
|
364
|
+
total_creditos += detalle.monto
|
|
365
|
+
|
|
366
|
+
else:
|
|
367
|
+
# Regular concept - use configured accounts (or NULL if missing)
|
|
368
|
+
if detalle.tipo == "ingreso" and detalle.percepcion_id:
|
|
369
|
+
percepcion = self.session.get(Percepcion, detalle.percepcion_id)
|
|
370
|
+
if percepcion and percepcion.contabilizable:
|
|
371
|
+
# Always create debit line (even if account is NULL)
|
|
372
|
+
orden += 1
|
|
373
|
+
linea_debe = ComprobanteContableLinea(
|
|
374
|
+
comprobante_id=comprobante.id,
|
|
375
|
+
nomina_empleado_id=ne.id,
|
|
376
|
+
empleado_id=empleado.id,
|
|
377
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
378
|
+
empleado_nombre=empleado_nombre_completo,
|
|
379
|
+
codigo_cuenta=percepcion.codigo_cuenta_debe, # Can be None
|
|
380
|
+
descripcion_cuenta=(
|
|
381
|
+
(percepcion.descripcion_cuenta_debe or percepcion.nombre)
|
|
382
|
+
if percepcion.codigo_cuenta_debe
|
|
383
|
+
else None
|
|
384
|
+
),
|
|
385
|
+
centro_costos=centro_costos,
|
|
386
|
+
tipo_debito_credito="debito",
|
|
387
|
+
debito=detalle.monto,
|
|
388
|
+
credito=Decimal("0.00"),
|
|
389
|
+
monto_calculado=detalle.monto,
|
|
390
|
+
concepto=detalle.descripcion or percepcion.nombre,
|
|
391
|
+
tipo_concepto="percepcion",
|
|
392
|
+
concepto_codigo=percepcion.codigo,
|
|
393
|
+
orden=orden,
|
|
394
|
+
)
|
|
395
|
+
self.session.add(linea_debe)
|
|
396
|
+
total_debitos += detalle.monto
|
|
397
|
+
|
|
398
|
+
# Always create credit line (even if account is NULL)
|
|
399
|
+
orden += 1
|
|
400
|
+
linea_haber = ComprobanteContableLinea(
|
|
401
|
+
comprobante_id=comprobante.id,
|
|
402
|
+
nomina_empleado_id=ne.id,
|
|
403
|
+
empleado_id=empleado.id,
|
|
404
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
405
|
+
empleado_nombre=empleado_nombre_completo,
|
|
406
|
+
codigo_cuenta=percepcion.codigo_cuenta_haber, # Can be None
|
|
407
|
+
descripcion_cuenta=(
|
|
408
|
+
(percepcion.descripcion_cuenta_haber or percepcion.nombre)
|
|
409
|
+
if percepcion.codigo_cuenta_haber
|
|
410
|
+
else None
|
|
411
|
+
),
|
|
412
|
+
centro_costos=centro_costos,
|
|
413
|
+
tipo_debito_credito="credito",
|
|
414
|
+
debito=Decimal("0.00"),
|
|
415
|
+
credito=detalle.monto,
|
|
416
|
+
monto_calculado=detalle.monto,
|
|
417
|
+
concepto=detalle.descripcion or percepcion.nombre,
|
|
418
|
+
tipo_concepto="percepcion",
|
|
419
|
+
concepto_codigo=percepcion.codigo,
|
|
420
|
+
orden=orden,
|
|
421
|
+
)
|
|
422
|
+
self.session.add(linea_haber)
|
|
423
|
+
total_creditos += detalle.monto
|
|
424
|
+
|
|
425
|
+
elif detalle.tipo == "deduccion" and detalle.deduccion_id:
|
|
426
|
+
deduccion = self.session.get(Deduccion, detalle.deduccion_id)
|
|
427
|
+
if deduccion and deduccion.contabilizable:
|
|
428
|
+
# Always create debit line (even if account is NULL)
|
|
429
|
+
orden += 1
|
|
430
|
+
linea_debe = ComprobanteContableLinea(
|
|
431
|
+
comprobante_id=comprobante.id,
|
|
432
|
+
nomina_empleado_id=ne.id,
|
|
433
|
+
empleado_id=empleado.id,
|
|
434
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
435
|
+
empleado_nombre=empleado_nombre_completo,
|
|
436
|
+
codigo_cuenta=deduccion.codigo_cuenta_debe, # Can be None
|
|
437
|
+
descripcion_cuenta=(
|
|
438
|
+
(deduccion.descripcion_cuenta_debe or deduccion.nombre)
|
|
439
|
+
if deduccion.codigo_cuenta_debe
|
|
440
|
+
else None
|
|
441
|
+
),
|
|
442
|
+
centro_costos=centro_costos,
|
|
443
|
+
tipo_debito_credito="debito",
|
|
444
|
+
debito=detalle.monto,
|
|
445
|
+
credito=Decimal("0.00"),
|
|
446
|
+
monto_calculado=detalle.monto,
|
|
447
|
+
concepto=detalle.descripcion or deduccion.nombre,
|
|
448
|
+
tipo_concepto="deduccion",
|
|
449
|
+
concepto_codigo=deduccion.codigo,
|
|
450
|
+
orden=orden,
|
|
451
|
+
)
|
|
452
|
+
self.session.add(linea_debe)
|
|
453
|
+
total_debitos += detalle.monto
|
|
454
|
+
|
|
455
|
+
# Always create credit line (even if account is NULL)
|
|
456
|
+
orden += 1
|
|
457
|
+
linea_haber = ComprobanteContableLinea(
|
|
458
|
+
comprobante_id=comprobante.id,
|
|
459
|
+
nomina_empleado_id=ne.id,
|
|
460
|
+
empleado_id=empleado.id,
|
|
461
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
462
|
+
empleado_nombre=empleado_nombre_completo,
|
|
463
|
+
codigo_cuenta=deduccion.codigo_cuenta_haber, # Can be None
|
|
464
|
+
descripcion_cuenta=(
|
|
465
|
+
(deduccion.descripcion_cuenta_haber or deduccion.nombre)
|
|
466
|
+
if deduccion.codigo_cuenta_haber
|
|
467
|
+
else None
|
|
468
|
+
),
|
|
469
|
+
centro_costos=centro_costos,
|
|
470
|
+
tipo_debito_credito="credito",
|
|
471
|
+
debito=Decimal("0.00"),
|
|
472
|
+
credito=detalle.monto,
|
|
473
|
+
monto_calculado=detalle.monto,
|
|
474
|
+
concepto=detalle.descripcion or deduccion.nombre,
|
|
475
|
+
tipo_concepto="deduccion",
|
|
476
|
+
concepto_codigo=deduccion.codigo,
|
|
477
|
+
orden=orden,
|
|
478
|
+
)
|
|
479
|
+
self.session.add(linea_haber)
|
|
480
|
+
total_creditos += detalle.monto
|
|
481
|
+
|
|
482
|
+
elif detalle.tipo == "prestacion" and detalle.prestacion_id:
|
|
483
|
+
prestacion = self.session.get(Prestacion, detalle.prestacion_id)
|
|
484
|
+
if prestacion and prestacion.contabilizable:
|
|
485
|
+
# Always create debit line (even if account is NULL)
|
|
486
|
+
orden += 1
|
|
487
|
+
linea_debe = ComprobanteContableLinea(
|
|
488
|
+
comprobante_id=comprobante.id,
|
|
489
|
+
nomina_empleado_id=ne.id,
|
|
490
|
+
empleado_id=empleado.id,
|
|
491
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
492
|
+
empleado_nombre=empleado_nombre_completo,
|
|
493
|
+
codigo_cuenta=prestacion.codigo_cuenta_debe, # Can be None
|
|
494
|
+
descripcion_cuenta=(
|
|
495
|
+
(prestacion.descripcion_cuenta_debe or prestacion.nombre)
|
|
496
|
+
if prestacion.codigo_cuenta_debe
|
|
497
|
+
else None
|
|
498
|
+
),
|
|
499
|
+
centro_costos=centro_costos,
|
|
500
|
+
tipo_debito_credito="debito",
|
|
501
|
+
debito=detalle.monto,
|
|
502
|
+
credito=Decimal("0.00"),
|
|
503
|
+
monto_calculado=detalle.monto,
|
|
504
|
+
concepto=detalle.descripcion or prestacion.nombre,
|
|
505
|
+
tipo_concepto="prestacion",
|
|
506
|
+
concepto_codigo=prestacion.codigo,
|
|
507
|
+
orden=orden,
|
|
508
|
+
)
|
|
509
|
+
self.session.add(linea_debe)
|
|
510
|
+
total_debitos += detalle.monto
|
|
511
|
+
|
|
512
|
+
# Always create credit line (even if account is NULL)
|
|
513
|
+
orden += 1
|
|
514
|
+
linea_haber = ComprobanteContableLinea(
|
|
515
|
+
comprobante_id=comprobante.id,
|
|
516
|
+
nomina_empleado_id=ne.id,
|
|
517
|
+
empleado_id=empleado.id,
|
|
518
|
+
empleado_codigo=empleado.codigo_empleado,
|
|
519
|
+
empleado_nombre=empleado_nombre_completo,
|
|
520
|
+
codigo_cuenta=prestacion.codigo_cuenta_haber, # Can be None
|
|
521
|
+
descripcion_cuenta=(
|
|
522
|
+
(prestacion.descripcion_cuenta_haber or prestacion.nombre)
|
|
523
|
+
if prestacion.codigo_cuenta_haber
|
|
524
|
+
else None
|
|
525
|
+
),
|
|
526
|
+
centro_costos=centro_costos,
|
|
527
|
+
tipo_debito_credito="credito",
|
|
528
|
+
debito=Decimal("0.00"),
|
|
529
|
+
credito=detalle.monto,
|
|
530
|
+
monto_calculado=detalle.monto,
|
|
531
|
+
concepto=detalle.descripcion or prestacion.nombre,
|
|
532
|
+
tipo_concepto="prestacion",
|
|
533
|
+
concepto_codigo=prestacion.codigo,
|
|
534
|
+
orden=orden,
|
|
535
|
+
)
|
|
536
|
+
self.session.add(linea_haber)
|
|
537
|
+
total_creditos += detalle.monto
|
|
538
|
+
|
|
539
|
+
# Calculate balance (should be 0 for balanced voucher)
|
|
540
|
+
balance = total_debitos - total_creditos
|
|
541
|
+
|
|
542
|
+
# Validate balance
|
|
543
|
+
if balance != Decimal("0.00"):
|
|
544
|
+
balance_warning = (
|
|
545
|
+
f"ADVERTENCIA: El comprobante no está balanceado. "
|
|
546
|
+
f"Débitos: {total_debitos}, Créditos: {total_creditos}, "
|
|
547
|
+
f"Diferencia: {abs(balance)}"
|
|
548
|
+
)
|
|
549
|
+
if balance_warning not in warnings:
|
|
550
|
+
warnings.append(balance_warning)
|
|
551
|
+
|
|
552
|
+
# Update comprobante totals
|
|
553
|
+
comprobante.total_debitos = total_debitos
|
|
554
|
+
comprobante.total_creditos = total_creditos
|
|
555
|
+
comprobante.balance = balance
|
|
556
|
+
comprobante.advertencias = warnings
|
|
557
|
+
|
|
558
|
+
return comprobante
|
|
559
|
+
|
|
560
|
+
def summarize_voucher(self, comprobante: ComprobanteContable) -> list[dict[str, Any]]:
|
|
561
|
+
"""Summarize voucher lines by account and cost center with netting.
|
|
562
|
+
|
|
563
|
+
Groups lines by (codigo_cuenta, centro_costos) and nets debits/credits.
|
|
564
|
+
If same account+cost center has both debits and credits, they are netted
|
|
565
|
+
and only one line with the net amount is shown.
|
|
566
|
+
|
|
567
|
+
Lines with NULL accounts are skipped from summarization as they indicate
|
|
568
|
+
incomplete accounting configuration.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
comprobante: The comprobante to summarize
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
List of summarized entries sorted by account code
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
ValueError: If comprobante has NULL accounts (incomplete configuration)
|
|
578
|
+
"""
|
|
579
|
+
# Dictionary to accumulate by (account, cost_center)
|
|
580
|
+
summary_dict: dict[tuple[str, str | None], dict[str, Any]] = defaultdict(
|
|
581
|
+
lambda: {"debito": Decimal("0.00"), "credito": Decimal("0.00"), "descripcion": ""}
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Get all lines
|
|
585
|
+
lineas = (
|
|
586
|
+
self.session.execute(
|
|
587
|
+
db.select(ComprobanteContableLinea)
|
|
588
|
+
.filter_by(comprobante_id=comprobante.id)
|
|
589
|
+
.order_by(ComprobanteContableLinea.orden)
|
|
590
|
+
)
|
|
591
|
+
.scalars()
|
|
592
|
+
.all()
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Check for NULL accounts and raise error if found
|
|
596
|
+
null_account_lines = [linea for linea in lineas if linea.codigo_cuenta is None]
|
|
597
|
+
if null_account_lines:
|
|
598
|
+
raise ValueError(
|
|
599
|
+
"No se puede generar comprobante sumarizado: existen líneas con cuentas contables sin configurar. "
|
|
600
|
+
"Por favor configure todas las cuentas contables o utilice el comprobante de auditoría."
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Accumulate by account + cost center (only lines with valid accounts)
|
|
604
|
+
for linea in lineas:
|
|
605
|
+
if linea.codigo_cuenta is None:
|
|
606
|
+
continue # Skip NULL accounts
|
|
607
|
+
|
|
608
|
+
key = (linea.codigo_cuenta, linea.centro_costos)
|
|
609
|
+
summary_dict[key]["debito"] += linea.debito
|
|
610
|
+
summary_dict[key]["credito"] += linea.credito
|
|
611
|
+
# Use first description found for this account
|
|
612
|
+
if not summary_dict[key]["descripcion"]:
|
|
613
|
+
summary_dict[key]["descripcion"] = linea.descripcion_cuenta or ""
|
|
614
|
+
|
|
615
|
+
# Create summarized entries with netting
|
|
616
|
+
summarized_entries = []
|
|
617
|
+
for (codigo_cuenta, centro_costos), amounts in summary_dict.items():
|
|
618
|
+
debito = amounts["debito"]
|
|
619
|
+
credito = amounts["credito"]
|
|
620
|
+
|
|
621
|
+
# Net debits and credits
|
|
622
|
+
if debito > credito:
|
|
623
|
+
monto_neto = debito - credito
|
|
624
|
+
summarized_entries.append(
|
|
625
|
+
{
|
|
626
|
+
"codigo_cuenta": codigo_cuenta,
|
|
627
|
+
"descripcion": amounts["descripcion"],
|
|
628
|
+
"centro_costos": centro_costos,
|
|
629
|
+
"debito": monto_neto,
|
|
630
|
+
"credito": Decimal("0.00"),
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
elif credito > debito:
|
|
634
|
+
monto_neto = credito - debito
|
|
635
|
+
summarized_entries.append(
|
|
636
|
+
{
|
|
637
|
+
"codigo_cuenta": codigo_cuenta,
|
|
638
|
+
"descripcion": amounts["descripcion"],
|
|
639
|
+
"centro_costos": centro_costos,
|
|
640
|
+
"debito": Decimal("0.00"),
|
|
641
|
+
"credito": monto_neto,
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
# If debito == credito, they cancel out completely, so line is excluded from summary
|
|
645
|
+
# This is intentional: zero-balance entries don't need to appear in the summarized voucher
|
|
646
|
+
|
|
647
|
+
# Sort by account code and cost center
|
|
648
|
+
summarized_entries.sort(key=lambda x: (x["codigo_cuenta"], x["centro_costos"] or ""))
|
|
649
|
+
|
|
650
|
+
return summarized_entries
|
|
651
|
+
|
|
652
|
+
def get_detailed_voucher_by_employee(self, comprobante: ComprobanteContable) -> list[dict[str, Any]]:
|
|
653
|
+
"""Get detailed voucher lines grouped by employee for audit purposes.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
comprobante: The comprobante to get details for
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
List of entries grouped by employee with all their accounting lines
|
|
660
|
+
"""
|
|
661
|
+
detailed_entries = []
|
|
662
|
+
|
|
663
|
+
# Get all lines grouped by employee using the denormalized employee info
|
|
664
|
+
lineas = (
|
|
665
|
+
self.session.execute(
|
|
666
|
+
db.select(ComprobanteContableLinea)
|
|
667
|
+
.filter_by(comprobante_id=comprobante.id)
|
|
668
|
+
.order_by(ComprobanteContableLinea.empleado_codigo, ComprobanteContableLinea.orden)
|
|
669
|
+
)
|
|
670
|
+
.scalars()
|
|
671
|
+
.all()
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Group by employee
|
|
675
|
+
current_empleado_codigo = None
|
|
676
|
+
current_entry = None
|
|
677
|
+
|
|
678
|
+
for linea in lineas:
|
|
679
|
+
if linea.empleado_codigo != current_empleado_codigo:
|
|
680
|
+
# New employee, save previous and start new
|
|
681
|
+
if current_entry:
|
|
682
|
+
detailed_entries.append(current_entry)
|
|
683
|
+
|
|
684
|
+
current_empleado_codigo = linea.empleado_codigo
|
|
685
|
+
current_entry = {
|
|
686
|
+
"empleado_codigo": linea.empleado_codigo,
|
|
687
|
+
"empleado_nombre": linea.empleado_nombre,
|
|
688
|
+
"centro_costos": linea.centro_costos,
|
|
689
|
+
"lineas": [],
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
# Add line to current employee
|
|
693
|
+
current_entry["lineas"].append(
|
|
694
|
+
{
|
|
695
|
+
"concepto": linea.concepto,
|
|
696
|
+
"tipo_concepto": linea.tipo_concepto,
|
|
697
|
+
"codigo_cuenta": linea.codigo_cuenta,
|
|
698
|
+
"descripcion_cuenta": linea.descripcion_cuenta,
|
|
699
|
+
"debito": linea.debito,
|
|
700
|
+
"credito": linea.credito,
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# Don't forget the last employee
|
|
705
|
+
if current_entry:
|
|
706
|
+
detailed_entries.append(current_entry)
|
|
707
|
+
|
|
708
|
+
return detailed_entries
|