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,90 @@
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
+ """Accumulation processor for annual accumulated values."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+
20
+ from coati_payroll.model import db, Deduccion
21
+ from coati_payroll.i18n import _
22
+ from ..domain.employee_calculation import EmpleadoCalculo
23
+ from ..repositories.acumulado_repository import AcumuladoRepository
24
+ from ..validators import ValidationError
25
+
26
+
27
+ class AccumulationProcessor:
28
+ """Processor for updating accumulated annual values."""
29
+
30
+ def __init__(self, acumulado_repository: AcumuladoRepository):
31
+ self.acumulado_repo = acumulado_repository
32
+
33
+ def update_accumulations(
34
+ self,
35
+ emp_calculo: EmpleadoCalculo,
36
+ planilla,
37
+ periodo_fin: date,
38
+ fecha_calculo: date,
39
+ ) -> None:
40
+ """Update accumulated annual values for the employee."""
41
+ if not planilla.tipo_planilla:
42
+ return
43
+
44
+ tipo_planilla = planilla.tipo_planilla
45
+ empleado = emp_calculo.empleado
46
+
47
+ # Calculate fiscal period
48
+ anio = fecha_calculo.year
49
+ mes_inicio = tipo_planilla.mes_inicio_fiscal
50
+ dia_inicio = tipo_planilla.dia_inicio_fiscal
51
+
52
+ if fecha_calculo.month < mes_inicio:
53
+ anio -= 1
54
+
55
+ periodo_fiscal_inicio = date(anio, mes_inicio, dia_inicio)
56
+
57
+ # Determine empresa_id
58
+ empresa_id = planilla.empresa_id or empleado.empresa_id
59
+ if not empresa_id:
60
+ raise ValidationError(
61
+ _("No se puede crear acumulado anual: ni la planilla ni el empleado tienen empresa_id asignado")
62
+ )
63
+
64
+ # Get or create accumulated record
65
+ acumulado = self.acumulado_repo.get_or_create(empleado, tipo_planilla.id, empresa_id, periodo_fiscal_inicio)
66
+
67
+ # Reset monthly accumulation if entering a new month
68
+ acumulado.reset_mes_acumulado_if_needed(periodo_fin)
69
+
70
+ # Update accumulated values
71
+ acumulado.salario_bruto_acumulado += emp_calculo.salario_bruto
72
+ acumulado.salario_acumulado_mes += emp_calculo.salario_bruto
73
+ acumulado.periodos_procesados += 1
74
+ acumulado.ultimo_periodo_procesado = periodo_fin
75
+
76
+ # Calculate gravable income (perceptions that are gravable)
77
+ salario_gravable = emp_calculo.salario_base
78
+ for percepcion in emp_calculo.percepciones:
79
+ if percepcion.gravable:
80
+ salario_gravable += percepcion.monto
81
+ acumulado.salario_gravable_acumulado += salario_gravable
82
+
83
+ # Sum up before-tax deductions and taxes
84
+ for deduccion in emp_calculo.deducciones:
85
+ deduccion_obj = db.session.get(Deduccion, deduccion.deduccion_id) if deduccion.deduccion_id else None
86
+ if deduccion_obj:
87
+ if deduccion_obj.es_impuesto:
88
+ acumulado.impuesto_retenido_acumulado += deduccion.monto
89
+ elif deduccion_obj.antes_impuesto:
90
+ acumulado.deducciones_antes_impuesto_acumulado += deduccion.monto
@@ -0,0 +1,227 @@
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
+ """Loan processor for automatic loan and advance deductions."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from coati_payroll.model import db, Adelanto, AdelantoAbono, Nomina, Liquidacion
22
+ from coati_payroll.enums import AdelantoEstado
23
+ from coati_payroll.i18n import _
24
+ from ..domain.calculation_items import DeduccionItem
25
+
26
+
27
+ class LoanProcessor:
28
+ """Processor for automatic loan and advance deductions."""
29
+
30
+ def __init__(
31
+ self,
32
+ nomina: Nomina | None,
33
+ fecha_calculo: date,
34
+ periodo_inicio: date,
35
+ periodo_fin: date,
36
+ liquidacion: Liquidacion | None = None,
37
+ calcular_interes: bool = True,
38
+ ):
39
+ self.nomina = nomina
40
+ self.liquidacion = liquidacion
41
+ self.fecha_calculo = fecha_calculo
42
+ self.periodo_inicio = periodo_inicio
43
+ self.periodo_fin = periodo_fin
44
+ self.calcular_interes = calcular_interes
45
+
46
+ def process_loans(
47
+ self, empleado_id: str, saldo_disponible: Decimal, aplicar_prestamos: bool, prioridad_prestamos: int
48
+ ) -> list[DeduccionItem]:
49
+ """Process loans for an employee."""
50
+ deductions = []
51
+
52
+ if not aplicar_prestamos:
53
+ return deductions
54
+
55
+ # Get active loans
56
+ from sqlalchemy import select
57
+
58
+ prestamos = list(
59
+ db.session.execute(
60
+ select(Adelanto).filter(
61
+ Adelanto.empleado_id == empleado_id,
62
+ Adelanto.estado == AdelantoEstado.APROBADO,
63
+ Adelanto.saldo_pendiente > 0,
64
+ Adelanto.deduccion_id.isnot(None), # Only loans, not advances
65
+ )
66
+ )
67
+ .scalars()
68
+ .all()
69
+ )
70
+
71
+ for prestamo in prestamos:
72
+ if saldo_disponible <= 0:
73
+ break
74
+
75
+ # Calculate and apply interest if applicable
76
+ if self.calcular_interes:
77
+ self._calculate_interest(prestamo)
78
+
79
+ monto_cuota = Decimal(str(prestamo.monto_por_cuota or 0))
80
+ if monto_cuota <= 0:
81
+ continue
82
+
83
+ monto_aplicar = min(monto_cuota, saldo_disponible)
84
+
85
+ item = DeduccionItem(
86
+ codigo=f"PRESTAMO_{prestamo.id[:8]}",
87
+ nombre=f"Cuota préstamo - {prestamo.motivo or 'N/A'}",
88
+ monto=monto_aplicar,
89
+ prioridad=prioridad_prestamos,
90
+ es_obligatoria=False,
91
+ tipo="prestamo",
92
+ )
93
+ deductions.append(item)
94
+ saldo_disponible -= monto_aplicar
95
+
96
+ # Record the payment
97
+ self._record_payment(prestamo, monto_aplicar)
98
+
99
+ return deductions
100
+
101
+ def process_advances(
102
+ self, empleado_id: str, saldo_disponible: Decimal, aplicar_adelantos: bool, prioridad_adelantos: int
103
+ ) -> list[DeduccionItem]:
104
+ """Process salary advances for an employee."""
105
+ deductions = []
106
+
107
+ if not aplicar_adelantos:
108
+ return deductions
109
+
110
+ from sqlalchemy import select
111
+
112
+ adelantos = list(
113
+ db.session.execute(
114
+ select(Adelanto).filter(
115
+ Adelanto.empleado_id == empleado_id,
116
+ Adelanto.estado == AdelantoEstado.APROBADO,
117
+ Adelanto.saldo_pendiente > 0,
118
+ Adelanto.deduccion_id.is_(None), # Only advances, not loans
119
+ )
120
+ )
121
+ .scalars()
122
+ .all()
123
+ )
124
+
125
+ for adelanto in adelantos:
126
+ if saldo_disponible <= 0:
127
+ break
128
+
129
+ monto_cuota = Decimal(str(adelanto.monto_por_cuota or adelanto.saldo_pendiente))
130
+ monto_aplicar = min(monto_cuota, saldo_disponible)
131
+
132
+ item = DeduccionItem(
133
+ codigo=f"ADELANTO_{adelanto.id[:8]}",
134
+ nombre=f"Adelanto salarial - {adelanto.motivo or 'N/A'}",
135
+ monto=monto_aplicar,
136
+ prioridad=prioridad_adelantos,
137
+ es_obligatoria=False,
138
+ tipo="adelanto",
139
+ )
140
+ deductions.append(item)
141
+ saldo_disponible -= monto_aplicar
142
+
143
+ # Record the payment
144
+ self._record_payment(adelanto, monto_aplicar)
145
+
146
+ return deductions
147
+
148
+ def _calculate_interest(self, prestamo: Adelanto) -> None:
149
+ """Calculate and apply interest for a loan."""
150
+ from coati_payroll.interes_engine import calcular_interes_periodo
151
+ from coati_payroll.model import InteresAdelanto
152
+
153
+ tasa_interes = prestamo.tasa_interes or Decimal("0.0000")
154
+ if tasa_interes <= 0:
155
+ return
156
+
157
+ if prestamo.saldo_pendiente <= 0:
158
+ return
159
+
160
+ fecha_desde = prestamo.fecha_ultimo_calculo_interes
161
+ if not fecha_desde:
162
+ fecha_desde = prestamo.fecha_desembolso or prestamo.fecha_aprobacion
163
+
164
+ if not fecha_desde:
165
+ return
166
+
167
+ fecha_hasta = self.fecha_calculo
168
+
169
+ if fecha_desde >= fecha_hasta:
170
+ return
171
+
172
+ tipo_interes = prestamo.tipo_interes or "simple"
173
+ interes_calculado, dias = calcular_interes_periodo(
174
+ saldo=prestamo.saldo_pendiente,
175
+ tasa_anual=tasa_interes,
176
+ fecha_desde=fecha_desde,
177
+ fecha_hasta=fecha_hasta,
178
+ tipo_interes=tipo_interes,
179
+ )
180
+
181
+ if interes_calculado <= 0:
182
+ return
183
+
184
+ # Record interest in journal
185
+ interes_entrada = InteresAdelanto(
186
+ adelanto_id=prestamo.id,
187
+ nomina_id=self.nomina.id if self.nomina else None,
188
+ fecha_desde=fecha_desde,
189
+ fecha_hasta=fecha_hasta,
190
+ dias_transcurridos=dias,
191
+ saldo_base=prestamo.saldo_pendiente,
192
+ tasa_aplicada=tasa_interes,
193
+ interes_calculado=interes_calculado,
194
+ saldo_anterior=prestamo.saldo_pendiente,
195
+ saldo_posterior=prestamo.saldo_pendiente + interes_calculado,
196
+ observaciones=_("Interés calculado por nómina del {inicio} al {fin}").format(
197
+ inicio=self.periodo_inicio, fin=self.periodo_fin
198
+ ),
199
+ )
200
+ db.session.add(interes_entrada)
201
+
202
+ # Update loan with interest
203
+ prestamo.saldo_pendiente += interes_calculado
204
+ prestamo.interes_acumulado = (prestamo.interes_acumulado or Decimal("0.00")) + interes_calculado
205
+ prestamo.fecha_ultimo_calculo_interes = fecha_hasta
206
+
207
+ def _record_payment(self, adelanto: Adelanto, monto: Decimal) -> None:
208
+ """Record a payment towards a loan/advance."""
209
+ saldo_anterior = Decimal(str(adelanto.saldo_pendiente))
210
+ saldo_posterior = saldo_anterior - monto
211
+
212
+ abono = AdelantoAbono(
213
+ adelanto_id=adelanto.id,
214
+ nomina_id=self.nomina.id if self.nomina else None,
215
+ liquidacion_id=self.liquidacion.id if self.liquidacion else None,
216
+ fecha_abono=self.fecha_calculo,
217
+ monto_abonado=monto,
218
+ saldo_anterior=saldo_anterior,
219
+ saldo_posterior=max(saldo_posterior, Decimal("0.00")),
220
+ tipo_abono="liquidacion" if self.liquidacion else "nomina",
221
+ )
222
+ db.session.add(abono)
223
+
224
+ # Update adelanto balance
225
+ adelanto.saldo_pendiente = max(saldo_posterior, Decimal("0.00"))
226
+ if adelanto.saldo_pendiente <= 0:
227
+ adelanto.estado = AdelantoEstado.PAGADO
@@ -0,0 +1,42 @@
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
+ """Novelty processor for loading employee novelties."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from coati_payroll.model import Empleado
22
+ from ..repositories.novelty_repository import NoveltyRepository
23
+
24
+
25
+ class NoveltyProcessor:
26
+ """Processor for loading employee novelties."""
27
+
28
+ def __init__(self, novelty_repository: NoveltyRepository):
29
+ self.novelty_repo = novelty_repository
30
+
31
+ def load_novelties(self, empleado: Empleado, periodo_inicio: date, periodo_fin: date) -> dict[str, Decimal]:
32
+ """Load novelties for the employee in this period."""
33
+ novedades: dict[str, Decimal] = {}
34
+
35
+ nomina_novedades = self.novelty_repo.get_by_employee_and_period(empleado.id, periodo_inicio, periodo_fin)
36
+
37
+ for novedad in nomina_novedades:
38
+ codigo = novedad.codigo_concepto
39
+ valor = Decimal(str(novedad.valor_cantidad or 0))
40
+ novedades[codigo] = novedades.get(codigo, Decimal("0")) + valor
41
+
42
+ return novedades
@@ -0,0 +1,67 @@
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
+ """Vacation processor for vacation accrual and usage."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from coati_payroll.model import Planilla, Empleado, NominaEmpleado
19
+ from coati_payroll.log import log
20
+ from ..domain.employee_calculation import EmpleadoCalculo
21
+
22
+
23
+ class VacationProcessor:
24
+ """Processor for vacation accrual and usage."""
25
+
26
+ def __init__(
27
+ self, planilla: Planilla, periodo_inicio, periodo_fin, usuario: str | None = None, warnings: list[str] = None
28
+ ):
29
+ self.planilla = planilla
30
+ self.periodo_inicio = periodo_inicio
31
+ self.periodo_fin = periodo_fin
32
+ self.usuario = usuario
33
+ self.warnings = warnings or []
34
+
35
+ def process_vacations(
36
+ self, empleado: Empleado, emp_calculo: EmpleadoCalculo, nomina_empleado: NominaEmpleado
37
+ ) -> None:
38
+ """Process vacation accrual and usage for an employee."""
39
+ try:
40
+ from coati_payroll.vacation_service import VacationService
41
+
42
+ vacation_service = VacationService(
43
+ planilla=self.planilla,
44
+ periodo_inicio=self.periodo_inicio,
45
+ periodo_fin=self.periodo_fin,
46
+ )
47
+
48
+ # Accumulate vacation time
49
+ vacation_service.acumular_vacaciones_empleado(
50
+ empleado=empleado,
51
+ nomina_empleado=nomina_empleado,
52
+ usuario=self.usuario,
53
+ )
54
+
55
+ # Process vacation novelties (time off taken)
56
+ vacation_service.procesar_novedades_vacaciones(
57
+ empleado=empleado,
58
+ novedades=emp_calculo.novedades,
59
+ usuario=self.usuario,
60
+ )
61
+
62
+ except Exception as e:
63
+ log.error(f"Error procesando vacaciones para empleado {empleado.codigo_empleado}: {str(e)}")
64
+ self.warnings.append(
65
+ f"No se pudieron procesar vacaciones para {empleado.primer_nombre} "
66
+ f"{empleado.primer_apellido}: {str(e)}"
67
+ )
@@ -0,0 +1,32 @@
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
+ """Repository layer for data access."""
15
+
16
+ from .base_repository import BaseRepository
17
+ from .planilla_repository import PlanillaRepository
18
+ from .employee_repository import EmployeeRepository
19
+ from .acumulado_repository import AcumuladoRepository
20
+ from .novelty_repository import NoveltyRepository
21
+ from .exchange_rate_repository import ExchangeRateRepository
22
+ from .config_repository import ConfigRepository
23
+
24
+ __all__ = [
25
+ "BaseRepository",
26
+ "PlanillaRepository",
27
+ "EmployeeRepository",
28
+ "AcumuladoRepository",
29
+ "NoveltyRepository",
30
+ "ExchangeRateRepository",
31
+ "ConfigRepository",
32
+ ]
@@ -0,0 +1,83 @@
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
+ """Repository for AcumuladoAnual operations."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from typing import Optional
20
+
21
+ from coati_payroll.model import AcumuladoAnual, Empleado
22
+ from .base_repository import BaseRepository
23
+
24
+
25
+ class AcumuladoRepository(BaseRepository[AcumuladoAnual]):
26
+ """Repository for AcumuladoAnual operations."""
27
+
28
+ def get_by_id(self, acumulado_id: str) -> Optional[AcumuladoAnual]:
29
+ """Get acumulado by ID."""
30
+ return self.session.get(AcumuladoAnual, acumulado_id)
31
+
32
+ def get_or_create(
33
+ self,
34
+ empleado: Empleado,
35
+ tipo_planilla_id: str,
36
+ empresa_id: str,
37
+ periodo_fiscal_inicio: date,
38
+ ) -> AcumuladoAnual:
39
+ """Get or create acumulado for employee and fiscal period."""
40
+ from sqlalchemy import select
41
+
42
+ acumulado = (
43
+ self.session.execute(
44
+ select(AcumuladoAnual).filter(
45
+ AcumuladoAnual.empleado_id == empleado.id,
46
+ AcumuladoAnual.tipo_planilla_id == tipo_planilla_id,
47
+ AcumuladoAnual.empresa_id == empresa_id,
48
+ AcumuladoAnual.periodo_fiscal_inicio == periodo_fiscal_inicio,
49
+ )
50
+ )
51
+ .unique()
52
+ .scalar_one_or_none()
53
+ )
54
+
55
+ if not acumulado:
56
+ from datetime import date as date_type
57
+ from decimal import Decimal
58
+
59
+ periodo_fiscal_fin = date_type(
60
+ periodo_fiscal_inicio.year + 1, periodo_fiscal_inicio.month, periodo_fiscal_inicio.day
61
+ )
62
+
63
+ acumulado = AcumuladoAnual(
64
+ empleado_id=empleado.id,
65
+ tipo_planilla_id=tipo_planilla_id,
66
+ empresa_id=empresa_id,
67
+ periodo_fiscal_inicio=periodo_fiscal_inicio,
68
+ periodo_fiscal_fin=periodo_fiscal_fin,
69
+ salario_bruto_acumulado=empleado.salario_acumulado or Decimal("0.00"),
70
+ salario_gravable_acumulado=Decimal("0.00"),
71
+ deducciones_antes_impuesto_acumulado=empleado.impuesto_acumulado or Decimal("0.00"),
72
+ impuesto_retenido_acumulado=Decimal("0.00"),
73
+ periodos_procesados=0,
74
+ salario_acumulado_mes=Decimal("0.00"),
75
+ )
76
+ self.session.add(acumulado)
77
+
78
+ return acumulado
79
+
80
+ def save(self, acumulado: AcumuladoAnual) -> AcumuladoAnual:
81
+ """Save acumulado."""
82
+ self.session.add(acumulado)
83
+ return acumulado
@@ -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
+ """Base repository for data access."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import Generic, TypeVar, Optional
20
+
21
+ from sqlalchemy.orm import Session
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class BaseRepository(ABC, Generic[T]):
27
+ """Base repository for data access operations."""
28
+
29
+ def __init__(self, session: Session):
30
+ self.session = session
31
+
32
+ @abstractmethod
33
+ def get_by_id(self, id: str) -> Optional[T]:
34
+ """Get entity by ID."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def save(self, entity: T) -> T:
39
+ """Save entity."""
40
+ pass
@@ -0,0 +1,102 @@
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
+ """Repository for ConfiguracionCalculos operations."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from decimal import Decimal
19
+ from typing import Optional
20
+
21
+ from coati_payroll.model import ConfiguracionCalculos
22
+ from .base_repository import BaseRepository
23
+
24
+
25
+ class ConfigRepository(BaseRepository[ConfiguracionCalculos]):
26
+ """Repository for ConfiguracionCalculos operations."""
27
+
28
+ def get_by_id(self, config_id: str) -> Optional[ConfiguracionCalculos]:
29
+ """Get configuration by ID."""
30
+ return self.session.get(ConfiguracionCalculos, config_id)
31
+
32
+ def get_for_empresa(self, empresa_id: Optional[str]) -> ConfiguracionCalculos:
33
+ """Get configuration for empresa, or global default."""
34
+ from sqlalchemy import select
35
+
36
+ # Try company-specific configuration
37
+ if empresa_id:
38
+ config = (
39
+ self.session.execute(
40
+ select(ConfiguracionCalculos).filter(
41
+ ConfiguracionCalculos.empresa_id == empresa_id,
42
+ ConfiguracionCalculos.activo.is_(True),
43
+ )
44
+ )
45
+ .unique()
46
+ .scalar_one_or_none()
47
+ )
48
+ if config:
49
+ return config
50
+
51
+ # Try global default
52
+ config = (
53
+ self.session.execute(
54
+ select(ConfiguracionCalculos).filter(
55
+ ConfiguracionCalculos.empresa_id.is_(None),
56
+ ConfiguracionCalculos.pais_id.is_(None),
57
+ ConfiguracionCalculos.activo.is_(True),
58
+ )
59
+ )
60
+ .unique()
61
+ .scalar_one_or_none()
62
+ )
63
+
64
+ if config:
65
+ return config
66
+
67
+ # Return default instance (not saved to DB)
68
+ # =====================================================================
69
+ # DEFAULT VALUES DISCLAIMER (Per Social Contract)
70
+ # =====================================================================
71
+ # These default values are provided SOLELY to facilitate initial adoption.
72
+ # They do NOT represent legal rules for any specific jurisdiction.
73
+ # They are completely configurable by the implementer.
74
+ # They should NOT be assumed as correct for any specific jurisdiction.
75
+ #
76
+ # Implementers MUST review and configure these values according to
77
+ # their specific legal and business requirements before production use.
78
+ # =====================================================================
79
+ return ConfiguracionCalculos(
80
+ empresa_id=None,
81
+ pais_id=None,
82
+ dias_mes_nomina=30, # Example default - configure per jurisdiction
83
+ dias_anio_nomina=365, # Example default - configure per jurisdiction
84
+ horas_jornada_diaria=Decimal("8.00"), # Example default - configure per jurisdiction
85
+ dias_mes_vacaciones=30, # Example default - configure per jurisdiction
86
+ dias_anio_vacaciones=365, # Example default - configure per jurisdiction
87
+ considerar_bisiesto_vacaciones=True, # Example default - configure per jurisdiction
88
+ dias_anio_financiero=365, # Example default - configure per jurisdiction
89
+ meses_anio_financiero=12, # Example default - configure per jurisdiction
90
+ dias_quincena=15, # Example default - configure per jurisdiction
91
+ liquidacion_modo_dias="calendario",
92
+ liquidacion_factor_calendario=30,
93
+ liquidacion_factor_laboral=28,
94
+ dias_mes_antiguedad=30, # Example default - configure per jurisdiction
95
+ dias_anio_antiguedad=365, # Example default - configure per jurisdiction
96
+ activo=True,
97
+ )
98
+
99
+ def save(self, config: ConfiguracionCalculos) -> ConfiguracionCalculos:
100
+ """Save configuration."""
101
+ self.session.add(config)
102
+ return config