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,238 @@
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
+ """Routes for managing planilla associations."""
15
+
16
+ from datetime import date
17
+ from flask import flash, redirect, request, url_for
18
+ from flask_login import current_user
19
+
20
+ from coati_payroll.model import (
21
+ db,
22
+ Planilla,
23
+ Empleado,
24
+ PlanillaEmpleado,
25
+ PlanillaIngreso,
26
+ PlanillaDeduccion,
27
+ PlanillaPrestacion,
28
+ PlanillaReglaCalculo,
29
+ )
30
+ from coati_payroll.i18n import _
31
+ from coati_payroll.rbac import require_write_access
32
+ from coati_payroll.vistas.planilla import planilla_bp
33
+ from coati_payroll.vistas.planilla.helpers.association_helpers import agregar_asociacion
34
+ from coati_payroll.vistas.planilla.validators.planilla_validators import PlanillaValidator
35
+
36
+ # Constants
37
+ ROUTE_CONFIG_EMPLEADOS = "planilla.config_empleados"
38
+ ROUTE_CONFIG_PERCEPCIONES = "planilla.config_percepciones"
39
+ ROUTE_CONFIG_DEDUCCIONES = "planilla.config_deducciones"
40
+ ROUTE_CONFIG_PRESTACIONES = "planilla.config_prestaciones"
41
+ ROUTE_CONFIG_REGLAS = "planilla.config_reglas"
42
+ ERROR_NOT_FOUND = "no encontrada"
43
+
44
+
45
+ @planilla_bp.route("/<planilla_id>/empleado/add", methods=["POST"])
46
+ @require_write_access()
47
+ def add_empleado(planilla_id: str):
48
+ """Add an employee to the planilla."""
49
+ planilla = db.get_or_404(Planilla, planilla_id)
50
+ empleado_id = request.form.get("empleado_id")
51
+
52
+ if not empleado_id:
53
+ flash(_("Debe seleccionar un empleado."), "error")
54
+ return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
55
+
56
+ # Check if already exists
57
+ existing = db.session.execute(
58
+ db.select(PlanillaEmpleado).filter_by(planilla_id=planilla_id, empleado_id=empleado_id)
59
+ ).scalar_one_or_none()
60
+
61
+ if existing:
62
+ flash(_("El empleado ya está asignado a esta planilla."), "warning")
63
+ return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
64
+
65
+ # Validate that employee and planilla belong to the same company
66
+ empleado = db.get_or_404(Empleado, empleado_id)
67
+ is_valid, error_message = PlanillaValidator.validar_empresa_empleado(planilla, empleado)
68
+ if not is_valid:
69
+ flash(_(error_message), "error")
70
+ return redirect(url_for(ROUTE_CONFIG_EMPLEADOS, planilla_id=planilla_id))
71
+
72
+ association = PlanillaEmpleado(
73
+ planilla_id=planilla_id,
74
+ empleado_id=empleado_id,
75
+ fecha_inicio=date.today(),
76
+ activo=True,
77
+ creado_por=current_user.usuario,
78
+ )
79
+ db.session.add(association)
80
+ db.session.commit()
81
+ flash(_("Empleado agregado exitosamente."), "success")
82
+ return redirect(url_for("planilla.config_empleados", planilla_id=planilla_id))
83
+
84
+
85
+ @planilla_bp.route("/<planilla_id>/empleado/<association_id>/remove", methods=["POST"])
86
+ @require_write_access()
87
+ def remove_empleado(planilla_id: str, association_id: str):
88
+ """Remove an employee from the planilla."""
89
+ association = db.get_or_404(PlanillaEmpleado, association_id)
90
+ db.session.delete(association)
91
+ db.session.commit()
92
+ flash(_("Empleado removido exitosamente."), "success")
93
+ return redirect(url_for("planilla.config_empleados", planilla_id=planilla_id))
94
+
95
+
96
+ @planilla_bp.route("/<planilla_id>/percepcion/add", methods=["POST"])
97
+ @require_write_access()
98
+ def add_percepcion(planilla_id: str):
99
+ """Add a perception to the planilla."""
100
+ orden = request.form.get("orden", 0, type=int)
101
+ success, error_message, association_id = agregar_asociacion(
102
+ planilla_id=planilla_id,
103
+ tipo_componente="percepcion",
104
+ componente_id=request.form.get("percepcion_id"),
105
+ datos_extra={"orden": orden},
106
+ usuario=current_user.usuario,
107
+ )
108
+
109
+ if not success:
110
+ flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
111
+ return redirect(url_for(ROUTE_CONFIG_PERCEPCIONES, planilla_id=planilla_id))
112
+
113
+ flash(_("Percepción agregada exitosamente."), "success")
114
+ return redirect(url_for(ROUTE_CONFIG_PERCEPCIONES, planilla_id=planilla_id))
115
+
116
+
117
+ @planilla_bp.route("/<planilla_id>/percepcion/<association_id>/remove", methods=["POST"])
118
+ @require_write_access()
119
+ def remove_percepcion(planilla_id: str, association_id: str):
120
+ """Remove a perception from the planilla."""
121
+ association = db.get_or_404(PlanillaIngreso, association_id)
122
+ db.session.delete(association)
123
+ db.session.commit()
124
+ flash(_("Percepción removida exitosamente."), "success")
125
+ return redirect(url_for("planilla.config_percepciones", planilla_id=planilla_id))
126
+
127
+
128
+ @planilla_bp.route("/<planilla_id>/deduccion/add", methods=["POST"])
129
+ @require_write_access()
130
+ def add_deduccion(planilla_id: str):
131
+ """Add a deduction to the planilla with priority."""
132
+ prioridad = request.form.get("prioridad", 100, type=int)
133
+ es_obligatoria = request.form.get("es_obligatoria") == "on"
134
+ success, error_message, association_id = agregar_asociacion(
135
+ planilla_id=planilla_id,
136
+ tipo_componente="deduccion",
137
+ componente_id=request.form.get("deduccion_id"),
138
+ datos_extra={"prioridad": prioridad, "es_obligatoria": es_obligatoria},
139
+ usuario=current_user.usuario,
140
+ )
141
+
142
+ if not success:
143
+ flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
144
+ return redirect(url_for(ROUTE_CONFIG_DEDUCCIONES, planilla_id=planilla_id))
145
+
146
+ flash(_("Deducción agregada exitosamente."), "success")
147
+ return redirect(url_for(ROUTE_CONFIG_DEDUCCIONES, planilla_id=planilla_id))
148
+
149
+
150
+ @planilla_bp.route("/<planilla_id>/deduccion/<association_id>/remove", methods=["POST"])
151
+ @require_write_access()
152
+ def remove_deduccion(planilla_id: str, association_id: str):
153
+ """Remove a deduction from the planilla."""
154
+ association = db.get_or_404(PlanillaDeduccion, association_id)
155
+ db.session.delete(association)
156
+ db.session.commit()
157
+ flash(_("Deducción removida exitosamente."), "success")
158
+ return redirect(url_for("planilla.config_deducciones", planilla_id=planilla_id))
159
+
160
+
161
+ @planilla_bp.route("/<planilla_id>/deduccion/<association_id>/update-priority", methods=["POST"])
162
+ @require_write_access()
163
+ def update_deduccion_priority(planilla_id: str, association_id: str):
164
+ """Update the priority of a deduction."""
165
+ association = db.get_or_404(PlanillaDeduccion, association_id)
166
+
167
+ prioridad = request.form.get("prioridad", type=int)
168
+ if prioridad is not None:
169
+ association.prioridad = prioridad
170
+ association.modificado_por = current_user.usuario
171
+ db.session.commit()
172
+ flash(_("Prioridad actualizada."), "success")
173
+
174
+ return redirect(url_for("planilla.config_deducciones", planilla_id=planilla_id))
175
+
176
+
177
+ @planilla_bp.route("/<planilla_id>/prestacion/add", methods=["POST"])
178
+ @require_write_access()
179
+ def add_prestacion(planilla_id: str):
180
+ """Add a benefit (prestacion) to the planilla."""
181
+ orden = request.form.get("orden", 0, type=int)
182
+ success, error_message, association_id = agregar_asociacion(
183
+ planilla_id=planilla_id,
184
+ tipo_componente="prestacion",
185
+ componente_id=request.form.get("prestacion_id"),
186
+ datos_extra={"orden": orden},
187
+ usuario=current_user.usuario,
188
+ )
189
+
190
+ if not success:
191
+ flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
192
+ return redirect(url_for(ROUTE_CONFIG_PRESTACIONES, planilla_id=planilla_id))
193
+
194
+ flash(_("Prestación agregada exitosamente."), "success")
195
+ return redirect(url_for(ROUTE_CONFIG_PRESTACIONES, planilla_id=planilla_id))
196
+
197
+
198
+ @planilla_bp.route("/<planilla_id>/prestacion/<association_id>/remove", methods=["POST"])
199
+ @require_write_access()
200
+ def remove_prestacion(planilla_id: str, association_id: str):
201
+ """Remove a benefit from the planilla."""
202
+ association = db.get_or_404(PlanillaPrestacion, association_id)
203
+ db.session.delete(association)
204
+ db.session.commit()
205
+ flash(_("Prestación removida exitosamente."), "success")
206
+ return redirect(url_for("planilla.config_prestaciones", planilla_id=planilla_id))
207
+
208
+
209
+ @planilla_bp.route("/<planilla_id>/regla/add", methods=["POST"])
210
+ @require_write_access()
211
+ def add_regla(planilla_id: str):
212
+ """Add a calculation rule to the planilla."""
213
+ orden = request.form.get("orden", 0, type=int)
214
+ success, error_message, association_id = agregar_asociacion(
215
+ planilla_id=planilla_id,
216
+ tipo_componente="regla",
217
+ componente_id=request.form.get("regla_calculo_id"),
218
+ datos_extra={"orden": orden},
219
+ usuario=current_user.usuario,
220
+ )
221
+
222
+ if not success:
223
+ flash(_(error_message), "error" if ERROR_NOT_FOUND in error_message else "warning")
224
+ return redirect(url_for(ROUTE_CONFIG_REGLAS, planilla_id=planilla_id))
225
+
226
+ flash(_("Regla de cálculo agregada exitosamente."), "success")
227
+ return redirect(url_for(ROUTE_CONFIG_REGLAS, planilla_id=planilla_id))
228
+
229
+
230
+ @planilla_bp.route("/<planilla_id>/regla/<association_id>/remove", methods=["POST"])
231
+ @require_write_access()
232
+ def remove_regla(planilla_id: str, association_id: str):
233
+ """Remove a calculation rule from the planilla."""
234
+ association = db.get_or_404(PlanillaReglaCalculo, association_id)
235
+ db.session.delete(association)
236
+ db.session.commit()
237
+ flash(_("Regla de cálculo removida exitosamente."), "success")
238
+ return redirect(url_for("planilla.config_reglas", planilla_id=planilla_id))
@@ -0,0 +1,158 @@
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
+ """Configuration routes for planilla associations."""
15
+
16
+ from flask import render_template
17
+
18
+ from coati_payroll.model import (
19
+ db,
20
+ Planilla,
21
+ Empleado,
22
+ PlanillaEmpleado,
23
+ PlanillaIngreso,
24
+ PlanillaDeduccion,
25
+ PlanillaPrestacion,
26
+ PlanillaReglaCalculo,
27
+ Percepcion,
28
+ Deduccion,
29
+ Prestacion,
30
+ ReglaCalculo,
31
+ )
32
+ from coati_payroll.rbac import require_read_access, require_write_access
33
+ from coati_payroll.vistas.planilla import planilla_bp
34
+
35
+
36
+ @planilla_bp.route("/<planilla_id>/config/empleados")
37
+ @require_read_access()
38
+ def config_empleados(planilla_id: str):
39
+ """View employees associated with a planilla."""
40
+ planilla = db.get_or_404(Planilla, planilla_id)
41
+
42
+ empleados_asignados = (
43
+ db.session.execute(db.select(PlanillaEmpleado).filter_by(planilla_id=planilla_id)).scalars().all()
44
+ )
45
+
46
+ # Filter employees to only show those from the same company as the planilla
47
+ # CRITICAL: Only employees with matching empresa_id can be added to this planilla
48
+ query = db.select(Empleado).filter_by(activo=True)
49
+ if planilla.empresa_id:
50
+ # Only show employees from the same company (exact match required)
51
+ query = query.filter(Empleado.empresa_id == planilla.empresa_id)
52
+ else:
53
+ # If planilla has no company, show no employees (planilla must have empresa)
54
+ query = query.filter(db.false())
55
+ empleados_disponibles = db.session.execute(query.order_by(Empleado.primer_apellido)).scalars().all()
56
+
57
+ return render_template(
58
+ "modules/planilla/config_empleados.html",
59
+ planilla=planilla,
60
+ empleados_asignados=empleados_asignados,
61
+ empleados_disponibles=empleados_disponibles,
62
+ )
63
+
64
+
65
+ @planilla_bp.route("/<planilla_id>/config/percepciones")
66
+ @require_read_access()
67
+ def config_percepciones(planilla_id: str):
68
+ """View perceptions associated with a planilla."""
69
+ planilla = db.get_or_404(Planilla, planilla_id)
70
+
71
+ percepciones_asignadas = (
72
+ db.session.execute(db.select(PlanillaIngreso).filter_by(planilla_id=planilla_id)).scalars().all()
73
+ )
74
+
75
+ percepciones_disponibles = (
76
+ db.session.execute(db.select(Percepcion).filter_by(activo=True).order_by(Percepcion.nombre)).scalars().all()
77
+ )
78
+
79
+ return render_template(
80
+ "modules/planilla/config_percepciones.html",
81
+ planilla=planilla,
82
+ percepciones_asignadas=percepciones_asignadas,
83
+ percepciones_disponibles=percepciones_disponibles,
84
+ )
85
+
86
+
87
+ @planilla_bp.route("/<planilla_id>/config/deducciones")
88
+ @require_read_access()
89
+ def config_deducciones(planilla_id: str):
90
+ """View deductions associated with a planilla."""
91
+ planilla = db.get_or_404(Planilla, planilla_id)
92
+
93
+ deducciones_asignadas = (
94
+ db.session.execute(
95
+ db.select(PlanillaDeduccion).filter_by(planilla_id=planilla_id).order_by(PlanillaDeduccion.prioridad)
96
+ )
97
+ .scalars()
98
+ .all()
99
+ )
100
+
101
+ deducciones_disponibles = (
102
+ db.session.execute(db.select(Deduccion).filter_by(activo=True).order_by(Deduccion.nombre)).scalars().all()
103
+ )
104
+
105
+ return render_template(
106
+ "modules/planilla/config_deducciones.html",
107
+ planilla=planilla,
108
+ deducciones_asignadas=deducciones_asignadas,
109
+ deducciones_disponibles=deducciones_disponibles,
110
+ )
111
+
112
+
113
+ @planilla_bp.route("/<planilla_id>/config/prestaciones")
114
+ @require_write_access()
115
+ def config_prestaciones(planilla_id: str):
116
+ """Manage benefits associated with a planilla."""
117
+ planilla = db.get_or_404(Planilla, planilla_id)
118
+
119
+ prestaciones_asignadas = (
120
+ db.session.execute(db.select(PlanillaPrestacion).filter_by(planilla_id=planilla_id)).scalars().all()
121
+ )
122
+
123
+ prestaciones_disponibles = (
124
+ db.session.execute(db.select(Prestacion).filter_by(activo=True).order_by(Prestacion.nombre)).scalars().all()
125
+ )
126
+
127
+ return render_template(
128
+ "modules/planilla/config_prestaciones.html",
129
+ planilla=planilla,
130
+ prestaciones_asignadas=prestaciones_asignadas,
131
+ prestaciones_disponibles=prestaciones_disponibles,
132
+ )
133
+
134
+
135
+ @planilla_bp.route("/<planilla_id>/config/reglas")
136
+ @require_read_access()
137
+ def config_reglas(planilla_id: str):
138
+ """View calculation rules associated with a planilla."""
139
+ planilla = db.get_or_404(Planilla, planilla_id)
140
+
141
+ reglas_asignadas = (
142
+ db.session.execute(
143
+ db.select(PlanillaReglaCalculo).filter_by(planilla_id=planilla_id).order_by(PlanillaReglaCalculo.orden)
144
+ )
145
+ .scalars()
146
+ .all()
147
+ )
148
+
149
+ reglas_disponibles = (
150
+ db.session.execute(db.select(ReglaCalculo).filter_by(activo=True).order_by(ReglaCalculo.nombre)).scalars().all()
151
+ )
152
+
153
+ return render_template(
154
+ "modules/planilla/config_reglas.html",
155
+ planilla=planilla,
156
+ reglas_asignadas=reglas_asignadas,
157
+ reglas_disponibles=reglas_disponibles,
158
+ )
@@ -0,0 +1,175 @@
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
+ """Routes for Excel export operations."""
15
+
16
+ from flask import flash, redirect, send_file, url_for
17
+ from flask_login import login_required
18
+
19
+ from coati_payroll.model import db, Planilla, Nomina
20
+ from coati_payroll.i18n import _
21
+ from coati_payroll.rbac import require_read_access
22
+ from coati_payroll.vistas.planilla import planilla_bp
23
+ from coati_payroll.vistas.planilla.helpers import check_openpyxl_available
24
+ from coati_payroll.vistas.planilla.services import ExportService
25
+
26
+ # Constants
27
+ ROUTE_VER_NOMINA = "planilla.ver_nomina"
28
+
29
+
30
+ @planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-excel")
31
+ @login_required
32
+ @require_read_access()
33
+ def exportar_nomina_excel(planilla_id: str, nomina_id: str):
34
+ """Export nomina to Excel with employee details and calculations."""
35
+ if not check_openpyxl_available():
36
+ flash(_("Excel export no disponible. Instale openpyxl."), "warning")
37
+ return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
38
+
39
+ planilla = db.get_or_404(Planilla, planilla_id)
40
+ nomina = db.get_or_404(Nomina, nomina_id)
41
+
42
+ if nomina.planilla_id != planilla_id:
43
+ flash(_("La nómina no pertenece a esta planilla."), "error")
44
+ return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
45
+
46
+ try:
47
+ output, filename = ExportService.exportar_nomina_excel(planilla, nomina)
48
+ return send_file(
49
+ output,
50
+ mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
51
+ as_attachment=True,
52
+ download_name=filename,
53
+ )
54
+ except Exception as e:
55
+ flash(_("Error al exportar nómina: {}").format(str(e)), "error")
56
+ return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
57
+
58
+
59
+ @planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-prestaciones-excel")
60
+ @login_required
61
+ @require_read_access()
62
+ def exportar_prestaciones_excel(planilla_id: str, nomina_id: str):
63
+ """Export benefits (prestaciones) to Excel separately."""
64
+ if not check_openpyxl_available():
65
+ flash(_("Excel export no disponible. Instale openpyxl."), "warning")
66
+ return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
67
+
68
+ planilla = db.get_or_404(Planilla, planilla_id)
69
+ nomina = db.get_or_404(Nomina, nomina_id)
70
+
71
+ if nomina.planilla_id != planilla_id:
72
+ flash(_("La nómina no pertenece a esta planilla."), "error")
73
+ return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
74
+
75
+ try:
76
+ output, filename = ExportService.exportar_prestaciones_excel(planilla, nomina)
77
+ return send_file(
78
+ output,
79
+ mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
80
+ as_attachment=True,
81
+ download_name=filename,
82
+ )
83
+ except Exception as e:
84
+ flash(_("Error al exportar prestaciones: {}").format(str(e)), "error")
85
+ return redirect(url_for(ROUTE_VER_NOMINA, planilla_id=planilla_id, nomina_id=nomina_id))
86
+
87
+
88
+ @planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-comprobante-excel")
89
+ @login_required
90
+ @require_read_access()
91
+ def exportar_comprobante_excel(planilla_id: str, nomina_id: str):
92
+ """Export summarized accounting voucher (comprobante contable) to Excel."""
93
+ if not check_openpyxl_available():
94
+ flash(_("Excel export no disponible. Instale openpyxl."), "warning")
95
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
96
+
97
+ planilla = db.get_or_404(Planilla, planilla_id)
98
+ nomina = db.get_or_404(Nomina, nomina_id)
99
+
100
+ if nomina.planilla_id != planilla_id:
101
+ flash(_("La nómina no pertenece a esta planilla."), "error")
102
+ return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
103
+
104
+ # Check if comprobante exists
105
+ from coati_payroll.model import ComprobanteContable
106
+
107
+ comprobante = db.session.execute(db.select(ComprobanteContable).filter_by(nomina_id=nomina_id)).scalar_one_or_none()
108
+
109
+ if not comprobante:
110
+ flash(_("No existe comprobante contable para esta nómina. Debe recalcular la nómina."), "error")
111
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
112
+
113
+ # Check for configuration warnings
114
+ if comprobante.advertencias:
115
+ flash(
116
+ _("ADVERTENCIA: La configuración contable está incompleta. Revise las advertencias en el log."),
117
+ "warning",
118
+ )
119
+
120
+ try:
121
+ output, filename = ExportService.exportar_comprobante_excel(planilla, nomina)
122
+ return send_file(
123
+ output,
124
+ mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
125
+ as_attachment=True,
126
+ download_name=filename,
127
+ )
128
+ except Exception as e:
129
+ flash(_("Error al exportar comprobante: {}").format(str(e)), "error")
130
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
131
+
132
+
133
+ @planilla_bp.route("/<planilla_id>/nomina/<nomina_id>/exportar-comprobante-detallado-excel")
134
+ @login_required
135
+ @require_read_access()
136
+ def exportar_comprobante_detallado_excel(planilla_id: str, nomina_id: str):
137
+ """Export detailed accounting voucher per employee to Excel."""
138
+ if not check_openpyxl_available():
139
+ flash(_("Excel export no disponible. Instale openpyxl."), "warning")
140
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
141
+
142
+ planilla = db.get_or_404(Planilla, planilla_id)
143
+ nomina = db.get_or_404(Nomina, nomina_id)
144
+
145
+ if nomina.planilla_id != planilla_id:
146
+ flash(_("La nómina no pertenece a esta planilla."), "error")
147
+ return redirect(url_for("planilla.listar_nominas", planilla_id=planilla_id))
148
+
149
+ # Check if comprobante exists
150
+ from coati_payroll.model import ComprobanteContable
151
+
152
+ comprobante = db.session.execute(db.select(ComprobanteContable).filter_by(nomina_id=nomina_id)).scalar_one_or_none()
153
+
154
+ if not comprobante:
155
+ flash(_("No existe comprobante contable para esta nómina. Debe recalcular la nómina."), "error")
156
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
157
+
158
+ # Check for configuration warnings
159
+ if comprobante.advertencias:
160
+ flash(
161
+ _("ADVERTENCIA: La configuración contable está incompleta. Revise las advertencias en el log."),
162
+ "warning",
163
+ )
164
+
165
+ try:
166
+ output, filename = ExportService.exportar_comprobante_detallado_excel(planilla, nomina)
167
+ return send_file(
168
+ output,
169
+ mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
170
+ as_attachment=True,
171
+ download_name=filename,
172
+ )
173
+ except Exception as e:
174
+ flash(_("Error al exportar comprobante detallado: {}").format(str(e)), "error")
175
+ return redirect(url_for("planilla.ver_nomina", planilla_id=planilla_id, nomina_id=nomina_id))
@@ -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
+ """Helper functions for planilla views."""
15
+
16
+ from coati_payroll.vistas.planilla.helpers.form_helpers import (
17
+ populate_form_choices,
18
+ populate_novedad_form_choices,
19
+ get_concepto_ids_from_form,
20
+ )
21
+ from coati_payroll.vistas.planilla.helpers.excel_helpers import check_openpyxl_available
22
+ from coati_payroll.vistas.planilla.helpers.association_helpers import (
23
+ agregar_asociacion,
24
+ get_planilla_component_counts,
25
+ )
26
+
27
+ __all__ = [
28
+ "populate_form_choices",
29
+ "populate_novedad_form_choices",
30
+ "get_concepto_ids_from_form",
31
+ "check_openpyxl_available",
32
+ "agregar_asociacion",
33
+ "get_planilla_component_counts",
34
+ ]