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,233 @@
|
|
|
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 nomina business logic."""
|
|
15
|
+
|
|
16
|
+
from datetime import date, timedelta
|
|
17
|
+
from flask import current_app
|
|
18
|
+
from coati_payroll.model import db, Planilla, Nomina
|
|
19
|
+
from coati_payroll.enums import NominaEstado
|
|
20
|
+
from coati_payroll.nomina_engine import NominaEngine
|
|
21
|
+
from coati_payroll.queue import get_queue_driver
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NominaService:
|
|
25
|
+
"""Service for nomina operations."""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def calcular_periodo_sugerido(planilla: Planilla) -> tuple[date, date]:
|
|
29
|
+
"""Calculate suggested period dates for a new nomina.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
planilla: The planilla to calculate period for
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of (periodo_inicio, periodo_fin)
|
|
36
|
+
"""
|
|
37
|
+
# Get last nomina for default dates
|
|
38
|
+
ultima_nomina = db.session.execute(
|
|
39
|
+
db.select(Nomina).filter_by(planilla_id=planilla.id).order_by(Nomina.periodo_fin.desc())
|
|
40
|
+
).scalar_one_or_none()
|
|
41
|
+
|
|
42
|
+
hoy = date.today()
|
|
43
|
+
|
|
44
|
+
if ultima_nomina:
|
|
45
|
+
# Start from the day after last period ended
|
|
46
|
+
periodo_inicio_sugerido = ultima_nomina.periodo_fin + timedelta(days=1)
|
|
47
|
+
else:
|
|
48
|
+
# First day of current month
|
|
49
|
+
periodo_inicio_sugerido = hoy.replace(day=1)
|
|
50
|
+
|
|
51
|
+
# Calculate end of period based on tipo_planilla
|
|
52
|
+
tipo = planilla.tipo_planilla
|
|
53
|
+
match tipo.periodicidad if tipo else "mensual":
|
|
54
|
+
case "semanal":
|
|
55
|
+
periodo_fin_sugerido = periodo_inicio_sugerido + timedelta(days=6)
|
|
56
|
+
case "quincenal":
|
|
57
|
+
if periodo_inicio_sugerido.day <= 15:
|
|
58
|
+
periodo_fin_sugerido = periodo_inicio_sugerido.replace(day=15)
|
|
59
|
+
else:
|
|
60
|
+
# End of month
|
|
61
|
+
next_month = periodo_inicio_sugerido.replace(day=28) + timedelta(days=4)
|
|
62
|
+
periodo_fin_sugerido = next_month - timedelta(days=next_month.day)
|
|
63
|
+
case _: # mensual or other
|
|
64
|
+
# End of month
|
|
65
|
+
next_month = periodo_inicio_sugerido.replace(day=28) + timedelta(days=4)
|
|
66
|
+
periodo_fin_sugerido = next_month - timedelta(days=next_month.day)
|
|
67
|
+
|
|
68
|
+
return periodo_inicio_sugerido, periodo_fin_sugerido
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def ejecutar_nomina(
|
|
72
|
+
planilla: Planilla,
|
|
73
|
+
periodo_inicio: date,
|
|
74
|
+
periodo_fin: date,
|
|
75
|
+
fecha_calculo: date,
|
|
76
|
+
usuario: str,
|
|
77
|
+
) -> tuple[Nomina | None, list[str], list[str]]:
|
|
78
|
+
"""Execute a nomina calculation.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
planilla: The planilla to execute
|
|
82
|
+
periodo_inicio: Start date of the period
|
|
83
|
+
periodo_fin: End date of the period
|
|
84
|
+
fecha_calculo: Calculation date
|
|
85
|
+
usuario: Username of the user executing
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (nomina, errors, warnings)
|
|
89
|
+
"""
|
|
90
|
+
# Count active employees
|
|
91
|
+
num_empleados = sum(1 for pe in planilla.planilla_empleados if pe.activo and pe.empleado.activo)
|
|
92
|
+
|
|
93
|
+
# Get configurable threshold for background processing
|
|
94
|
+
threshold = current_app.config.get("BACKGROUND_PAYROLL_THRESHOLD", 100)
|
|
95
|
+
|
|
96
|
+
# Determine if we should process in background
|
|
97
|
+
if num_empleados > threshold:
|
|
98
|
+
# Create nomina record with "calculando" status
|
|
99
|
+
nomina = Nomina(
|
|
100
|
+
planilla_id=planilla.id,
|
|
101
|
+
periodo_inicio=periodo_inicio,
|
|
102
|
+
periodo_fin=periodo_fin,
|
|
103
|
+
generado_por=usuario,
|
|
104
|
+
estado=NominaEstado.CALCULANDO,
|
|
105
|
+
total_bruto=0,
|
|
106
|
+
total_deducciones=0,
|
|
107
|
+
total_neto=0,
|
|
108
|
+
total_empleados=num_empleados,
|
|
109
|
+
empleados_procesados=0,
|
|
110
|
+
empleados_con_error=0,
|
|
111
|
+
procesamiento_en_background=True,
|
|
112
|
+
)
|
|
113
|
+
db.session.add(nomina)
|
|
114
|
+
db.session.commit()
|
|
115
|
+
|
|
116
|
+
# Enqueue background task
|
|
117
|
+
try:
|
|
118
|
+
queue = get_queue_driver()
|
|
119
|
+
queue.enqueue(
|
|
120
|
+
"process_large_payroll",
|
|
121
|
+
nomina_id=nomina.id,
|
|
122
|
+
planilla_id=planilla.id,
|
|
123
|
+
periodo_inicio=periodo_inicio.isoformat(),
|
|
124
|
+
periodo_fin=periodo_fin.isoformat(),
|
|
125
|
+
fecha_calculo=fecha_calculo.isoformat(),
|
|
126
|
+
usuario=usuario,
|
|
127
|
+
)
|
|
128
|
+
return nomina, [], []
|
|
129
|
+
except Exception as e:
|
|
130
|
+
# If background processing fails, mark nomina as error
|
|
131
|
+
nomina.estado = NominaEstado.ERROR
|
|
132
|
+
nomina.errores_calculo = {"background_task_initialization_error": str(e)}
|
|
133
|
+
db.session.commit()
|
|
134
|
+
return None, [f"Error al iniciar el procesamiento en segundo plano: {str(e)}"], []
|
|
135
|
+
else:
|
|
136
|
+
# For smaller payrolls, process synchronously
|
|
137
|
+
engine = NominaEngine(
|
|
138
|
+
planilla=planilla,
|
|
139
|
+
periodo_inicio=periodo_inicio,
|
|
140
|
+
periodo_fin=periodo_fin,
|
|
141
|
+
fecha_calculo=fecha_calculo,
|
|
142
|
+
usuario=usuario,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
nomina = engine.ejecutar()
|
|
146
|
+
return nomina, engine.errors, engine.warnings
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def recalcular_nomina(
|
|
150
|
+
nomina: Nomina, planilla: Planilla, usuario: str
|
|
151
|
+
) -> tuple[Nomina | None, list[str], list[str]]:
|
|
152
|
+
"""Recalculate an existing nomina.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
nomina: The nomina to recalculate
|
|
156
|
+
planilla: The planilla
|
|
157
|
+
usuario: Username of the user recalculating
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Tuple of (new_nomina, errors, warnings)
|
|
161
|
+
"""
|
|
162
|
+
from coati_payroll.model import (
|
|
163
|
+
NominaEmpleado,
|
|
164
|
+
NominaDetalle,
|
|
165
|
+
NominaNovedad,
|
|
166
|
+
AdelantoAbono,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Store the original period and calculation date for consistency
|
|
170
|
+
periodo_inicio = nomina.periodo_inicio
|
|
171
|
+
periodo_fin = nomina.periodo_fin
|
|
172
|
+
fecha_calculo_original = nomina.fecha_calculo_original or nomina.fecha_generacion.date()
|
|
173
|
+
nomina_original_id = nomina.id
|
|
174
|
+
|
|
175
|
+
# Delete related AdelantoAbono records
|
|
176
|
+
db.session.execute(db.delete(AdelantoAbono).where(AdelantoAbono.nomina_id == nomina.id))
|
|
177
|
+
|
|
178
|
+
# Delete NominaNovedad records
|
|
179
|
+
db.session.execute(db.delete(NominaNovedad).where(NominaNovedad.nomina_id == nomina.id))
|
|
180
|
+
|
|
181
|
+
# Delete NominaDetalle records
|
|
182
|
+
db.session.execute(
|
|
183
|
+
db.delete(NominaDetalle).where(
|
|
184
|
+
NominaDetalle.nomina_empleado_id.in_(
|
|
185
|
+
db.select(NominaEmpleado.id).where(NominaEmpleado.nomina_id == nomina.id)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Delete all NominaEmpleado records
|
|
191
|
+
db.session.execute(db.delete(NominaEmpleado).where(NominaEmpleado.nomina_id == nomina.id))
|
|
192
|
+
|
|
193
|
+
# Delete the nomina record itself
|
|
194
|
+
db.session.delete(nomina)
|
|
195
|
+
db.session.commit()
|
|
196
|
+
|
|
197
|
+
# Re-execute the payroll with the ORIGINAL calculation date for consistency
|
|
198
|
+
engine = NominaEngine(
|
|
199
|
+
planilla=planilla,
|
|
200
|
+
periodo_inicio=periodo_inicio,
|
|
201
|
+
periodo_fin=periodo_fin,
|
|
202
|
+
fecha_calculo=fecha_calculo_original,
|
|
203
|
+
usuario=usuario,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
new_nomina = engine.ejecutar()
|
|
207
|
+
|
|
208
|
+
# Mark as recalculation and link to original
|
|
209
|
+
if new_nomina:
|
|
210
|
+
new_nomina.es_recalculo = True
|
|
211
|
+
new_nomina.nomina_original_id = nomina_original_id
|
|
212
|
+
|
|
213
|
+
# Create audit log for recalculation
|
|
214
|
+
from coati_payroll.audit_helpers import crear_log_auditoria_nomina
|
|
215
|
+
|
|
216
|
+
crear_log_auditoria_nomina(
|
|
217
|
+
nomina=new_nomina,
|
|
218
|
+
accion="recalculated",
|
|
219
|
+
usuario=usuario,
|
|
220
|
+
descripcion=f"Nómina recalculada desde nómina original {nomina_original_id}",
|
|
221
|
+
cambios={
|
|
222
|
+
"nomina_original_id": nomina_original_id,
|
|
223
|
+
"fecha_calculo_original": fecha_calculo_original.isoformat(),
|
|
224
|
+
"periodo_inicio": periodo_inicio.isoformat(),
|
|
225
|
+
"periodo_fin": periodo_fin.isoformat(),
|
|
226
|
+
},
|
|
227
|
+
estado_anterior="deleted",
|
|
228
|
+
estado_nuevo=new_nomina.estado,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
db.session.commit()
|
|
232
|
+
|
|
233
|
+
return new_nomina, engine.errors, engine.warnings
|
|
@@ -0,0 +1,126 @@
|
|
|
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 novedad business logic."""
|
|
15
|
+
|
|
16
|
+
from decimal import Decimal
|
|
17
|
+
from coati_payroll.model import db, Planilla, Nomina, NominaNovedad
|
|
18
|
+
from coati_payroll.vistas.planilla.helpers.form_helpers import get_concepto_ids_from_form
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NovedadService:
|
|
22
|
+
"""Service for novedad operations."""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def listar_novedades(planilla: Planilla, nomina: Nomina) -> list:
|
|
26
|
+
"""List all novedades for a nomina.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
planilla: The planilla
|
|
30
|
+
nomina: The nomina
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of NominaNovedad objects
|
|
34
|
+
"""
|
|
35
|
+
# Get all employees in this planilla
|
|
36
|
+
empleado_ids = [pe.empleado_id for pe in planilla.planilla_empleados if pe.activo]
|
|
37
|
+
|
|
38
|
+
# Query novedades that fall within the nomina period and are for employees in this planilla
|
|
39
|
+
novedades = (
|
|
40
|
+
db.session.execute(
|
|
41
|
+
db.select(NominaNovedad)
|
|
42
|
+
.filter(
|
|
43
|
+
NominaNovedad.empleado_id.in_(empleado_ids),
|
|
44
|
+
NominaNovedad.fecha_novedad >= nomina.periodo_inicio,
|
|
45
|
+
NominaNovedad.fecha_novedad <= nomina.periodo_fin,
|
|
46
|
+
)
|
|
47
|
+
.order_by(NominaNovedad.fecha_novedad.desc(), NominaNovedad.timestamp.desc())
|
|
48
|
+
)
|
|
49
|
+
.scalars()
|
|
50
|
+
.all()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return novedades
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def validar_fecha_novedad(fecha_novedad, nomina: Nomina) -> tuple[bool, str | None]:
|
|
57
|
+
"""Validate that fecha_novedad falls within the nomina period.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
fecha_novedad: The date to validate
|
|
61
|
+
nomina: The nomina
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple of (is_valid, error_message)
|
|
65
|
+
"""
|
|
66
|
+
from coati_payroll.i18n import _
|
|
67
|
+
|
|
68
|
+
if fecha_novedad:
|
|
69
|
+
if fecha_novedad < nomina.periodo_inicio or fecha_novedad > nomina.periodo_fin:
|
|
70
|
+
return False, _(
|
|
71
|
+
"La fecha de la novedad debe estar dentro del período de la nómina " "({} a {})."
|
|
72
|
+
).format(
|
|
73
|
+
nomina.periodo_inicio.strftime("%d/%m/%Y"),
|
|
74
|
+
nomina.periodo_fin.strftime("%d/%m/%Y"),
|
|
75
|
+
)
|
|
76
|
+
return True, None
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def crear_novedad(nomina: Nomina, form, usuario: str) -> NominaNovedad:
|
|
80
|
+
"""Create a new novedad.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
nomina: The nomina
|
|
84
|
+
form: The form with novedad data
|
|
85
|
+
usuario: Username of the user creating
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The created NominaNovedad
|
|
89
|
+
"""
|
|
90
|
+
percepcion_id, deduccion_id = get_concepto_ids_from_form(form)
|
|
91
|
+
|
|
92
|
+
novedad = NominaNovedad(
|
|
93
|
+
nomina_id=nomina.id,
|
|
94
|
+
empleado_id=form.empleado_id.data,
|
|
95
|
+
codigo_concepto=form.codigo_concepto.data,
|
|
96
|
+
tipo_valor=form.tipo_valor.data,
|
|
97
|
+
valor_cantidad=Decimal(str(form.valor_cantidad.data)),
|
|
98
|
+
fecha_novedad=form.fecha_novedad.data,
|
|
99
|
+
percepcion_id=percepcion_id,
|
|
100
|
+
deduccion_id=deduccion_id,
|
|
101
|
+
creado_por=usuario,
|
|
102
|
+
)
|
|
103
|
+
db.session.add(novedad)
|
|
104
|
+
db.session.commit()
|
|
105
|
+
return novedad
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def actualizar_novedad(novedad: NominaNovedad, form, usuario: str):
|
|
109
|
+
"""Update an existing novedad.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
novedad: The novedad to update
|
|
113
|
+
form: The form with updated data
|
|
114
|
+
usuario: Username of the user updating
|
|
115
|
+
"""
|
|
116
|
+
percepcion_id, deduccion_id = get_concepto_ids_from_form(form)
|
|
117
|
+
|
|
118
|
+
novedad.empleado_id = form.empleado_id.data
|
|
119
|
+
novedad.codigo_concepto = form.codigo_concepto.data
|
|
120
|
+
novedad.tipo_valor = form.tipo_valor.data
|
|
121
|
+
novedad.valor_cantidad = Decimal(str(form.valor_cantidad.data))
|
|
122
|
+
novedad.fecha_novedad = form.fecha_novedad.data
|
|
123
|
+
novedad.percepcion_id = percepcion_id
|
|
124
|
+
novedad.deduccion_id = deduccion_id
|
|
125
|
+
novedad.modificado_por = usuario
|
|
126
|
+
db.session.commit()
|
|
@@ -0,0 +1,34 @@
|
|
|
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 planilla business logic."""
|
|
15
|
+
|
|
16
|
+
from coati_payroll.model import Planilla
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlanillaService:
|
|
20
|
+
"""Service for planilla operations."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def can_delete(planilla: Planilla) -> tuple[bool, str | None]:
|
|
24
|
+
"""Check if a planilla can be deleted.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
planilla: The planilla to check
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple of (can_delete, error_message). If can_delete is True, error_message is None.
|
|
31
|
+
"""
|
|
32
|
+
if planilla.nominas:
|
|
33
|
+
return False, "No se puede eliminar una planilla con nóminas generadas."
|
|
34
|
+
return True, None
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"""Validators for planilla business logic."""
|
|
15
|
+
|
|
16
|
+
from coati_payroll.vistas.planilla.validators.planilla_validators import PlanillaValidator
|
|
17
|
+
|
|
18
|
+
__all__ = ["PlanillaValidator"]
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
"""Business logic validators for planilla operations."""
|
|
15
|
+
|
|
16
|
+
from coati_payroll.model import Planilla, Empleado
|
|
17
|
+
from coati_payroll.i18n import _
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlanillaValidator:
|
|
21
|
+
"""Validators for planilla business logic."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def validar_empresa_empleado(planilla: Planilla, empleado: Empleado) -> tuple[bool, str | None]:
|
|
25
|
+
"""Validate that planilla and employee belong to the same company.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
planilla: The planilla to validate
|
|
29
|
+
empleado: The employee to validate
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Tuple of (is_valid, error_message). If valid, error_message is None.
|
|
33
|
+
"""
|
|
34
|
+
if not planilla.empresa_id:
|
|
35
|
+
return False, _("La planilla debe tener una empresa asignada antes de agregar empleados.")
|
|
36
|
+
if not empleado.empresa_id:
|
|
37
|
+
return False, _("El empleado debe tener una empresa asignada antes de ser agregado a una planilla.")
|
|
38
|
+
if planilla.empresa_id != empleado.empresa_id:
|
|
39
|
+
return False, _("El empleado y la planilla deben pertenecer a la misma empresa.")
|
|
40
|
+
return True, None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, flash, redirect, render_template, url_for
|
|
4
|
+
|
|
5
|
+
from coati_payroll.i18n import _
|
|
6
|
+
from coati_payroll.model import PluginRegistry, db
|
|
7
|
+
from coati_payroll.rbac import require_write_access
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
plugins_bp = Blueprint("plugins", __name__, url_prefix="/plugins")
|
|
11
|
+
|
|
12
|
+
# Constants
|
|
13
|
+
ROUTE_PLUGINS_INDEX = "plugins.index"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@plugins_bp.route("/")
|
|
17
|
+
@require_write_access()
|
|
18
|
+
def index():
|
|
19
|
+
plugins = db.session.execute(db.select(PluginRegistry).order_by(PluginRegistry.distribution_name)).scalars().all()
|
|
20
|
+
return render_template("modules/plugins/index.html", plugins=plugins)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@plugins_bp.route("/toggle/<string:plugin_id>", methods=["POST"])
|
|
24
|
+
@require_write_access()
|
|
25
|
+
def toggle(plugin_id: str):
|
|
26
|
+
plugin = db.session.get(PluginRegistry, plugin_id)
|
|
27
|
+
if not plugin:
|
|
28
|
+
flash(_("Plugin no encontrado."), "error")
|
|
29
|
+
return redirect(url_for(ROUTE_PLUGINS_INDEX))
|
|
30
|
+
|
|
31
|
+
if not plugin.installed and plugin.active:
|
|
32
|
+
plugin.active = False
|
|
33
|
+
db.session.commit()
|
|
34
|
+
flash(_("Plugin marcado como inactivo."), "success")
|
|
35
|
+
return redirect(url_for(ROUTE_PLUGINS_INDEX))
|
|
36
|
+
|
|
37
|
+
plugin.active = not plugin.active
|
|
38
|
+
db.session.commit()
|
|
39
|
+
|
|
40
|
+
if plugin.active:
|
|
41
|
+
flash(_("Plugin activado. Reinicie la aplicación para cargar sus blueprints."), "success")
|
|
42
|
+
else:
|
|
43
|
+
flash(_("Plugin desactivado."), "success")
|
|
44
|
+
|
|
45
|
+
return redirect(url_for(ROUTE_PLUGINS_INDEX))
|