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,904 @@
|
|
|
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
|
+
"""Helper functions for audit and governance of payroll concepts."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from coati_payroll.enums import EstadoAprobacion, TipoUsuario, NominaEstado
|
|
22
|
+
from coati_payroll.model import (
|
|
23
|
+
ConceptoAuditLog,
|
|
24
|
+
PlanillaAuditLog,
|
|
25
|
+
NominaAuditLog,
|
|
26
|
+
ReglaCalculoAuditLog,
|
|
27
|
+
Percepcion,
|
|
28
|
+
Deduccion,
|
|
29
|
+
Prestacion,
|
|
30
|
+
Planilla,
|
|
31
|
+
Nomina,
|
|
32
|
+
ReglaCalculo,
|
|
33
|
+
db,
|
|
34
|
+
utc_now,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def puede_aprobar_concepto(usuario_tipo: str) -> bool:
|
|
39
|
+
"""Check if user can approve payroll concepts.
|
|
40
|
+
|
|
41
|
+
Only ADMIN and HHRR users can approve percepciones, deducciones, and prestaciones.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
usuario_tipo: User type (admin, hhrr, audit)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if user can approve concepts, False otherwise
|
|
48
|
+
"""
|
|
49
|
+
return usuario_tipo in [TipoUsuario.ADMIN, TipoUsuario.HHRR]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def crear_log_auditoria(
|
|
53
|
+
concepto: Percepcion | Deduccion | Prestacion,
|
|
54
|
+
accion: str,
|
|
55
|
+
usuario: str,
|
|
56
|
+
descripcion: Optional[str] = None,
|
|
57
|
+
cambios: Optional[Dict[str, Any]] = None,
|
|
58
|
+
estado_anterior: Optional[str] = None,
|
|
59
|
+
estado_nuevo: Optional[str] = None,
|
|
60
|
+
) -> ConceptoAuditLog:
|
|
61
|
+
"""Create an audit log entry for a payroll concept change.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
concepto: The concept that was changed (Percepcion, Deduccion, or Prestacion)
|
|
65
|
+
accion: Action performed (created, updated, approved, rejected, etc.)
|
|
66
|
+
usuario: Username who performed the action
|
|
67
|
+
descripcion: Human-readable description of the change
|
|
68
|
+
cambios: Dictionary of field-level changes {field: {old: value, new: value}}
|
|
69
|
+
estado_anterior: Previous approval status
|
|
70
|
+
estado_nuevo: New approval status
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The created audit log entry
|
|
74
|
+
"""
|
|
75
|
+
# Determine concept type
|
|
76
|
+
if isinstance(concepto, Percepcion):
|
|
77
|
+
tipo_concepto = "percepcion"
|
|
78
|
+
percepcion_id = concepto.id
|
|
79
|
+
deduccion_id = None
|
|
80
|
+
prestacion_id = None
|
|
81
|
+
elif isinstance(concepto, Deduccion):
|
|
82
|
+
tipo_concepto = "deduccion"
|
|
83
|
+
percepcion_id = None
|
|
84
|
+
deduccion_id = concepto.id
|
|
85
|
+
prestacion_id = None
|
|
86
|
+
elif isinstance(concepto, Prestacion):
|
|
87
|
+
tipo_concepto = "prestacion"
|
|
88
|
+
percepcion_id = None
|
|
89
|
+
deduccion_id = None
|
|
90
|
+
prestacion_id = concepto.id
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Invalid concept type: {type(concepto)}")
|
|
93
|
+
|
|
94
|
+
# Create audit log entry
|
|
95
|
+
log = ConceptoAuditLog(
|
|
96
|
+
tipo_concepto=tipo_concepto,
|
|
97
|
+
percepcion_id=percepcion_id,
|
|
98
|
+
deduccion_id=deduccion_id,
|
|
99
|
+
prestacion_id=prestacion_id,
|
|
100
|
+
accion=accion,
|
|
101
|
+
usuario=usuario,
|
|
102
|
+
descripcion=descripcion,
|
|
103
|
+
cambios=cambios or {},
|
|
104
|
+
estado_anterior=estado_anterior,
|
|
105
|
+
estado_nuevo=estado_nuevo,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
db.session.add(log)
|
|
109
|
+
return log
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def generar_descripcion_cambios(cambios: Dict[str, Any]) -> str:
|
|
113
|
+
"""Generate a human-readable description of changes.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
cambios: Dictionary of field-level changes
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Human-readable description
|
|
120
|
+
"""
|
|
121
|
+
if not cambios:
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
descripciones = []
|
|
125
|
+
for campo, valores in cambios.items():
|
|
126
|
+
old_val = valores.get("old", "")
|
|
127
|
+
new_val = valores.get("new", "")
|
|
128
|
+
|
|
129
|
+
# Format field name
|
|
130
|
+
campo_legible = campo.replace("_", " ").title()
|
|
131
|
+
|
|
132
|
+
if old_val == "" or old_val is None:
|
|
133
|
+
descripciones.append(f"{campo_legible} establecido a {new_val}")
|
|
134
|
+
elif new_val == "" or new_val is None:
|
|
135
|
+
descripciones.append(f"{campo_legible} eliminado (era {old_val})")
|
|
136
|
+
else:
|
|
137
|
+
descripciones.append(f"{campo_legible} cambió de {old_val} a {new_val}")
|
|
138
|
+
|
|
139
|
+
return "; ".join(descripciones)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def aprobar_concepto(
|
|
143
|
+
concepto: Percepcion | Deduccion | Prestacion,
|
|
144
|
+
usuario: str,
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""Approve a payroll concept.
|
|
147
|
+
|
|
148
|
+
Changes status from 'borrador' to 'aprobado' and records approval information.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
concepto: The concept to approve
|
|
152
|
+
usuario: Username who is approving
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True if approved successfully, False if already approved or invalid
|
|
156
|
+
"""
|
|
157
|
+
if concepto.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
estado_anterior = concepto.estado_aprobacion
|
|
161
|
+
concepto.estado_aprobacion = EstadoAprobacion.APROBADO
|
|
162
|
+
concepto.aprobado_por = usuario
|
|
163
|
+
concepto.aprobado_en = utc_now()
|
|
164
|
+
|
|
165
|
+
# Create audit log
|
|
166
|
+
tipo_concepto = type(concepto).__name__.lower()
|
|
167
|
+
crear_log_auditoria(
|
|
168
|
+
concepto=concepto,
|
|
169
|
+
accion="approved",
|
|
170
|
+
usuario=usuario,
|
|
171
|
+
descripcion=f"Aprobó {tipo_concepto} '{concepto.nombre}' (código: {concepto.codigo})",
|
|
172
|
+
estado_anterior=estado_anterior,
|
|
173
|
+
estado_nuevo=EstadoAprobacion.APROBADO,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def rechazar_concepto(
|
|
180
|
+
concepto: Percepcion | Deduccion | Prestacion,
|
|
181
|
+
usuario: str,
|
|
182
|
+
razon: Optional[str] = None,
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Reject a payroll concept (keep as draft).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
concepto: The concept to reject
|
|
188
|
+
usuario: Username who is rejecting
|
|
189
|
+
razon: Reason for rejection
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if rejected successfully
|
|
193
|
+
"""
|
|
194
|
+
estado_anterior = concepto.estado_aprobacion
|
|
195
|
+
concepto.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
196
|
+
concepto.aprobado_por = None
|
|
197
|
+
concepto.aprobado_en = None
|
|
198
|
+
|
|
199
|
+
# Create audit log
|
|
200
|
+
tipo_concepto = type(concepto).__name__.lower()
|
|
201
|
+
descripcion = f"Rechazó {tipo_concepto} '{concepto.nombre}' (código: {concepto.codigo})"
|
|
202
|
+
if razon:
|
|
203
|
+
descripcion += f" - Razón: {razon}"
|
|
204
|
+
|
|
205
|
+
crear_log_auditoria(
|
|
206
|
+
concepto=concepto,
|
|
207
|
+
accion="rejected",
|
|
208
|
+
usuario=usuario,
|
|
209
|
+
descripcion=descripcion,
|
|
210
|
+
estado_anterior=estado_anterior,
|
|
211
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def marcar_como_borrador_si_editado(
|
|
218
|
+
concepto: Percepcion | Deduccion | Prestacion,
|
|
219
|
+
usuario: str,
|
|
220
|
+
cambios: Dict[str, Any],
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Mark concept as draft if it was edited while approved.
|
|
223
|
+
|
|
224
|
+
When an approved concept is edited, it must return to draft status
|
|
225
|
+
unless it was created by a plugin.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
concepto: The concept that was edited
|
|
229
|
+
usuario: Username who edited
|
|
230
|
+
cambios: Dictionary of changes made
|
|
231
|
+
"""
|
|
232
|
+
# Don't change status if created by plugin
|
|
233
|
+
if concepto.creado_por_plugin:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# If currently approved, mark as draft
|
|
237
|
+
if concepto.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
238
|
+
estado_anterior = concepto.estado_aprobacion
|
|
239
|
+
concepto.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
240
|
+
concepto.aprobado_por = None
|
|
241
|
+
concepto.aprobado_en = None
|
|
242
|
+
|
|
243
|
+
# Create audit log
|
|
244
|
+
tipo_concepto = type(concepto).__name__.lower()
|
|
245
|
+
descripcion_cambios = generar_descripcion_cambios(cambios)
|
|
246
|
+
|
|
247
|
+
crear_log_auditoria(
|
|
248
|
+
concepto=concepto,
|
|
249
|
+
accion="updated",
|
|
250
|
+
usuario=usuario,
|
|
251
|
+
descripcion=f"Editó {tipo_concepto} '{concepto.nombre}' - {descripcion_cambios}."
|
|
252
|
+
+ " Estado cambiado a borrador.",
|
|
253
|
+
cambios=cambios,
|
|
254
|
+
estado_anterior=estado_anterior,
|
|
255
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def detectar_cambios(concepto_original: Dict[str, Any], concepto_nuevo: Dict[str, Any]) -> Dict[str, Any]:
|
|
260
|
+
"""Detect changes between original and new concept data.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
concepto_original: Original concept data
|
|
264
|
+
concepto_nuevo: New concept data
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary of changes {field: {old: value, new: value}}
|
|
268
|
+
"""
|
|
269
|
+
cambios = {}
|
|
270
|
+
|
|
271
|
+
# Fields to track
|
|
272
|
+
campos_importantes = [
|
|
273
|
+
"nombre",
|
|
274
|
+
"descripcion",
|
|
275
|
+
"codigo",
|
|
276
|
+
"formula_tipo",
|
|
277
|
+
"monto_default",
|
|
278
|
+
"porcentaje",
|
|
279
|
+
"base_calculo",
|
|
280
|
+
"gravable",
|
|
281
|
+
"recurrente",
|
|
282
|
+
"activo",
|
|
283
|
+
"codigo_cuenta_debe",
|
|
284
|
+
"codigo_cuenta_haber",
|
|
285
|
+
"tipo",
|
|
286
|
+
"es_impuesto",
|
|
287
|
+
"antes_impuesto",
|
|
288
|
+
"tipo_acumulacion",
|
|
289
|
+
"tope_aplicacion",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
for campo in campos_importantes:
|
|
293
|
+
if campo in concepto_original and campo in concepto_nuevo:
|
|
294
|
+
old_val = concepto_original[campo]
|
|
295
|
+
new_val = concepto_nuevo[campo]
|
|
296
|
+
|
|
297
|
+
# Compare values (handle None and empty strings as equivalent)
|
|
298
|
+
if (old_val or "") != (new_val or ""):
|
|
299
|
+
cambios[campo] = {"old": old_val, "new": new_val}
|
|
300
|
+
|
|
301
|
+
return cambios
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def obtener_conceptos_en_borrador(planilla_id: str) -> Dict[str, list]:
|
|
305
|
+
"""Get all draft concepts associated with a planilla.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
planilla_id: ID of the planilla
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dictionary with lists of draft percepciones, deducciones, and prestaciones
|
|
312
|
+
"""
|
|
313
|
+
from coati_payroll.model import PlanillaIngreso, PlanillaDeduccion, PlanillaPrestacion
|
|
314
|
+
|
|
315
|
+
# Get draft percepciones
|
|
316
|
+
percepciones_borrador = (
|
|
317
|
+
db.session.query(Percepcion)
|
|
318
|
+
.join(PlanillaIngreso)
|
|
319
|
+
.filter(
|
|
320
|
+
PlanillaIngreso.planilla_id == planilla_id,
|
|
321
|
+
Percepcion.estado_aprobacion == EstadoAprobacion.BORRADOR,
|
|
322
|
+
Percepcion.activo == True, # noqa: E712
|
|
323
|
+
)
|
|
324
|
+
.all()
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Get draft deducciones
|
|
328
|
+
deducciones_borrador = (
|
|
329
|
+
db.session.query(Deduccion)
|
|
330
|
+
.join(PlanillaDeduccion)
|
|
331
|
+
.filter(
|
|
332
|
+
PlanillaDeduccion.planilla_id == planilla_id,
|
|
333
|
+
Deduccion.estado_aprobacion == EstadoAprobacion.BORRADOR,
|
|
334
|
+
Deduccion.activo == True, # noqa: E712
|
|
335
|
+
)
|
|
336
|
+
.all()
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Get draft prestaciones
|
|
340
|
+
prestaciones_borrador = (
|
|
341
|
+
db.session.query(Prestacion)
|
|
342
|
+
.join(PlanillaPrestacion)
|
|
343
|
+
.filter(
|
|
344
|
+
PlanillaPrestacion.planilla_id == planilla_id,
|
|
345
|
+
Prestacion.estado_aprobacion == EstadoAprobacion.BORRADOR,
|
|
346
|
+
Prestacion.activo == True, # noqa: E712
|
|
347
|
+
)
|
|
348
|
+
.all()
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"percepciones": percepciones_borrador,
|
|
353
|
+
"deducciones": deducciones_borrador,
|
|
354
|
+
"prestaciones": prestaciones_borrador,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def tiene_conceptos_en_borrador(planilla_id: str) -> bool:
|
|
359
|
+
"""Check if a planilla has any draft concepts.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
planilla_id: ID of the planilla
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
True if there are any draft concepts, False otherwise
|
|
366
|
+
"""
|
|
367
|
+
conceptos = obtener_conceptos_en_borrador(planilla_id)
|
|
368
|
+
return bool(conceptos["percepciones"] or conceptos["deducciones"] or conceptos["prestaciones"])
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ============================================================================
|
|
372
|
+
# PLANILLA AUDIT FUNCTIONS
|
|
373
|
+
# ============================================================================
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def crear_log_auditoria_planilla(
|
|
377
|
+
planilla: Planilla,
|
|
378
|
+
accion: str,
|
|
379
|
+
usuario: str,
|
|
380
|
+
descripcion: Optional[str] = None,
|
|
381
|
+
cambios: Optional[Dict[str, Any]] = None,
|
|
382
|
+
estado_anterior: Optional[str] = None,
|
|
383
|
+
estado_nuevo: Optional[str] = None,
|
|
384
|
+
) -> PlanillaAuditLog:
|
|
385
|
+
"""Create an audit log entry for a planilla change.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
planilla: The planilla that was changed
|
|
389
|
+
accion: Action performed (created, updated, approved, rejected, etc.)
|
|
390
|
+
usuario: Username who performed the action
|
|
391
|
+
descripcion: Human-readable description of the change
|
|
392
|
+
cambios: Dictionary of field-level changes
|
|
393
|
+
estado_anterior: Previous approval status
|
|
394
|
+
estado_nuevo: New approval status
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
The created audit log entry
|
|
398
|
+
"""
|
|
399
|
+
log = PlanillaAuditLog(
|
|
400
|
+
planilla_id=planilla.id,
|
|
401
|
+
accion=accion,
|
|
402
|
+
usuario=usuario,
|
|
403
|
+
descripcion=descripcion,
|
|
404
|
+
cambios=cambios or {},
|
|
405
|
+
estado_anterior=estado_anterior,
|
|
406
|
+
estado_nuevo=estado_nuevo,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
db.session.add(log)
|
|
410
|
+
return log
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def aprobar_planilla(planilla: Planilla, usuario: str) -> bool:
|
|
414
|
+
"""Approve a planilla.
|
|
415
|
+
|
|
416
|
+
Changes status from 'borrador' to 'aprobado' and records approval information.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
planilla: The planilla to approve
|
|
420
|
+
usuario: Username who is approving
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
True if approved successfully, False if already approved or invalid
|
|
424
|
+
"""
|
|
425
|
+
if planilla.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
estado_anterior = planilla.estado_aprobacion
|
|
429
|
+
planilla.estado_aprobacion = EstadoAprobacion.APROBADO
|
|
430
|
+
planilla.aprobado_por = usuario
|
|
431
|
+
planilla.aprobado_en = utc_now()
|
|
432
|
+
|
|
433
|
+
# Create audit log
|
|
434
|
+
crear_log_auditoria_planilla(
|
|
435
|
+
planilla=planilla,
|
|
436
|
+
accion="approved",
|
|
437
|
+
usuario=usuario,
|
|
438
|
+
descripcion=f"Aprobó planilla '{planilla.nombre}'",
|
|
439
|
+
estado_anterior=estado_anterior,
|
|
440
|
+
estado_nuevo=EstadoAprobacion.APROBADO,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def rechazar_planilla(planilla: Planilla, usuario: str, razon: Optional[str] = None) -> bool:
|
|
447
|
+
"""Reject a planilla (keep as draft).
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
planilla: The planilla to reject
|
|
451
|
+
usuario: Username who is rejecting
|
|
452
|
+
razon: Reason for rejection
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
True if rejected successfully
|
|
456
|
+
"""
|
|
457
|
+
estado_anterior = planilla.estado_aprobacion
|
|
458
|
+
planilla.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
459
|
+
planilla.aprobado_por = None
|
|
460
|
+
planilla.aprobado_en = None
|
|
461
|
+
|
|
462
|
+
# Create audit log
|
|
463
|
+
descripcion = f"Rechazó planilla '{planilla.nombre}'"
|
|
464
|
+
if razon:
|
|
465
|
+
descripcion += f" - Razón: {razon}"
|
|
466
|
+
|
|
467
|
+
crear_log_auditoria_planilla(
|
|
468
|
+
planilla=planilla,
|
|
469
|
+
accion="rejected",
|
|
470
|
+
usuario=usuario,
|
|
471
|
+
descripcion=descripcion,
|
|
472
|
+
estado_anterior=estado_anterior,
|
|
473
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def marcar_planilla_como_borrador_si_editada(
|
|
480
|
+
planilla: Planilla,
|
|
481
|
+
usuario: str,
|
|
482
|
+
cambios: Dict[str, Any],
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Mark planilla as draft if it was edited while approved.
|
|
485
|
+
|
|
486
|
+
When an approved planilla is edited, it must return to draft status
|
|
487
|
+
unless it was created by a plugin.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
planilla: The planilla that was edited
|
|
491
|
+
usuario: Username who edited
|
|
492
|
+
cambios: Dictionary of changes made
|
|
493
|
+
"""
|
|
494
|
+
# Don't change status if created by plugin
|
|
495
|
+
if planilla.creado_por_plugin:
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
# If currently approved, mark as draft
|
|
499
|
+
if planilla.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
500
|
+
estado_anterior = planilla.estado_aprobacion
|
|
501
|
+
planilla.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
502
|
+
planilla.aprobado_por = None
|
|
503
|
+
planilla.aprobado_en = None
|
|
504
|
+
|
|
505
|
+
# Create audit log
|
|
506
|
+
descripcion_cambios = generar_descripcion_cambios(cambios)
|
|
507
|
+
|
|
508
|
+
crear_log_auditoria_planilla(
|
|
509
|
+
planilla=planilla,
|
|
510
|
+
accion="updated",
|
|
511
|
+
usuario=usuario,
|
|
512
|
+
descripcion=f"Editó planilla '{planilla.nombre}' - {descripcion_cambios}. Estado cambiado a borrador.",
|
|
513
|
+
cambios=cambios,
|
|
514
|
+
estado_anterior=estado_anterior,
|
|
515
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ============================================================================
|
|
520
|
+
# NOMINA AUDIT FUNCTIONS
|
|
521
|
+
# ============================================================================
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def crear_log_auditoria_nomina(
|
|
525
|
+
nomina: Nomina,
|
|
526
|
+
accion: str,
|
|
527
|
+
usuario: str,
|
|
528
|
+
descripcion: Optional[str] = None,
|
|
529
|
+
cambios: Optional[Dict[str, Any]] = None,
|
|
530
|
+
estado_anterior: Optional[str] = None,
|
|
531
|
+
estado_nuevo: Optional[str] = None,
|
|
532
|
+
) -> NominaAuditLog:
|
|
533
|
+
"""Create an audit log entry for a nomina state change.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
nomina: The nomina that changed state
|
|
537
|
+
accion: Action performed (generated, approved, applied, cancelled, etc.)
|
|
538
|
+
usuario: Username who performed the action
|
|
539
|
+
descripcion: Human-readable description of the change
|
|
540
|
+
cambios: Dictionary of field-level changes
|
|
541
|
+
estado_anterior: Previous state
|
|
542
|
+
estado_nuevo: New state
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
The created audit log entry
|
|
546
|
+
"""
|
|
547
|
+
log = NominaAuditLog(
|
|
548
|
+
nomina_id=nomina.id,
|
|
549
|
+
accion=accion,
|
|
550
|
+
usuario=usuario,
|
|
551
|
+
descripcion=descripcion,
|
|
552
|
+
cambios=cambios or {},
|
|
553
|
+
estado_anterior=estado_anterior,
|
|
554
|
+
estado_nuevo=estado_nuevo,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
db.session.add(log)
|
|
558
|
+
return log
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def aprobar_nomina(nomina: Nomina, usuario: str) -> bool:
|
|
562
|
+
"""Approve a nomina.
|
|
563
|
+
|
|
564
|
+
Changes state from 'generado' to 'aprobado' and records approval information.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
nomina: The nomina to approve
|
|
568
|
+
usuario: Username who is approving
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
True if approved successfully, False if already approved or invalid state
|
|
572
|
+
"""
|
|
573
|
+
if nomina.estado != NominaEstado.GENERADO:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
estado_anterior = nomina.estado
|
|
577
|
+
nomina.estado = NominaEstado.APROBADO
|
|
578
|
+
nomina.aprobado_por = usuario
|
|
579
|
+
nomina.aprobado_en = utc_now()
|
|
580
|
+
|
|
581
|
+
# Create audit log
|
|
582
|
+
crear_log_auditoria_nomina(
|
|
583
|
+
nomina=nomina,
|
|
584
|
+
accion="approved",
|
|
585
|
+
usuario=usuario,
|
|
586
|
+
descripcion=f"Aprobó nómina del período {nomina.periodo_inicio} al {nomina.periodo_fin}",
|
|
587
|
+
estado_anterior=estado_anterior,
|
|
588
|
+
estado_nuevo=NominaEstado.APROBADO,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return True
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def aplicar_nomina(nomina: Nomina, usuario: str) -> bool:
|
|
595
|
+
"""Apply a nomina (mark as paid/executed).
|
|
596
|
+
|
|
597
|
+
Changes state from 'aprobado' to 'aplicado' and records application information.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
nomina: The nomina to apply
|
|
601
|
+
usuario: Username who is applying
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
True if applied successfully, False if not in approved state
|
|
605
|
+
"""
|
|
606
|
+
if nomina.estado != NominaEstado.APROBADO:
|
|
607
|
+
return False
|
|
608
|
+
|
|
609
|
+
estado_anterior = nomina.estado
|
|
610
|
+
nomina.estado = NominaEstado.APLICADO
|
|
611
|
+
nomina.aplicado_por = usuario
|
|
612
|
+
nomina.aplicado_en = utc_now()
|
|
613
|
+
|
|
614
|
+
# Create audit log
|
|
615
|
+
crear_log_auditoria_nomina(
|
|
616
|
+
nomina=nomina,
|
|
617
|
+
accion="applied",
|
|
618
|
+
usuario=usuario,
|
|
619
|
+
descripcion=f"Aplicó nómina del período {nomina.periodo_inicio} al {nomina.periodo_fin}",
|
|
620
|
+
estado_anterior=estado_anterior,
|
|
621
|
+
estado_nuevo=NominaEstado.APLICADO,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return True
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def anular_nomina(nomina: Nomina, usuario: str, razon: str) -> bool:
|
|
628
|
+
"""Cancel/void a nomina.
|
|
629
|
+
|
|
630
|
+
Changes state to 'anulado' and records cancellation information.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
nomina: The nomina to cancel
|
|
634
|
+
usuario: Username who is cancelling
|
|
635
|
+
razon: Reason for cancellation
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
True if cancelled successfully, False if already cancelled
|
|
639
|
+
"""
|
|
640
|
+
if nomina.estado == NominaEstado.ANULADO:
|
|
641
|
+
return False
|
|
642
|
+
|
|
643
|
+
estado_anterior = nomina.estado
|
|
644
|
+
nomina.estado = NominaEstado.ANULADO
|
|
645
|
+
nomina.anulado_por = usuario
|
|
646
|
+
nomina.anulado_en = utc_now()
|
|
647
|
+
nomina.razon_anulacion = razon
|
|
648
|
+
|
|
649
|
+
# Create audit log
|
|
650
|
+
crear_log_auditoria_nomina(
|
|
651
|
+
nomina=nomina,
|
|
652
|
+
accion="cancelled",
|
|
653
|
+
usuario=usuario,
|
|
654
|
+
descripcion=f"Anuló nómina del período {nomina.periodo_inicio} al {nomina.periodo_fin} - Razón: {razon}",
|
|
655
|
+
estado_anterior=estado_anterior,
|
|
656
|
+
estado_nuevo=NominaEstado.ANULADO,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return True
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# ============================================================================
|
|
663
|
+
# REGLA CALCULO AUDIT FUNCTIONS
|
|
664
|
+
# ============================================================================
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def crear_log_auditoria_regla_calculo(
|
|
668
|
+
regla_calculo: ReglaCalculo,
|
|
669
|
+
accion: str,
|
|
670
|
+
usuario: str,
|
|
671
|
+
descripcion: Optional[str] = None,
|
|
672
|
+
cambios: Optional[Dict[str, Any]] = None,
|
|
673
|
+
estado_anterior: Optional[str] = None,
|
|
674
|
+
estado_nuevo: Optional[str] = None,
|
|
675
|
+
) -> ReglaCalculoAuditLog:
|
|
676
|
+
"""Create an audit log entry for a calculation rule change.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
regla_calculo: The calculation rule that was changed
|
|
680
|
+
accion: Action performed (created, updated, approved, rejected, etc.)
|
|
681
|
+
usuario: Username who performed the action
|
|
682
|
+
descripcion: Human-readable description of the change
|
|
683
|
+
cambios: Dictionary of field-level changes
|
|
684
|
+
estado_anterior: Previous approval status
|
|
685
|
+
estado_nuevo: New approval status
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
The created audit log entry
|
|
689
|
+
"""
|
|
690
|
+
log = ReglaCalculoAuditLog(
|
|
691
|
+
regla_calculo_id=regla_calculo.id,
|
|
692
|
+
accion=accion,
|
|
693
|
+
usuario=usuario,
|
|
694
|
+
descripcion=descripcion,
|
|
695
|
+
cambios=cambios or {},
|
|
696
|
+
estado_anterior=estado_anterior,
|
|
697
|
+
estado_nuevo=estado_nuevo,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
db.session.add(log)
|
|
701
|
+
return log
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def aprobar_regla_calculo(regla_calculo: ReglaCalculo, usuario: str) -> bool:
|
|
705
|
+
"""Approve a calculation rule.
|
|
706
|
+
|
|
707
|
+
Changes status from 'borrador' to 'aprobado' and records approval information.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
regla_calculo: The calculation rule to approve
|
|
711
|
+
usuario: Username who is approving
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
True if approved successfully, False if already approved or invalid
|
|
715
|
+
"""
|
|
716
|
+
if regla_calculo.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
717
|
+
return False
|
|
718
|
+
|
|
719
|
+
estado_anterior = regla_calculo.estado_aprobacion
|
|
720
|
+
regla_calculo.estado_aprobacion = EstadoAprobacion.APROBADO
|
|
721
|
+
regla_calculo.aprobado_por = usuario
|
|
722
|
+
regla_calculo.aprobado_en = utc_now()
|
|
723
|
+
|
|
724
|
+
# Create audit log
|
|
725
|
+
crear_log_auditoria_regla_calculo(
|
|
726
|
+
regla_calculo=regla_calculo,
|
|
727
|
+
accion="approved",
|
|
728
|
+
usuario=usuario,
|
|
729
|
+
descripcion=(
|
|
730
|
+
"Aprobó regla de cálculo "
|
|
731
|
+
+ f"'{regla_calculo.nombre}' (código: {regla_calculo.codigo}, "
|
|
732
|
+
+ f"versión: {regla_calculo.version})"
|
|
733
|
+
),
|
|
734
|
+
estado_anterior=estado_anterior,
|
|
735
|
+
estado_nuevo=EstadoAprobacion.APROBADO,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return True
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def rechazar_regla_calculo(
|
|
742
|
+
regla_calculo: ReglaCalculo,
|
|
743
|
+
usuario: str,
|
|
744
|
+
razon: Optional[str] = None,
|
|
745
|
+
) -> bool:
|
|
746
|
+
"""Reject a calculation rule (keep as draft).
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
regla_calculo: The calculation rule to reject
|
|
750
|
+
usuario: Username who is rejecting
|
|
751
|
+
razon: Reason for rejection
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
True if rejected successfully
|
|
755
|
+
"""
|
|
756
|
+
estado_anterior = regla_calculo.estado_aprobacion
|
|
757
|
+
regla_calculo.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
758
|
+
regla_calculo.aprobado_por = None
|
|
759
|
+
regla_calculo.aprobado_en = None
|
|
760
|
+
|
|
761
|
+
# Create audit log
|
|
762
|
+
descripcion = (
|
|
763
|
+
f"Rechazó regla de cálculo '{regla_calculo.nombre}' "
|
|
764
|
+
+ f"(código: {regla_calculo.codigo}, versión: {regla_calculo.version})"
|
|
765
|
+
)
|
|
766
|
+
if razon:
|
|
767
|
+
descripcion += f" - Razón: {razon}"
|
|
768
|
+
|
|
769
|
+
crear_log_auditoria_regla_calculo(
|
|
770
|
+
regla_calculo=regla_calculo,
|
|
771
|
+
accion="rejected",
|
|
772
|
+
usuario=usuario,
|
|
773
|
+
descripcion=descripcion,
|
|
774
|
+
estado_anterior=estado_anterior,
|
|
775
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
return True
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def marcar_regla_calculo_como_borrador_si_editada(
|
|
782
|
+
regla_calculo: ReglaCalculo,
|
|
783
|
+
usuario: str,
|
|
784
|
+
cambios: Dict[str, Any],
|
|
785
|
+
) -> None:
|
|
786
|
+
"""Mark calculation rule as draft if it was edited while approved.
|
|
787
|
+
|
|
788
|
+
When an approved calculation rule is edited, it must return to draft status
|
|
789
|
+
unless it was created by a plugin.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
regla_calculo: The calculation rule that was edited
|
|
793
|
+
usuario: Username who edited
|
|
794
|
+
cambios: Dictionary of changes made
|
|
795
|
+
"""
|
|
796
|
+
# Don't change status if created by plugin
|
|
797
|
+
if regla_calculo.creado_por_plugin:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
# If currently approved, mark as draft
|
|
801
|
+
if regla_calculo.estado_aprobacion == EstadoAprobacion.APROBADO:
|
|
802
|
+
estado_anterior = regla_calculo.estado_aprobacion
|
|
803
|
+
regla_calculo.estado_aprobacion = EstadoAprobacion.BORRADOR
|
|
804
|
+
regla_calculo.aprobado_por = None
|
|
805
|
+
regla_calculo.aprobado_en = None
|
|
806
|
+
|
|
807
|
+
# Create audit log
|
|
808
|
+
descripcion_cambios = generar_descripcion_cambios(cambios)
|
|
809
|
+
|
|
810
|
+
crear_log_auditoria_regla_calculo(
|
|
811
|
+
regla_calculo=regla_calculo,
|
|
812
|
+
accion="updated",
|
|
813
|
+
usuario=usuario,
|
|
814
|
+
descripcion=(
|
|
815
|
+
f"Editó regla de cálculo '{regla_calculo.nombre}' - "
|
|
816
|
+
+ f"{descripcion_cambios}. Estado cambiado a borrador."
|
|
817
|
+
),
|
|
818
|
+
cambios=cambios,
|
|
819
|
+
estado_anterior=estado_anterior,
|
|
820
|
+
estado_nuevo=EstadoAprobacion.BORRADOR,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def obtener_reglas_calculo_en_borrador(planilla_id: str) -> list:
|
|
825
|
+
"""Get all draft calculation rules associated with a planilla.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
planilla_id: ID of the planilla
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
List of draft calculation rules
|
|
832
|
+
"""
|
|
833
|
+
from coati_payroll.model import PlanillaReglaCalculo
|
|
834
|
+
|
|
835
|
+
reglas_borrador = (
|
|
836
|
+
db.session.query(ReglaCalculo)
|
|
837
|
+
.join(PlanillaReglaCalculo)
|
|
838
|
+
.filter(
|
|
839
|
+
PlanillaReglaCalculo.planilla_id == planilla_id,
|
|
840
|
+
ReglaCalculo.estado_aprobacion == EstadoAprobacion.BORRADOR,
|
|
841
|
+
ReglaCalculo.activo == True, # noqa: E712
|
|
842
|
+
)
|
|
843
|
+
.all()
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
return reglas_borrador
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def validar_configuracion_nomina(planilla_id: str) -> Dict[str, Any]:
|
|
850
|
+
"""Validate payroll configuration before execution.
|
|
851
|
+
|
|
852
|
+
Checks for draft concepts and calculation rules that may affect payroll accuracy.
|
|
853
|
+
Returns warnings but does not prevent execution (allows test runs).
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
planilla_id: ID of the planilla
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Dictionary with validation results:
|
|
860
|
+
{
|
|
861
|
+
"tiene_advertencias": bool,
|
|
862
|
+
"advertencias": list of warning messages,
|
|
863
|
+
"conceptos_borrador": dict with draft concepts,
|
|
864
|
+
"reglas_borrador": list of draft calculation rules
|
|
865
|
+
}
|
|
866
|
+
"""
|
|
867
|
+
advertencias = []
|
|
868
|
+
|
|
869
|
+
# Check for draft concepts
|
|
870
|
+
conceptos_borrador = obtener_conceptos_en_borrador(planilla_id)
|
|
871
|
+
|
|
872
|
+
if conceptos_borrador["percepciones"]:
|
|
873
|
+
percepciones_nombres = [p.nombre for p in conceptos_borrador["percepciones"]]
|
|
874
|
+
advertencias.append(
|
|
875
|
+
f"⚠️ Hay {len(percepciones_nombres)} percepción(es) en estado BORRADOR: {', '.join(percepciones_nombres)}"
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
if conceptos_borrador["deducciones"]:
|
|
879
|
+
deducciones_nombres = [d.nombre for d in conceptos_borrador["deducciones"]]
|
|
880
|
+
advertencias.append(
|
|
881
|
+
f"⚠️ Hay {len(deducciones_nombres)} deducción(es) en estado BORRADOR: {', '.join(deducciones_nombres)}"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
if conceptos_borrador["prestaciones"]:
|
|
885
|
+
prestaciones_nombres = [p.nombre for p in conceptos_borrador["prestaciones"]]
|
|
886
|
+
advertencias.append(
|
|
887
|
+
f"⚠️ Hay {len(prestaciones_nombres)} prestación(es) en estado BORRADOR: {', '.join(prestaciones_nombres)}"
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
# Check for draft calculation rules
|
|
891
|
+
reglas_borrador = obtener_reglas_calculo_en_borrador(planilla_id)
|
|
892
|
+
|
|
893
|
+
if reglas_borrador:
|
|
894
|
+
reglas_nombres = [f"{r.nombre} (v{r.version})" for r in reglas_borrador]
|
|
895
|
+
advertencias.append(
|
|
896
|
+
f"⚠️ Hay {len(reglas_nombres)} regla(s) de cálculo en estado BORRADOR: {', '.join(reglas_nombres)}"
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
"tiene_advertencias": bool(advertencias),
|
|
901
|
+
"advertencias": advertencias,
|
|
902
|
+
"conceptos_borrador": conceptos_borrador,
|
|
903
|
+
"reglas_borrador": reglas_borrador,
|
|
904
|
+
}
|