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,112 @@
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
+ """Currency CRUD routes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
19
+ from flask_login import current_user
20
+
21
+ from coati_payroll.forms import CurrencyForm
22
+ from coati_payroll.i18n import _
23
+ from coati_payroll.rbac import require_read_access, require_write_access
24
+ from coati_payroll.model import Moneda, db
25
+ from coati_payroll.vistas.constants import PER_PAGE
26
+
27
+ currency_bp = Blueprint("currency", __name__, url_prefix="/currency")
28
+
29
+
30
+ @currency_bp.route("/")
31
+ @require_read_access()
32
+ def index():
33
+ """List all currencies with pagination."""
34
+ page = request.args.get("page", 1, type=int)
35
+ pagination = db.paginate(
36
+ db.select(Moneda).order_by(Moneda.codigo),
37
+ page=page,
38
+ per_page=PER_PAGE,
39
+ error_out=False,
40
+ )
41
+ return render_template(
42
+ "modules/currency/index.html",
43
+ currencies=pagination.items,
44
+ pagination=pagination,
45
+ )
46
+
47
+
48
+ @currency_bp.route("/new", methods=["GET", "POST"])
49
+ @require_write_access()
50
+ def new():
51
+ """Create a new currency."""
52
+ form = CurrencyForm()
53
+
54
+ if form.validate_on_submit():
55
+ currency = Moneda()
56
+ currency.codigo = form.codigo.data
57
+ currency.nombre = form.nombre.data
58
+ currency.simbolo = form.simbolo.data
59
+ currency.activo = form.activo.data
60
+ currency.creado_por = current_user.usuario
61
+
62
+ db.session.add(currency)
63
+ db.session.commit()
64
+ flash(_("Moneda creada exitosamente."), "success")
65
+ return redirect(url_for("currency.index"))
66
+
67
+ return render_template("modules/currency/form.html", form=form, title=_("Nueva Moneda"))
68
+
69
+
70
+ @currency_bp.route("/edit/<string:id>", methods=["GET", "POST"])
71
+ @require_write_access()
72
+ def edit(id: str):
73
+ """Edit an existing currency."""
74
+ currency = db.session.get(Moneda, id)
75
+ if not currency:
76
+ flash(_("Moneda no encontrada."), "error")
77
+ return redirect(url_for("currency.index"))
78
+
79
+ form = CurrencyForm(obj=currency)
80
+
81
+ if form.validate_on_submit():
82
+ currency.codigo = form.codigo.data
83
+ currency.nombre = form.nombre.data
84
+ currency.simbolo = form.simbolo.data
85
+ currency.activo = form.activo.data
86
+ currency.modificado_por = current_user.usuario
87
+
88
+ db.session.commit()
89
+ flash(_("Moneda actualizada exitosamente."), "success")
90
+ return redirect(url_for("currency.index"))
91
+
92
+ return render_template(
93
+ "modules/currency/form.html",
94
+ form=form,
95
+ title=_("Editar Moneda"),
96
+ currency=currency,
97
+ )
98
+
99
+
100
+ @currency_bp.route("/delete/<string:id>", methods=["POST"])
101
+ @require_write_access()
102
+ def delete(id: str):
103
+ """Delete a currency."""
104
+ currency = db.session.get(Moneda, id)
105
+ if not currency:
106
+ flash(_("Moneda no encontrada."), "error")
107
+ return redirect(url_for("currency.index"))
108
+
109
+ db.session.delete(currency)
110
+ db.session.commit()
111
+ flash(_("Moneda eliminada exitosamente."), "success")
112
+ return redirect(url_for("currency.index"))
@@ -0,0 +1,120 @@
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
+ """Custom employee field CRUD routes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
19
+ from flask_login import current_user
20
+
21
+ from coati_payroll.forms import CustomFieldForm
22
+ from coati_payroll.i18n import _
23
+ from coati_payroll.rbac import require_read_access, require_write_access
24
+ from coati_payroll.model import CampoPersonalizado, db
25
+ from coati_payroll.vistas.constants import PER_PAGE
26
+
27
+ custom_field_bp = Blueprint("custom_field", __name__, url_prefix="/custom_field")
28
+
29
+
30
+ @custom_field_bp.route("/")
31
+ @require_read_access()
32
+ def index():
33
+ """List all custom fields with pagination."""
34
+ page = request.args.get("page", 1, type=int)
35
+ pagination = db.paginate(
36
+ db.select(CampoPersonalizado).order_by(CampoPersonalizado.orden),
37
+ page=page,
38
+ per_page=PER_PAGE,
39
+ error_out=False,
40
+ )
41
+ return render_template(
42
+ "modules/custom_field/index.html",
43
+ custom_fields=pagination.items,
44
+ pagination=pagination,
45
+ )
46
+
47
+
48
+ @custom_field_bp.route("/new", methods=["GET", "POST"])
49
+ @require_write_access()
50
+ def new():
51
+ """Create a new custom field."""
52
+ form = CustomFieldForm()
53
+
54
+ if form.validate_on_submit():
55
+ custom_field = CampoPersonalizado()
56
+ custom_field.nombre_campo = form.nombre_campo.data
57
+ custom_field.etiqueta = form.etiqueta.data
58
+ custom_field.tipo_dato = form.tipo_dato.data
59
+ custom_field.descripcion = form.descripcion.data
60
+ custom_field.orden = int(form.orden.data or 0)
61
+ custom_field.activo = form.activo.data
62
+ custom_field.creado_por = current_user.usuario
63
+
64
+ db.session.add(custom_field)
65
+ db.session.commit()
66
+ flash(_("Campo personalizado creado exitosamente."), "success")
67
+ return redirect(url_for("custom_field.index"))
68
+
69
+ return render_template(
70
+ "modules/custom_field/form.html",
71
+ form=form,
72
+ title=_("Nuevo Campo Personalizado"),
73
+ )
74
+
75
+
76
+ @custom_field_bp.route("/edit/<string:id>", methods=["GET", "POST"])
77
+ @require_write_access()
78
+ def edit(id: str):
79
+ """Edit an existing custom field."""
80
+ custom_field = db.session.get(CampoPersonalizado, id)
81
+ if not custom_field:
82
+ flash(_("Campo personalizado no encontrado."), "error")
83
+ return redirect(url_for("custom_field.index"))
84
+
85
+ form = CustomFieldForm(obj=custom_field)
86
+
87
+ if form.validate_on_submit():
88
+ custom_field.nombre_campo = form.nombre_campo.data
89
+ custom_field.etiqueta = form.etiqueta.data
90
+ custom_field.tipo_dato = form.tipo_dato.data
91
+ custom_field.descripcion = form.descripcion.data
92
+ custom_field.orden = int(form.orden.data or 0)
93
+ custom_field.activo = form.activo.data
94
+ custom_field.modificado_por = current_user.usuario
95
+
96
+ db.session.commit()
97
+ flash(_("Campo personalizado actualizado exitosamente."), "success")
98
+ return redirect(url_for("custom_field.index"))
99
+
100
+ return render_template(
101
+ "modules/custom_field/form.html",
102
+ form=form,
103
+ title=_("Editar Campo Personalizado"),
104
+ custom_field=custom_field,
105
+ )
106
+
107
+
108
+ @custom_field_bp.route("/delete/<string:id>", methods=["POST"])
109
+ @require_write_access()
110
+ def delete(id: str):
111
+ """Delete a custom field."""
112
+ custom_field = db.session.get(CampoPersonalizado, id)
113
+ if not custom_field:
114
+ flash(_("Campo personalizado no encontrado."), "error")
115
+ return redirect(url_for("custom_field.index"))
116
+
117
+ db.session.delete(custom_field)
118
+ db.session.commit()
119
+ flash(_("Campo personalizado eliminado exitosamente."), "success")
120
+ return redirect(url_for("custom_field.index"))
@@ -0,0 +1,305 @@
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
+ """Employee CRUD routes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import date
19
+ from decimal import Decimal
20
+
21
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
22
+ from flask_login import current_user
23
+
24
+ from coati_payroll.forms import EmployeeForm
25
+ from coati_payroll.i18n import _
26
+ from coati_payroll.model import CampoPersonalizado, Empleado, Moneda, db
27
+ from coati_payroll.rbac import require_read_access, require_write_access
28
+ from coati_payroll.vistas.constants import PER_PAGE
29
+
30
+ employee_bp = Blueprint("employee", __name__, url_prefix="/employee")
31
+
32
+
33
+ def get_currency_choices():
34
+ """Get list of currencies for select fields."""
35
+ currencies = db.session.execute(db.select(Moneda).filter_by(activo=True).order_by(Moneda.codigo)).scalars().all()
36
+ return [("", _("Seleccionar..."))] + [(c.id, f"{c.codigo} - {c.nombre}") for c in currencies]
37
+
38
+
39
+ def get_empresa_choices():
40
+ """Get list of companies for select fields."""
41
+ from coati_payroll.model import Empresa
42
+
43
+ empresas = (
44
+ db.session.execute(db.select(Empresa).filter_by(activo=True).order_by(Empresa.razon_social)).scalars().all()
45
+ )
46
+ return [("", _("Seleccionar..."))] + [(e.id, f"{e.codigo} - {e.razon_social}") for e in empresas]
47
+
48
+
49
+ def get_custom_fields():
50
+ """Get all active custom fields ordered by 'orden'."""
51
+ return (
52
+ db.session.execute(db.select(CampoPersonalizado).filter_by(activo=True).order_by(CampoPersonalizado.orden))
53
+ .scalars()
54
+ .all()
55
+ )
56
+
57
+
58
+ def process_custom_fields_from_request(custom_fields):
59
+ """Process custom field values from form request and return as dict.
60
+
61
+ Args:
62
+ custom_fields: List of CampoPersonalizado objects
63
+
64
+ Returns:
65
+ Dictionary with custom field names as keys and their converted values
66
+ """
67
+ datos_adicionales = {}
68
+ for field in custom_fields:
69
+ field_name = f"custom_{field.nombre_campo}"
70
+ raw_value = request.form.get(field_name, "")
71
+
72
+ match field.tipo_dato:
73
+ case "texto":
74
+ stripped = raw_value.strip() if raw_value else ""
75
+ datos_adicionales[field.nombre_campo] = stripped or None
76
+ case "entero":
77
+ try:
78
+ datos_adicionales[field.nombre_campo] = int(raw_value) if raw_value else None
79
+ except ValueError:
80
+ datos_adicionales[field.nombre_campo] = None
81
+ case "decimal":
82
+ try:
83
+ datos_adicionales[field.nombre_campo] = float(raw_value) if raw_value else None
84
+ except ValueError:
85
+ datos_adicionales[field.nombre_campo] = None
86
+ case "booleano":
87
+ # Checkbox will send value only if checked
88
+ datos_adicionales[field.nombre_campo] = field_name in request.form
89
+ case _:
90
+ # Unknown type, store as text
91
+ datos_adicionales[field.nombre_campo] = raw_value or None
92
+ return datos_adicionales
93
+
94
+
95
+ def process_last_three_salaries(form):
96
+ """Process last three salary fields from form and return as dict.
97
+
98
+ Stores salaries as strings to preserve Decimal precision in JSON.
99
+
100
+ Args:
101
+ form: EmployeeForm instance with salary fields
102
+
103
+ Returns:
104
+ Dictionary with last three salaries as strings, or None if empty
105
+ """
106
+ ultimos_salarios = {}
107
+ if form.ultimo_salario_1.data:
108
+ ultimos_salarios["salario_1"] = str(form.ultimo_salario_1.data)
109
+ if form.ultimo_salario_2.data:
110
+ ultimos_salarios["salario_2"] = str(form.ultimo_salario_2.data)
111
+ if form.ultimo_salario_3.data:
112
+ ultimos_salarios["salario_3"] = str(form.ultimo_salario_3.data)
113
+ return ultimos_salarios if ultimos_salarios else None
114
+
115
+
116
+ @employee_bp.route("/")
117
+ @require_read_access()
118
+ def index():
119
+ """List all employees with pagination."""
120
+ page = request.args.get("page", 1, type=int)
121
+ pagination = db.paginate(
122
+ db.select(Empleado).order_by(Empleado.primer_apellido, Empleado.primer_nombre),
123
+ page=page,
124
+ per_page=PER_PAGE,
125
+ error_out=False,
126
+ )
127
+ return render_template("modules/employee/index.html", employees=pagination.items, pagination=pagination)
128
+
129
+
130
+ @employee_bp.route("/new", methods=["GET", "POST"])
131
+ @require_write_access()
132
+ def new():
133
+ """Create a new employee. Admin and HR can create employees."""
134
+ form = EmployeeForm()
135
+ form.moneda_id.choices = get_currency_choices()
136
+ form.empresa_id.choices = get_empresa_choices()
137
+ custom_fields = get_custom_fields()
138
+
139
+ if form.validate_on_submit():
140
+ employee = Empleado()
141
+ # Set codigo_empleado only if provided (otherwise default will be used)
142
+ if form.codigo_empleado.data and form.codigo_empleado.data.strip():
143
+ employee.codigo_empleado = form.codigo_empleado.data.strip()
144
+ employee.primer_nombre = form.primer_nombre.data
145
+ employee.segundo_nombre = form.segundo_nombre.data
146
+ employee.primer_apellido = form.primer_apellido.data
147
+ employee.segundo_apellido = form.segundo_apellido.data
148
+ employee.genero = form.genero.data or None
149
+ employee.nacionalidad = form.nacionalidad.data
150
+ employee.tipo_identificacion = form.tipo_identificacion.data or None
151
+ employee.identificacion_personal = form.identificacion_personal.data
152
+ employee.id_seguridad_social = form.id_seguridad_social.data or None
153
+ employee.id_fiscal = form.id_fiscal.data or None
154
+ employee.tipo_sangre = form.tipo_sangre.data or None
155
+ employee.fecha_nacimiento = form.fecha_nacimiento.data
156
+ employee.fecha_alta = form.fecha_alta.data
157
+ employee.fecha_baja = form.fecha_baja.data
158
+ employee.activo = form.activo.data
159
+ employee.cargo = form.cargo.data
160
+ employee.area = form.area.data
161
+ employee.centro_costos = form.centro_costos.data
162
+ employee.salario_base = form.salario_base.data or Decimal("0.00")
163
+ employee.moneda_id = form.moneda_id.data or None
164
+ employee.empresa_id = form.empresa_id.data or None
165
+ employee.correo = form.correo.data
166
+ employee.telefono = form.telefono.data
167
+ employee.direccion = form.direccion.data
168
+ employee.estado_civil = form.estado_civil.data or None
169
+ employee.banco = form.banco.data
170
+ employee.numero_cuenta_bancaria = form.numero_cuenta_bancaria.data
171
+ employee.tipo_contrato = form.tipo_contrato.data or None
172
+ employee.creado_por = current_user.usuario
173
+
174
+ # Initial implementation data
175
+ employee.anio_implementacion_inicial = form.anio_implementacion_inicial.data
176
+ employee.mes_ultimo_cierre = form.mes_ultimo_cierre.data
177
+ employee.salario_acumulado = form.salario_acumulado.data or Decimal("0.00")
178
+ employee.impuesto_acumulado = form.impuesto_acumulado.data or Decimal("0.00")
179
+
180
+ # Store last three salaries in JSON format using helper function
181
+ employee.ultimos_tres_salarios = process_last_three_salaries(form)
182
+
183
+ # Process custom fields
184
+ employee.datos_adicionales = process_custom_fields_from_request(custom_fields)
185
+
186
+ db.session.add(employee)
187
+ db.session.commit()
188
+ flash(_("Empleado creado exitosamente."), "success")
189
+ return redirect(url_for("employee.index"))
190
+
191
+ # Default date to today
192
+ if not form.fecha_alta.data:
193
+ form.fecha_alta.data = date.today()
194
+ if not form.salario_base.data:
195
+ form.salario_base.data = Decimal("0.00")
196
+
197
+ return render_template(
198
+ "modules/employee/form.html",
199
+ form=form,
200
+ title=_("Nuevo Empleado"),
201
+ custom_fields=custom_fields,
202
+ custom_values={},
203
+ )
204
+
205
+
206
+ @employee_bp.route("/edit/<string:id>", methods=["GET", "POST"])
207
+ @require_write_access()
208
+ def edit(id: str):
209
+ """Edit an existing employee. Admin and HR can edit employees."""
210
+ employee = db.session.get(Empleado, id)
211
+ if not employee:
212
+ flash(_("Empleado no encontrado."), "error")
213
+ return redirect(url_for("employee.index"))
214
+
215
+ form = EmployeeForm(obj=employee)
216
+ form.moneda_id.choices = get_currency_choices()
217
+ form.empresa_id.choices = get_empresa_choices()
218
+ custom_fields = get_custom_fields()
219
+
220
+ if form.validate_on_submit():
221
+ # Update codigo_empleado only if provided
222
+ if form.codigo_empleado.data and form.codigo_empleado.data.strip():
223
+ employee.codigo_empleado = form.codigo_empleado.data.strip()
224
+ employee.primer_nombre = form.primer_nombre.data
225
+ employee.segundo_nombre = form.segundo_nombre.data
226
+ employee.primer_apellido = form.primer_apellido.data
227
+ employee.segundo_apellido = form.segundo_apellido.data
228
+ employee.genero = form.genero.data or None
229
+ employee.nacionalidad = form.nacionalidad.data
230
+ employee.tipo_identificacion = form.tipo_identificacion.data or None
231
+ employee.identificacion_personal = form.identificacion_personal.data
232
+ employee.id_seguridad_social = form.id_seguridad_social.data or None
233
+ employee.id_fiscal = form.id_fiscal.data or None
234
+ employee.tipo_sangre = form.tipo_sangre.data or None
235
+ employee.fecha_nacimiento = form.fecha_nacimiento.data
236
+ employee.fecha_alta = form.fecha_alta.data
237
+ employee.fecha_baja = form.fecha_baja.data
238
+ employee.activo = form.activo.data
239
+ employee.cargo = form.cargo.data
240
+ employee.area = form.area.data
241
+ employee.centro_costos = form.centro_costos.data
242
+ employee.salario_base = form.salario_base.data or Decimal("0.00")
243
+ employee.moneda_id = form.moneda_id.data or None
244
+ employee.empresa_id = form.empresa_id.data or None
245
+ employee.correo = form.correo.data
246
+ employee.telefono = form.telefono.data
247
+ employee.direccion = form.direccion.data
248
+ employee.estado_civil = form.estado_civil.data or None
249
+ employee.banco = form.banco.data
250
+ employee.numero_cuenta_bancaria = form.numero_cuenta_bancaria.data
251
+ employee.tipo_contrato = form.tipo_contrato.data or None
252
+ employee.modificado_por = current_user.usuario
253
+
254
+ # Initial implementation data
255
+ employee.anio_implementacion_inicial = form.anio_implementacion_inicial.data
256
+ employee.mes_ultimo_cierre = form.mes_ultimo_cierre.data
257
+ employee.salario_acumulado = form.salario_acumulado.data or Decimal("0.00")
258
+ employee.impuesto_acumulado = form.impuesto_acumulado.data or Decimal("0.00")
259
+
260
+ # Store last three salaries in JSON format using helper function
261
+ employee.ultimos_tres_salarios = process_last_three_salaries(form)
262
+
263
+ # Process custom fields
264
+ employee.datos_adicionales = process_custom_fields_from_request(custom_fields)
265
+
266
+ db.session.commit()
267
+ flash(_("Empleado actualizado exitosamente."), "success")
268
+ return redirect(url_for("employee.index"))
269
+
270
+ # Pre-populate last three salaries from employee data
271
+ if request.method != "POST":
272
+ ultimos_salarios = employee.ultimos_tres_salarios or {}
273
+ if ultimos_salarios.get("salario_1"):
274
+ form.ultimo_salario_1.data = Decimal(str(ultimos_salarios["salario_1"]))
275
+ if ultimos_salarios.get("salario_2"):
276
+ form.ultimo_salario_2.data = Decimal(str(ultimos_salarios["salario_2"]))
277
+ if ultimos_salarios.get("salario_3"):
278
+ form.ultimo_salario_3.data = Decimal(str(ultimos_salarios["salario_3"]))
279
+
280
+ # Get existing custom field values
281
+ custom_values = employee.datos_adicionales or {}
282
+
283
+ return render_template(
284
+ "modules/employee/form.html",
285
+ form=form,
286
+ title=_("Editar Empleado"),
287
+ employee=employee,
288
+ custom_fields=custom_fields,
289
+ custom_values=custom_values,
290
+ )
291
+
292
+
293
+ @employee_bp.route("/delete/<string:id>", methods=["POST"])
294
+ @require_write_access()
295
+ def delete(id: str):
296
+ """Delete an employee. Admin and HR can delete employees."""
297
+ employee = db.session.get(Empleado, id)
298
+ if not employee:
299
+ flash(_("Empleado no encontrado."), "error")
300
+ return redirect(url_for("employee.index"))
301
+
302
+ db.session.delete(employee)
303
+ db.session.commit()
304
+ flash(_("Empleado eliminado exitosamente."), "success")
305
+ return redirect(url_for("employee.index"))
@@ -0,0 +1,153 @@
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
+ """Empresa (Company) views module."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from flask import Blueprint, flash, redirect, render_template, request, url_for
19
+ from flask_login import current_user
20
+
21
+ from coati_payroll.enums import TipoUsuario
22
+ from coati_payroll.i18n import _
23
+ from coati_payroll.model import Empresa, db
24
+ from coati_payroll.rbac import require_role, require_read_access
25
+
26
+ empresa_bp = Blueprint("empresa", __name__, url_prefix="/empresa")
27
+
28
+
29
+ @empresa_bp.route("/")
30
+ @require_read_access()
31
+ def index():
32
+ """List all companies."""
33
+ page = request.args.get("page", 1, type=int)
34
+ per_page = 20
35
+
36
+ query = db.select(Empresa).order_by(Empresa.razon_social)
37
+ pagination = db.paginate(query, page=page, per_page=per_page, error_out=False)
38
+ empresas = pagination.items
39
+
40
+ return render_template(
41
+ "modules/empresa/index.html",
42
+ empresas=empresas,
43
+ pagination=pagination,
44
+ )
45
+
46
+
47
+ @empresa_bp.route("/new", methods=["GET", "POST"])
48
+ @require_role(TipoUsuario.ADMIN)
49
+ def new():
50
+ """Create a new company. Only administrators can create companies."""
51
+ from coati_payroll.forms import EmpresaForm
52
+
53
+ form = EmpresaForm()
54
+
55
+ if form.validate_on_submit():
56
+ empresa = Empresa()
57
+ form.populate_obj(empresa)
58
+ empresa.creado_por = current_user.usuario
59
+
60
+ db.session.add(empresa)
61
+ try:
62
+ db.session.commit()
63
+ flash(_("Empresa creada exitosamente."), "success")
64
+ return redirect(url_for("empresa.index"))
65
+ except Exception as e:
66
+ db.session.rollback()
67
+ flash(_("Error al crear la empresa: {}").format(str(e)), "danger")
68
+
69
+ return render_template("modules/empresa/form.html", form=form, titulo=_("Nueva Empresa"))
70
+
71
+
72
+ @empresa_bp.route("/<string:empresa_id>/edit", methods=["GET", "POST"])
73
+ @require_role(TipoUsuario.ADMIN)
74
+ def edit(empresa_id):
75
+ """Edit an existing company. Only administrators can edit companies."""
76
+ from coati_payroll.forms import EmpresaForm
77
+
78
+ empresa = db.session.get(Empresa, empresa_id)
79
+ if not empresa:
80
+ flash(_("Empresa no encontrada."), "warning")
81
+ return redirect(url_for("empresa.index"))
82
+
83
+ form = EmpresaForm(obj=empresa)
84
+
85
+ if form.validate_on_submit():
86
+ form.populate_obj(empresa)
87
+ empresa.modificado_por = current_user.usuario
88
+
89
+ try:
90
+ db.session.commit()
91
+ flash(_("Empresa actualizada exitosamente."), "success")
92
+ return redirect(url_for("empresa.index"))
93
+ except Exception as e:
94
+ db.session.rollback()
95
+ flash(_("Error al actualizar la empresa: {}").format(str(e)), "danger")
96
+
97
+ return render_template(
98
+ "modules/empresa/form.html",
99
+ form=form,
100
+ empresa=empresa,
101
+ titulo=_("Editar Empresa"),
102
+ )
103
+
104
+
105
+ @empresa_bp.route("/<string:empresa_id>/delete", methods=["POST"])
106
+ @require_role(TipoUsuario.ADMIN)
107
+ def delete(empresa_id):
108
+ """Delete a company. Only administrators can delete companies."""
109
+ empresa = db.session.get(Empresa, empresa_id)
110
+ if not empresa:
111
+ flash(_("Empresa no encontrada."), "warning")
112
+ return redirect(url_for("empresa.index"))
113
+
114
+ # Check if company has employees or payrolls
115
+ if empresa.empleados or empresa.planillas:
116
+ flash(
117
+ _("No se puede eliminar la empresa porque tiene empleados o planillas asociadas."),
118
+ "danger",
119
+ )
120
+ return redirect(url_for("empresa.index"))
121
+
122
+ try:
123
+ db.session.delete(empresa)
124
+ db.session.commit()
125
+ flash(_("Empresa eliminada exitosamente."), "success")
126
+ except Exception as e:
127
+ db.session.rollback()
128
+ flash(_("Error al eliminar la empresa: {}").format(str(e)), "danger")
129
+
130
+ return redirect(url_for("empresa.index"))
131
+
132
+
133
+ @empresa_bp.route("/<string:empresa_id>/toggle", methods=["POST"])
134
+ @require_role(TipoUsuario.ADMIN)
135
+ def toggle_active(empresa_id):
136
+ """Toggle company active status. Only administrators can toggle status."""
137
+ empresa = db.session.get(Empresa, empresa_id)
138
+ if not empresa:
139
+ flash(_("Empresa no encontrada."), "warning")
140
+ return redirect(url_for("empresa.index"))
141
+
142
+ empresa.activo = not empresa.activo
143
+ empresa.modificado_por = current_user.usuario
144
+
145
+ try:
146
+ db.session.commit()
147
+ estado = _("activada") if empresa.activo else _("desactivada")
148
+ flash(_("Empresa {} exitosamente.").format(estado), "success")
149
+ except Exception as e:
150
+ db.session.rollback()
151
+ flash(_("Error al cambiar el estado de la empresa: {}").format(str(e)), "danger")
152
+
153
+ return redirect(url_for("empresa.index"))