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