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,450 @@
|
|
|
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
|
+
"""Interest calculation engine for loans.
|
|
15
|
+
|
|
16
|
+
This module provides functions to calculate interest for loans based on
|
|
17
|
+
different methods and amortization schedules.
|
|
18
|
+
|
|
19
|
+
Supported methods:
|
|
20
|
+
- French method (Préstamo Francés): Constant payment amount
|
|
21
|
+
- German method (Préstamo Alemán): Constant principal amortization
|
|
22
|
+
- Simple and compound interest calculations
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
# <-------------------------------------------------------------------------> #
|
|
28
|
+
# Standard library
|
|
29
|
+
# <-------------------------------------------------------------------------> #
|
|
30
|
+
from datetime import date
|
|
31
|
+
from dateutil.relativedelta import relativedelta
|
|
32
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
33
|
+
from typing import NamedTuple
|
|
34
|
+
|
|
35
|
+
# <-------------------------------------------------------------------------> #
|
|
36
|
+
# Third party libraries
|
|
37
|
+
# <-------------------------------------------------------------------------> #
|
|
38
|
+
|
|
39
|
+
# <-------------------------------------------------------------------------> #
|
|
40
|
+
# Local modules
|
|
41
|
+
# <-------------------------------------------------------------------------> #
|
|
42
|
+
from coati_payroll.enums import MetodoAmortizacion, TipoInteres
|
|
43
|
+
from typing import TYPE_CHECKING
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from coati_payroll.model import ConfiguracionCalculos
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CuotaPrestamo(NamedTuple):
|
|
50
|
+
"""Represents a single loan installment."""
|
|
51
|
+
|
|
52
|
+
numero: int # Installment number
|
|
53
|
+
fecha_estimada: date # Estimated payment date
|
|
54
|
+
cuota_total: Decimal # Total payment amount
|
|
55
|
+
interes: Decimal # Interest portion
|
|
56
|
+
capital: Decimal # Principal portion
|
|
57
|
+
saldo: Decimal # Remaining balance after payment
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _obtener_config_default(empresa_id: str | None = None) -> "ConfiguracionCalculos":
|
|
61
|
+
"""Get default configuration for interest calculations.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
empresa_id: Optional company ID to get company-specific config
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ConfiguracionCalculos instance with defaults
|
|
68
|
+
"""
|
|
69
|
+
from coati_payroll.model import ConfiguracionCalculos
|
|
70
|
+
from flask import has_app_context
|
|
71
|
+
|
|
72
|
+
# Only try to access database if we have an application context
|
|
73
|
+
if has_app_context():
|
|
74
|
+
from coati_payroll.model import db
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Try to find company-specific configuration
|
|
78
|
+
if empresa_id:
|
|
79
|
+
config = (
|
|
80
|
+
db.session.execute(
|
|
81
|
+
db.select(ConfiguracionCalculos).filter(
|
|
82
|
+
ConfiguracionCalculos.empresa_id == empresa_id,
|
|
83
|
+
ConfiguracionCalculos.activo.is_(True),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
.scalars()
|
|
87
|
+
.first()
|
|
88
|
+
)
|
|
89
|
+
if config:
|
|
90
|
+
return config
|
|
91
|
+
|
|
92
|
+
# Try to find global default (no empresa_id, no pais_id)
|
|
93
|
+
config = (
|
|
94
|
+
db.session.execute(
|
|
95
|
+
db.select(ConfiguracionCalculos).filter(
|
|
96
|
+
ConfiguracionCalculos.empresa_id.is_(None),
|
|
97
|
+
ConfiguracionCalculos.pais_id.is_(None),
|
|
98
|
+
ConfiguracionCalculos.activo.is_(True),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
.scalars()
|
|
102
|
+
.first()
|
|
103
|
+
)
|
|
104
|
+
if config:
|
|
105
|
+
return config
|
|
106
|
+
except RuntimeError:
|
|
107
|
+
# No application context, fall through to defaults
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# If no configuration exists or no app context, return a default instance (not saved to DB)
|
|
111
|
+
# This ensures backward compatibility with existing tests
|
|
112
|
+
return ConfiguracionCalculos(
|
|
113
|
+
empresa_id=None,
|
|
114
|
+
pais_id=None,
|
|
115
|
+
dias_mes_nomina=30,
|
|
116
|
+
dias_anio_nomina=365,
|
|
117
|
+
horas_jornada_diaria=Decimal("8.00"),
|
|
118
|
+
dias_mes_vacaciones=30,
|
|
119
|
+
dias_anio_vacaciones=365,
|
|
120
|
+
considerar_bisiesto_vacaciones=True,
|
|
121
|
+
dias_anio_financiero=365,
|
|
122
|
+
meses_anio_financiero=12,
|
|
123
|
+
dias_quincena=15,
|
|
124
|
+
dias_mes_antiguedad=30,
|
|
125
|
+
dias_anio_antiguedad=365,
|
|
126
|
+
activo=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def calcular_interes_simple(
|
|
131
|
+
principal: Decimal,
|
|
132
|
+
tasa_anual: Decimal,
|
|
133
|
+
dias: int,
|
|
134
|
+
config: "ConfiguracionCalculos | None" = None,
|
|
135
|
+
empresa_id: str | None = None,
|
|
136
|
+
) -> Decimal:
|
|
137
|
+
"""Calculate simple interest.
|
|
138
|
+
|
|
139
|
+
Formula: I = P * r * t
|
|
140
|
+
Where:
|
|
141
|
+
P = principal (saldo)
|
|
142
|
+
r = annual interest rate (as decimal, e.g., 0.05 for 5%)
|
|
143
|
+
t = time in years (dias / dias_anio_financiero)
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
principal: Loan balance
|
|
147
|
+
tasa_anual: Annual interest rate as percentage (e.g., 5.0 for 5%)
|
|
148
|
+
dias: Number of days to calculate interest for
|
|
149
|
+
config: Optional configuration object (if not provided, will fetch defaults)
|
|
150
|
+
empresa_id: Optional company ID to get company-specific config
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Interest amount
|
|
154
|
+
"""
|
|
155
|
+
if principal <= 0 or tasa_anual <= 0 or dias <= 0:
|
|
156
|
+
return Decimal("0.00")
|
|
157
|
+
|
|
158
|
+
# Get configuration if not provided
|
|
159
|
+
if config is None:
|
|
160
|
+
config = _obtener_config_default(empresa_id)
|
|
161
|
+
|
|
162
|
+
# Convert percentage to decimal (5.0 -> 0.05)
|
|
163
|
+
tasa_decimal = tasa_anual / Decimal("100")
|
|
164
|
+
|
|
165
|
+
# Calculate time in years using configured financial year days
|
|
166
|
+
dias_anio = Decimal(str(config.dias_anio_financiero))
|
|
167
|
+
tiempo_anios = Decimal(dias) / dias_anio
|
|
168
|
+
|
|
169
|
+
# Calculate interest: I = P * r * t
|
|
170
|
+
interes = principal * tasa_decimal * tiempo_anios
|
|
171
|
+
|
|
172
|
+
return interes.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def calcular_interes_compuesto(
|
|
176
|
+
principal: Decimal,
|
|
177
|
+
tasa_anual: Decimal,
|
|
178
|
+
dias: int,
|
|
179
|
+
config: "ConfiguracionCalculos | None" = None,
|
|
180
|
+
empresa_id: str | None = None,
|
|
181
|
+
) -> Decimal:
|
|
182
|
+
"""Calculate compound interest.
|
|
183
|
+
|
|
184
|
+
Formula: A = P * (1 + r/n)^(n*t)
|
|
185
|
+
Interest = A - P
|
|
186
|
+
|
|
187
|
+
For daily compounding:
|
|
188
|
+
n = dias_anio_financiero (compounds daily)
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
principal: Loan balance
|
|
192
|
+
tasa_anual: Annual interest rate as percentage (e.g., 5.0 for 5%)
|
|
193
|
+
dias: Number of days to calculate interest for
|
|
194
|
+
config: Optional configuration object (if not provided, will fetch defaults)
|
|
195
|
+
empresa_id: Optional company ID to get company-specific config
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Interest amount
|
|
199
|
+
"""
|
|
200
|
+
if principal <= 0 or tasa_anual <= 0 or dias <= 0:
|
|
201
|
+
return Decimal("0.00")
|
|
202
|
+
|
|
203
|
+
# Get configuration if not provided
|
|
204
|
+
if config is None:
|
|
205
|
+
config = _obtener_config_default(empresa_id)
|
|
206
|
+
|
|
207
|
+
# Convert percentage to decimal
|
|
208
|
+
tasa_decimal = tasa_anual / Decimal("100")
|
|
209
|
+
|
|
210
|
+
# For simplicity, we compound daily using configured financial year days
|
|
211
|
+
dias_anio = Decimal(str(config.dias_anio_financiero))
|
|
212
|
+
n = dias_anio
|
|
213
|
+
tiempo_anios = Decimal(dias) / dias_anio
|
|
214
|
+
|
|
215
|
+
# A = P * (1 + r/n)^(n*t)
|
|
216
|
+
# Use iterative multiplication to maintain decimal precision
|
|
217
|
+
# This is more precise than float conversion for financial calculations
|
|
218
|
+
base = Decimal("1") + (tasa_decimal / n)
|
|
219
|
+
num_periodos = int(n * tiempo_anios)
|
|
220
|
+
|
|
221
|
+
# Calculate factor iteratively to maintain precision
|
|
222
|
+
factor = Decimal("1")
|
|
223
|
+
for _ in range(num_periodos):
|
|
224
|
+
factor *= base
|
|
225
|
+
|
|
226
|
+
monto_final = principal * factor
|
|
227
|
+
|
|
228
|
+
interes = monto_final - principal
|
|
229
|
+
|
|
230
|
+
return interes.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def calcular_cuota_frances(
|
|
234
|
+
principal: Decimal,
|
|
235
|
+
tasa_anual: Decimal,
|
|
236
|
+
num_cuotas: int,
|
|
237
|
+
config: "ConfiguracionCalculos | None" = None,
|
|
238
|
+
empresa_id: str | None = None,
|
|
239
|
+
) -> Decimal:
|
|
240
|
+
"""Calculate constant payment amount for French method.
|
|
241
|
+
|
|
242
|
+
Formula: C = P * [r(1+r)^n] / [(1+r)^n - 1]
|
|
243
|
+
Where:
|
|
244
|
+
C = constant payment
|
|
245
|
+
P = principal
|
|
246
|
+
r = periodic interest rate (monthly)
|
|
247
|
+
n = number of periods
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
principal: Loan amount
|
|
251
|
+
tasa_anual: Annual interest rate as percentage
|
|
252
|
+
num_cuotas: Number of installments
|
|
253
|
+
config: Optional configuration object (if not provided, will fetch defaults)
|
|
254
|
+
empresa_id: Optional company ID to get company-specific config
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Constant payment amount
|
|
258
|
+
"""
|
|
259
|
+
if principal <= 0 or num_cuotas <= 0:
|
|
260
|
+
return Decimal("0.00")
|
|
261
|
+
|
|
262
|
+
# Get configuration if not provided
|
|
263
|
+
if config is None:
|
|
264
|
+
config = _obtener_config_default(empresa_id)
|
|
265
|
+
|
|
266
|
+
# If no interest, simple division
|
|
267
|
+
if tasa_anual <= 0:
|
|
268
|
+
return (principal / Decimal(num_cuotas)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
269
|
+
|
|
270
|
+
# Convert annual rate to monthly rate using configured months per year
|
|
271
|
+
tasa_decimal = tasa_anual / Decimal("100")
|
|
272
|
+
meses_anio = Decimal(str(config.meses_anio_financiero))
|
|
273
|
+
tasa_mensual = tasa_decimal / meses_anio
|
|
274
|
+
|
|
275
|
+
# Calculate (1 + r)^n using iterative multiplication for precision
|
|
276
|
+
base = Decimal("1") + tasa_mensual
|
|
277
|
+
factor = Decimal("1")
|
|
278
|
+
for _ in range(num_cuotas):
|
|
279
|
+
factor *= base
|
|
280
|
+
|
|
281
|
+
# Calculate payment: C = P * [r(1+r)^n] / [(1+r)^n - 1]
|
|
282
|
+
numerador = principal * tasa_mensual * factor
|
|
283
|
+
denominador = factor - Decimal("1")
|
|
284
|
+
|
|
285
|
+
if denominador == 0:
|
|
286
|
+
return principal / Decimal(num_cuotas)
|
|
287
|
+
|
|
288
|
+
cuota = numerador / denominador
|
|
289
|
+
|
|
290
|
+
return cuota.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def generar_tabla_amortizacion(
|
|
294
|
+
principal: Decimal,
|
|
295
|
+
tasa_anual: Decimal,
|
|
296
|
+
num_cuotas: int,
|
|
297
|
+
fecha_inicio: date,
|
|
298
|
+
metodo: str = MetodoAmortizacion.FRANCES,
|
|
299
|
+
tipo_interes: str = TipoInteres.SIMPLE,
|
|
300
|
+
) -> list[CuotaPrestamo]:
|
|
301
|
+
"""Generate complete amortization schedule for a loan.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
principal: Loan amount
|
|
305
|
+
tasa_anual: Annual interest rate as percentage
|
|
306
|
+
num_cuotas: Number of installments
|
|
307
|
+
fecha_inicio: Start date for first payment
|
|
308
|
+
metodo: Amortization method (frances or aleman)
|
|
309
|
+
tipo_interes: Interest type (simple or compuesto)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
List of loan installments
|
|
313
|
+
"""
|
|
314
|
+
if principal <= 0 or num_cuotas <= 0:
|
|
315
|
+
return []
|
|
316
|
+
|
|
317
|
+
tabla: list[CuotaPrestamo] = []
|
|
318
|
+
saldo = principal
|
|
319
|
+
# Get configuration for interest calculations
|
|
320
|
+
config = _obtener_config_default(None)
|
|
321
|
+
tasa_decimal = tasa_anual / Decimal("100")
|
|
322
|
+
meses_anio = Decimal(str(config.meses_anio_financiero))
|
|
323
|
+
tasa_mensual = tasa_decimal / meses_anio
|
|
324
|
+
|
|
325
|
+
# Get configuration for interest calculations
|
|
326
|
+
# Note: This function doesn't have empresa_id, so we use defaults
|
|
327
|
+
config = _obtener_config_default(None)
|
|
328
|
+
|
|
329
|
+
# Calculate based on method
|
|
330
|
+
if metodo == MetodoAmortizacion.FRANCES:
|
|
331
|
+
# French method: constant payment
|
|
332
|
+
cuota_constante = calcular_cuota_frances(principal, tasa_anual, num_cuotas, config=config)
|
|
333
|
+
|
|
334
|
+
for i in range(num_cuotas):
|
|
335
|
+
numero = i + 1
|
|
336
|
+
|
|
337
|
+
# Calculate interest for this period
|
|
338
|
+
if tasa_anual > 0:
|
|
339
|
+
interes = (saldo * tasa_mensual).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
340
|
+
else:
|
|
341
|
+
interes = Decimal("0.00")
|
|
342
|
+
|
|
343
|
+
# For last payment, adjust to clear remaining balance
|
|
344
|
+
if numero == num_cuotas:
|
|
345
|
+
capital = saldo
|
|
346
|
+
cuota_total = capital + interes
|
|
347
|
+
else:
|
|
348
|
+
capital = cuota_constante - interes
|
|
349
|
+
cuota_total = cuota_constante
|
|
350
|
+
|
|
351
|
+
saldo_nuevo = saldo - capital
|
|
352
|
+
|
|
353
|
+
fecha_estimada = fecha_inicio + relativedelta(months=numero)
|
|
354
|
+
|
|
355
|
+
tabla.append(
|
|
356
|
+
CuotaPrestamo(
|
|
357
|
+
numero=numero,
|
|
358
|
+
fecha_estimada=fecha_estimada,
|
|
359
|
+
cuota_total=cuota_total,
|
|
360
|
+
interes=interes,
|
|
361
|
+
capital=capital,
|
|
362
|
+
saldo=max(saldo_nuevo, Decimal("0.00")),
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
saldo = saldo_nuevo
|
|
367
|
+
|
|
368
|
+
elif metodo == MetodoAmortizacion.ALEMAN:
|
|
369
|
+
# German method: constant principal amortization
|
|
370
|
+
capital_constante = (principal / Decimal(num_cuotas)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
371
|
+
|
|
372
|
+
for i in range(num_cuotas):
|
|
373
|
+
numero = i + 1
|
|
374
|
+
|
|
375
|
+
# Calculate interest for this period
|
|
376
|
+
if tasa_anual > 0:
|
|
377
|
+
interes = (saldo * tasa_mensual).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
378
|
+
else:
|
|
379
|
+
interes = Decimal("0.00")
|
|
380
|
+
|
|
381
|
+
# For last payment, adjust to clear remaining balance
|
|
382
|
+
if numero == num_cuotas:
|
|
383
|
+
capital = saldo
|
|
384
|
+
else:
|
|
385
|
+
capital = capital_constante
|
|
386
|
+
|
|
387
|
+
cuota_total = capital + interes
|
|
388
|
+
saldo_nuevo = saldo - capital
|
|
389
|
+
|
|
390
|
+
fecha_estimada = fecha_inicio + relativedelta(months=numero)
|
|
391
|
+
|
|
392
|
+
tabla.append(
|
|
393
|
+
CuotaPrestamo(
|
|
394
|
+
numero=numero,
|
|
395
|
+
fecha_estimada=fecha_estimada,
|
|
396
|
+
cuota_total=cuota_total,
|
|
397
|
+
interes=interes,
|
|
398
|
+
capital=capital,
|
|
399
|
+
saldo=max(saldo_nuevo, Decimal("0.00")),
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
saldo = saldo_nuevo
|
|
404
|
+
|
|
405
|
+
return tabla
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def calcular_interes_periodo(
|
|
409
|
+
saldo: Decimal,
|
|
410
|
+
tasa_anual: Decimal,
|
|
411
|
+
fecha_desde: date,
|
|
412
|
+
fecha_hasta: date,
|
|
413
|
+
tipo_interes: str = TipoInteres.SIMPLE,
|
|
414
|
+
config: "ConfiguracionCalculos | None" = None,
|
|
415
|
+
empresa_id: str | None = None,
|
|
416
|
+
) -> tuple[Decimal, int]:
|
|
417
|
+
"""Calculate interest for a specific period.
|
|
418
|
+
|
|
419
|
+
This function is used during payroll processing to calculate
|
|
420
|
+
interest for the days elapsed since the last calculation.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
saldo: Current loan balance
|
|
424
|
+
tasa_anual: Annual interest rate as percentage
|
|
425
|
+
fecha_desde: Start date of period
|
|
426
|
+
fecha_hasta: End date of period
|
|
427
|
+
tipo_interes: Type of interest (simple or compuesto)
|
|
428
|
+
config: Optional configuration object (if not provided, will fetch defaults)
|
|
429
|
+
empresa_id: Optional company ID to get company-specific config
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Tuple of (interest amount, number of days)
|
|
433
|
+
"""
|
|
434
|
+
if saldo <= 0 or tasa_anual <= 0:
|
|
435
|
+
return Decimal("0.00"), 0
|
|
436
|
+
|
|
437
|
+
# Calculate days elapsed
|
|
438
|
+
dias = (fecha_hasta - fecha_desde).days
|
|
439
|
+
|
|
440
|
+
if dias <= 0:
|
|
441
|
+
return Decimal("0.00"), 0
|
|
442
|
+
|
|
443
|
+
# Calculate interest based on type
|
|
444
|
+
if tipo_interes == TipoInteres.COMPUESTO:
|
|
445
|
+
interes = calcular_interes_compuesto(saldo, tasa_anual, dias, config=config, empresa_id=empresa_id)
|
|
446
|
+
else:
|
|
447
|
+
# Default to simple interest
|
|
448
|
+
interes = calcular_interes_simple(saldo, tasa_anual, dias, config=config, empresa_id=empresa_id)
|
|
449
|
+
|
|
450
|
+
return interes, dias
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
|
|
15
|
+
"""Liquidación laboral execution engine."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .engine import LiquidacionEngine, ejecutar_liquidacion, recalcular_liquidacion
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"LiquidacionEngine",
|
|
23
|
+
"ejecutar_liquidacion",
|
|
24
|
+
"recalcular_liquidacion",
|
|
25
|
+
]
|