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.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. 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
+ ]