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,451 @@
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 service for integration with payroll engine.
15
+
16
+ This module provides the service layer for vacation accrual and usage
17
+ during payroll execution.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ # <-------------------------------------------------------------------------> #
23
+ # Standard library
24
+ # <-------------------------------------------------------------------------> #
25
+ from datetime import date
26
+ from decimal import Decimal, ROUND_HALF_UP
27
+ from typing import TYPE_CHECKING
28
+
29
+ # <-------------------------------------------------------------------------> #
30
+ # Third party libraries
31
+ # <-------------------------------------------------------------------------> #
32
+
33
+ # <-------------------------------------------------------------------------> #
34
+ # Local modules
35
+ # <-------------------------------------------------------------------------> #
36
+ from coati_payroll.enums import VacationLedgerType, AccrualMethod, AccrualFrequency
37
+ from coati_payroll.log import log
38
+
39
+ if TYPE_CHECKING:
40
+ from coati_payroll.model import (
41
+ Empleado,
42
+ Planilla,
43
+ VacationPolicy,
44
+ VacationAccount,
45
+ NominaEmpleado,
46
+ ConfiguracionCalculos,
47
+ )
48
+
49
+
50
+ class VacationService:
51
+ """Service for vacation accrual and usage during payroll execution."""
52
+
53
+ def __init__(self, planilla: Planilla, periodo_inicio: date, periodo_fin: date):
54
+ """Initialize vacation service.
55
+
56
+ Args:
57
+ planilla: The payroll being executed
58
+ periodo_inicio: Start date of payroll period
59
+ periodo_fin: End date of payroll period
60
+ """
61
+ self.planilla = planilla
62
+ self.periodo_inicio = periodo_inicio
63
+ self.periodo_fin = periodo_fin
64
+
65
+ def _obtener_config_calculos(self) -> ConfiguracionCalculos:
66
+ """Get calculation configuration for the current planilla.
67
+
68
+ Returns configuration specific to the planilla's company, or global defaults.
69
+ Always returns a valid configuration object with defaults if none exists.
70
+
71
+ Returns:
72
+ ConfiguracionCalculos instance with appropriate values
73
+ """
74
+ from coati_payroll.model import db, ConfiguracionCalculos
75
+
76
+ empresa_id = self.planilla.empresa_id if self.planilla else None
77
+
78
+ # Try to find company-specific configuration
79
+ if empresa_id:
80
+ config = (
81
+ db.session.execute(
82
+ db.select(ConfiguracionCalculos).filter(
83
+ ConfiguracionCalculos.empresa_id == empresa_id,
84
+ ConfiguracionCalculos.activo.is_(True),
85
+ )
86
+ )
87
+ .scalars()
88
+ .first()
89
+ )
90
+ if config:
91
+ return config
92
+
93
+ # Try to find global default (no empresa_id, no pais_id)
94
+ config = (
95
+ db.session.execute(
96
+ db.select(ConfiguracionCalculos).filter(
97
+ ConfiguracionCalculos.empresa_id.is_(None),
98
+ ConfiguracionCalculos.pais_id.is_(None),
99
+ ConfiguracionCalculos.activo.is_(True),
100
+ )
101
+ )
102
+ .scalars()
103
+ .first()
104
+ )
105
+ if config:
106
+ return config
107
+
108
+ # If no configuration exists, return a default instance (not saved to DB)
109
+ # This ensures backward compatibility with existing tests
110
+ from decimal import Decimal
111
+
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
+ def acumular_vacaciones_empleado(
130
+ self, empleado: Empleado, nomina_empleado: NominaEmpleado, usuario: str | None = None
131
+ ) -> Decimal:
132
+ """Accumulate vacation for an employee during payroll execution.
133
+
134
+ This method is called during payroll processing to automatically
135
+ accrue vacation time based on the employee's vacation policy.
136
+
137
+ Args:
138
+ empleado: The employee to accrue vacation for
139
+ nomina_empleado: The payroll record for this employee
140
+ usuario: Username executing the payroll
141
+
142
+ Returns:
143
+ The amount of vacation accrued
144
+ """
145
+ from coati_payroll.model import db, VacationAccount, VacationLedger
146
+
147
+ # Get active vacation account for this employee and payroll
148
+ account = db.session.execute(
149
+ db.select(VacationAccount)
150
+ .filter(
151
+ VacationAccount.empleado_id == empleado.id,
152
+ VacationAccount.activo.is_(True),
153
+ )
154
+ .join(VacationAccount.policy)
155
+ .filter(
156
+ (VacationAccount.policy.has(planilla_id=self.planilla.id))
157
+ | (VacationAccount.policy.has(empresa_id=self.planilla.empresa_id))
158
+ | ((VacationAccount.policy.has(planilla_id=None)) & (VacationAccount.policy.has(empresa_id=None)))
159
+ )
160
+ ).scalar_one_or_none()
161
+
162
+ if not account:
163
+ log.debug(
164
+ f"No active vacation account found for employee {empleado.codigo_empleado} "
165
+ f"in payroll {self.planilla.nombre}"
166
+ )
167
+ return Decimal("0.00")
168
+
169
+ policy = account.policy
170
+
171
+ # Check if employee meets minimum service requirement
172
+ if empleado.fecha_alta:
173
+ dias_servicio = (self.periodo_fin - empleado.fecha_alta).days
174
+ if dias_servicio < policy.min_service_days:
175
+ log.debug(
176
+ f"Employee {empleado.codigo_empleado} has not met minimum service days "
177
+ f"({dias_servicio} < {policy.min_service_days})"
178
+ )
179
+ return Decimal("0.00")
180
+
181
+ # Calculate accrual amount based on policy
182
+ accrual_amount = self._calcular_acumulacion(empleado, account, nomina_empleado)
183
+
184
+ if accrual_amount <= 0:
185
+ return Decimal("0.00")
186
+
187
+ # Check max balance limit
188
+ if policy.max_balance:
189
+ if account.current_balance + accrual_amount > policy.max_balance:
190
+ # Cap at max balance
191
+ accrual_amount = policy.max_balance - account.current_balance
192
+ if accrual_amount <= 0:
193
+ log.debug(
194
+ f"Employee {empleado.codigo_empleado} has reached max vacation balance "
195
+ f"({account.current_balance} >= {policy.max_balance})"
196
+ )
197
+ return Decimal("0.00")
198
+
199
+ # Create ledger entry for accrual
200
+ ledger_entry = VacationLedger(
201
+ account_id=account.id,
202
+ empleado_id=empleado.id,
203
+ fecha=self.periodo_fin,
204
+ entry_type=VacationLedgerType.ACCRUAL,
205
+ quantity=accrual_amount,
206
+ source="payroll",
207
+ reference_id=nomina_empleado.id,
208
+ reference_type="nomina_empleado",
209
+ observaciones=f"Acumulación automática en nómina del {self.periodo_inicio} al {self.periodo_fin}",
210
+ creado_por=usuario,
211
+ )
212
+
213
+ # Update account balance
214
+ account.current_balance = account.current_balance + accrual_amount
215
+ account.last_accrual_date = self.periodo_fin
216
+ account.modificado_por = usuario
217
+
218
+ ledger_entry.balance_after = account.current_balance
219
+
220
+ db.session.add(ledger_entry)
221
+ db.session.flush()
222
+
223
+ log.info(
224
+ f"Accrued {accrual_amount} {policy.unit_type} vacation for employee "
225
+ f"{empleado.codigo_empleado} (new balance: {account.current_balance})"
226
+ )
227
+
228
+ return accrual_amount
229
+
230
+ def _calcular_acumulacion(
231
+ self, empleado: Empleado, account: VacationAccount, nomina_empleado: NominaEmpleado
232
+ ) -> Decimal:
233
+ """Calculate vacation accrual amount based on policy.
234
+
235
+ Args:
236
+ empleado: The employee
237
+ account: The vacation account
238
+ nomina_empleado: The payroll record
239
+
240
+ Returns:
241
+ Amount to accrue
242
+ """
243
+ policy = account.policy
244
+
245
+ if policy.accrual_method == AccrualMethod.PERIODIC:
246
+ return self._calcular_acumulacion_periodica(policy)
247
+ elif policy.accrual_method == AccrualMethod.PROPORTIONAL:
248
+ return self._calcular_acumulacion_proporcional(empleado, policy, nomina_empleado)
249
+ elif policy.accrual_method == AccrualMethod.SENIORITY:
250
+ return self._calcular_acumulacion_antiguedad(empleado, policy)
251
+ else:
252
+ log.warning(f"Unknown accrual method: {policy.accrual_method}")
253
+ return Decimal("0.00")
254
+
255
+ def _calcular_acumulacion_periodica(self, policy: VacationPolicy) -> Decimal:
256
+ """Calculate periodic accrual (fixed amount per period).
257
+
258
+ Args:
259
+ policy: The vacation policy
260
+
261
+ Returns:
262
+ Accrual amount
263
+ """
264
+ # For periodic accrual, the rate is the amount per configured frequency
265
+ # If frequency matches payroll period, use the rate directly
266
+ # Otherwise, prorate based on actual days in period
267
+
268
+ dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
269
+
270
+ # Get configuration for vacation calculations
271
+ config = self._obtener_config_calculos()
272
+
273
+ # Determine expected days for frequency using configuration
274
+ if policy.accrual_frequency == AccrualFrequency.MONTHLY:
275
+ dias_esperados = config.dias_mes_vacaciones
276
+ elif policy.accrual_frequency == AccrualFrequency.BIWEEKLY:
277
+ dias_esperados = config.dias_quincena
278
+ elif policy.accrual_frequency == AccrualFrequency.ANNUAL:
279
+ dias_esperados = config.dias_anio_vacaciones
280
+ else:
281
+ dias_esperados = config.dias_mes_vacaciones
282
+
283
+ # Prorate if period doesn't match frequency
284
+ if dias_periodo == dias_esperados:
285
+ return policy.accrual_rate
286
+ else:
287
+ # Prorate based on days
288
+ return (policy.accrual_rate * Decimal(dias_periodo) / Decimal(dias_esperados)).quantize(
289
+ Decimal("0.0001"), rounding=ROUND_HALF_UP
290
+ )
291
+
292
+ def _calcular_acumulacion_proporcional(
293
+ self, empleado: Empleado, policy: VacationPolicy, nomina_empleado: NominaEmpleado
294
+ ) -> Decimal:
295
+ """Calculate proportional accrual (based on worked days/hours).
296
+
297
+ Args:
298
+ empleado: The employee
299
+ policy: The vacation policy
300
+ nomina_empleado: The payroll record
301
+
302
+ Returns:
303
+ Accrual amount
304
+ """
305
+ # For proportional accrual, calculate based on actual worked days/hours
306
+ # This requires tracking in the payroll record
307
+
308
+ dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
309
+
310
+ if policy.accrual_basis == "days_worked":
311
+ # Assume full days worked for now (could be enhanced to track absences)
312
+ dias_trabajados = Decimal(dias_periodo)
313
+ return (policy.accrual_rate * dias_trabajados).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
314
+ elif policy.accrual_basis == "hours_worked":
315
+ # Calculate based on hours (would need hours tracking in payroll)
316
+ # For now, estimate based on standard hours from configuration
317
+ config = self._obtener_config_calculos()
318
+ horas_estandar = Decimal(str(config.horas_jornada_diaria)) * Decimal(dias_periodo)
319
+ return (policy.accrual_rate * horas_estandar).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
320
+ else:
321
+ return Decimal("0.00")
322
+
323
+ def _calcular_acumulacion_antiguedad(self, empleado: Empleado, policy: VacationPolicy) -> Decimal:
324
+ """Calculate seniority-based accrual (tiered by years of service).
325
+
326
+ Args:
327
+ empleado: The employee
328
+ policy: The vacation policy
329
+
330
+ Returns:
331
+ Accrual amount
332
+ """
333
+ if not empleado.fecha_alta or not policy.seniority_tiers:
334
+ return Decimal("0.00")
335
+
336
+ # Get configuration for seniority calculations
337
+ config = self._obtener_config_calculos()
338
+
339
+ # Calculate years of service
340
+ # Use configured days per year, with leap year consideration if enabled
341
+ dias_anio = Decimal(str(config.dias_anio_antiguedad))
342
+ if config.considerar_bisiesto_vacaciones:
343
+ # Use 365.25 to account for leap years
344
+ dias_anio = Decimal("365.25")
345
+ anos_servicio = (self.periodo_fin - empleado.fecha_alta).days / float(dias_anio)
346
+
347
+ # Find applicable tier
348
+ rate = Decimal("0.00")
349
+ for tier in sorted(policy.seniority_tiers, key=lambda t: t.get("years", 0), reverse=True):
350
+ if anos_servicio >= tier.get("years", 0):
351
+ rate = Decimal(str(tier.get("rate", 0)))
352
+ break
353
+
354
+ if rate == 0:
355
+ return Decimal("0.00")
356
+
357
+ # For seniority, rate is typically annual, so prorate for period
358
+ if policy.accrual_frequency == AccrualFrequency.ANNUAL:
359
+ dias_periodo = (self.periodo_fin - self.periodo_inicio).days + 1
360
+ dias_anio = Decimal(str(config.dias_anio_vacaciones))
361
+ return (rate * Decimal(dias_periodo) / dias_anio).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
362
+ else:
363
+ # If frequency is monthly/biweekly, divide rate accordingly
364
+ meses_anio = Decimal(str(config.meses_anio_financiero))
365
+ return (rate / meses_anio).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
366
+
367
+ def procesar_novedades_vacaciones(
368
+ self, empleado: Empleado, novedades: dict | list, usuario: str | None = None
369
+ ) -> Decimal:
370
+ """Process vacation novelties (leave taken) during payroll execution.
371
+
372
+ This method processes vacation leave novelties that have been approved
373
+ and creates ledger entries to reduce the vacation balance.
374
+
375
+ Args:
376
+ empleado: The employee
377
+ novedades: Dictionary or list of novelties to process
378
+ usuario: Username executing the payroll
379
+
380
+ Returns:
381
+ Total vacation days/hours used
382
+ """
383
+ from coati_payroll.model import db, VacationNovelty, VacationLedger, NominaNovedad
384
+
385
+ total_usado = Decimal("0.00")
386
+
387
+ # Query vacation-related novedades for this employee in this period
388
+ nomina_novedades = (
389
+ db.session.execute(
390
+ db.select(NominaNovedad).filter(
391
+ NominaNovedad.empleado_id == empleado.id,
392
+ NominaNovedad.es_descanso_vacaciones.is_(True),
393
+ NominaNovedad.fecha_novedad >= self.periodo_inicio,
394
+ NominaNovedad.fecha_novedad <= self.periodo_fin,
395
+ )
396
+ )
397
+ .scalars()
398
+ .all()
399
+ )
400
+
401
+ for nomina_novedad in nomina_novedades:
402
+ # Get the associated vacation novelty
403
+ if not nomina_novedad.vacation_novelty_id:
404
+ continue
405
+
406
+ vac_novelty = db.session.get(VacationNovelty, nomina_novedad.vacation_novelty_id)
407
+
408
+ if not vac_novelty or vac_novelty.estado != "aprobado":
409
+ continue
410
+
411
+ # Skip if already processed (has ledger entry)
412
+ if vac_novelty.ledger_entry_id:
413
+ continue
414
+
415
+ account = vac_novelty.account
416
+
417
+ # Create ledger entry for usage
418
+ ledger_entry = VacationLedger(
419
+ account_id=account.id,
420
+ empleado_id=empleado.id,
421
+ fecha=self.periodo_fin,
422
+ entry_type=VacationLedgerType.USAGE,
423
+ quantity=-abs(vac_novelty.units), # Negative for usage
424
+ source="novelty",
425
+ reference_id=vac_novelty.id,
426
+ reference_type="vacation_novelty",
427
+ observaciones=f"Vacaciones del {vac_novelty.start_date} al {vac_novelty.end_date}",
428
+ creado_por=usuario,
429
+ )
430
+
431
+ # Update account balance
432
+ account.current_balance = account.current_balance - abs(vac_novelty.units)
433
+ account.modificado_por = usuario
434
+
435
+ ledger_entry.balance_after = account.current_balance
436
+
437
+ # Link ledger entry to novelty
438
+ vac_novelty.ledger_entry_id = ledger_entry.id
439
+ vac_novelty.estado = "disfrutado"
440
+
441
+ db.session.add(ledger_entry)
442
+ db.session.flush()
443
+
444
+ total_usado = total_usado + abs(vac_novelty.units)
445
+
446
+ log.info(
447
+ f"Processed vacation usage of {abs(vac_novelty.units)} for employee "
448
+ f"{empleado.codigo_empleado} (new balance: {account.current_balance})"
449
+ )
450
+
451
+ return total_usado
@@ -0,0 +1,18 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ Data model for the payroll module.
16
+ """
17
+
18
+ __version__ = "0.0.2"
@@ -0,0 +1,64 @@
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
+ """Vistas module for the payroll application."""
15
+
16
+ from coati_payroll.vistas.user import user_bp
17
+ from coati_payroll.vistas.currency import currency_bp
18
+ from coati_payroll.vistas.exchange_rate import exchange_rate_bp
19
+ from coati_payroll.vistas.employee import employee_bp
20
+ from coati_payroll.vistas.custom_field import custom_field_bp
21
+ from coati_payroll.vistas.calculation_rule import calculation_rule_bp
22
+ from coati_payroll.vistas.payroll_concepts import (
23
+ percepcion_bp,
24
+ deduccion_bp,
25
+ prestacion_bp,
26
+ )
27
+ from coati_payroll.vistas.planilla import planilla_bp
28
+ from coati_payroll.vistas.tipo_planilla import tipo_planilla_bp
29
+ from coati_payroll.vistas.prestamo import prestamo_bp
30
+ from coati_payroll.vistas.empresa import empresa_bp
31
+ from coati_payroll.vistas.configuracion import configuracion_bp
32
+ from coati_payroll.vistas.carga_inicial_prestacion import carga_inicial_prestacion_bp
33
+ from coati_payroll.vistas.vacation import vacation_bp
34
+ from coati_payroll.vistas.prestacion import prestacion_management_bp
35
+ from coati_payroll.vistas.report import report_bp
36
+ from coati_payroll.vistas.settings import settings_bp
37
+ from coati_payroll.vistas.config_calculos import config_calculos_bp
38
+ from coati_payroll.vistas.liquidacion import liquidacion_bp
39
+ from coati_payroll.vistas.plugins import plugins_bp
40
+
41
+ __all__ = [
42
+ "user_bp",
43
+ "currency_bp",
44
+ "exchange_rate_bp",
45
+ "employee_bp",
46
+ "custom_field_bp",
47
+ "calculation_rule_bp",
48
+ "percepcion_bp",
49
+ "deduccion_bp",
50
+ "prestacion_bp",
51
+ "planilla_bp",
52
+ "tipo_planilla_bp",
53
+ "prestamo_bp",
54
+ "empresa_bp",
55
+ "configuracion_bp",
56
+ "carga_inicial_prestacion_bp",
57
+ "vacation_bp",
58
+ "prestacion_management_bp",
59
+ "report_bp",
60
+ "settings_bp",
61
+ "plugins_bp",
62
+ "config_calculos_bp",
63
+ "liquidacion_bp",
64
+ ]