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,432 @@
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
+ """Report management routes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime
19
+
20
+ from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify, send_file
21
+ from flask_login import current_user
22
+
23
+ from coati_payroll.enums import ReportType, ReportStatus, TipoUsuario
24
+ from coati_payroll.i18n import _
25
+ from coati_payroll.model import db, Report, ReportRole, ReportExecution, ReportAudit
26
+ from coati_payroll.rbac import require_read_access, require_role
27
+ from coati_payroll.report_engine import (
28
+ ReportExecutionManager,
29
+ can_view_report,
30
+ can_execute_report,
31
+ can_export_report,
32
+ )
33
+ from coati_payroll.report_export import export_report_to_excel, export_report_to_csv
34
+ from coati_payroll.system_reports import get_system_report_metadata
35
+ from coati_payroll.log import log
36
+ from coati_payroll.vistas.constants import PER_PAGE
37
+
38
+ report_bp = Blueprint("report", __name__, url_prefix="/report")
39
+
40
+
41
+ # ============================================================================
42
+ # Report List and Administration
43
+ # ============================================================================
44
+
45
+
46
+ @report_bp.route("/")
47
+ @require_read_access()
48
+ def index():
49
+ """List all available reports.
50
+
51
+ Shows reports based on user permissions.
52
+ """
53
+ page = request.args.get("page", 1, type=int)
54
+ category = request.args.get("category", "", type=str)
55
+ report_type = request.args.get("type", "", type=str)
56
+ status = request.args.get("status", "", type=str)
57
+
58
+ # Base query
59
+ stmt = db.select(Report)
60
+
61
+ # Apply filters
62
+ if category:
63
+ stmt = stmt.filter(Report.category == category)
64
+ if report_type:
65
+ stmt = stmt.filter(Report.type == report_type)
66
+ if status:
67
+ stmt = stmt.filter(Report.status == status)
68
+
69
+ # Only show enabled reports to non-admin users
70
+ if current_user.tipo != TipoUsuario.ADMIN:
71
+ stmt = stmt.filter(Report.status == ReportStatus.ENABLED)
72
+
73
+ # Filter by user permissions
74
+ if current_user.tipo != TipoUsuario.ADMIN:
75
+ # Filter reports where user has view permission
76
+ stmt = stmt.join(ReportRole).filter(
77
+ ReportRole.role == current_user.tipo, ReportRole.can_view == True # noqa: E712
78
+ )
79
+
80
+ stmt = stmt.order_by(Report.category, Report.name)
81
+
82
+ # Paginate using Flask-SQLAlchemy's paginate method
83
+ pagination = db.paginate(stmt, page=page, per_page=PER_PAGE, error_out=False)
84
+
85
+ # Get unique categories for filter
86
+ categories = (
87
+ db.session.execute(
88
+ db.select(Report.category).distinct().filter(Report.category.isnot(None)).order_by(Report.category)
89
+ )
90
+ .scalars()
91
+ .all()
92
+ )
93
+
94
+ return render_template(
95
+ "modules/report/index.html",
96
+ reports=pagination.items,
97
+ pagination=pagination,
98
+ categories=categories,
99
+ current_category=category,
100
+ current_type=report_type,
101
+ current_status=status,
102
+ )
103
+
104
+
105
+ @report_bp.route("/admin")
106
+ @require_role(TipoUsuario.ADMIN)
107
+ def admin_index():
108
+ """Administrative report list.
109
+
110
+ Only accessible to administrators.
111
+ Shows all reports with management options.
112
+ """
113
+ page = request.args.get("page", 1, type=int)
114
+ category = request.args.get("category", "", type=str)
115
+ report_type = request.args.get("type", "", type=str)
116
+ status = request.args.get("status", "", type=str)
117
+
118
+ # Base query - show all reports for admin
119
+ stmt = db.select(Report)
120
+
121
+ # Apply filters
122
+ if category:
123
+ stmt = stmt.filter(Report.category == category)
124
+ if report_type:
125
+ stmt = stmt.filter(Report.type == report_type)
126
+ if status:
127
+ stmt = stmt.filter(Report.status == status)
128
+
129
+ stmt = stmt.order_by(Report.category, Report.name)
130
+
131
+ # Paginate using Flask-SQLAlchemy's paginate method
132
+ pagination = db.paginate(stmt, page=page, per_page=PER_PAGE, error_out=False)
133
+
134
+ # Get unique categories
135
+ categories = (
136
+ db.session.execute(
137
+ db.select(Report.category).distinct().filter(Report.category.isnot(None)).order_by(Report.category)
138
+ )
139
+ .scalars()
140
+ .all()
141
+ )
142
+
143
+ return render_template(
144
+ "modules/report/admin_index.html",
145
+ reports=pagination.items,
146
+ pagination=pagination,
147
+ categories=categories,
148
+ current_category=category,
149
+ current_type=report_type,
150
+ current_status=status,
151
+ )
152
+
153
+
154
+ # ============================================================================
155
+ # Report Execution
156
+ # ============================================================================
157
+
158
+
159
+ @report_bp.route("/<report_id>/execute")
160
+ @require_read_access()
161
+ def execute_form(report_id: str):
162
+ """Show report execution form.
163
+
164
+ Displays form to input parameters and execute report.
165
+ """
166
+ report = db.session.get(Report, report_id)
167
+ if not report:
168
+ flash(_("Reporte no encontrado."), "danger")
169
+ return redirect(url_for("report.index"))
170
+
171
+ # Check if report is enabled
172
+ if report.status != ReportStatus.ENABLED and current_user.tipo != TipoUsuario.ADMIN:
173
+ flash(_("Este reporte está deshabilitado."), "warning")
174
+ return redirect(url_for("report.index"))
175
+
176
+ # Check view permission
177
+ if not can_view_report(report, current_user.tipo):
178
+ flash(_("No tiene permisos para ver este reporte."), "danger")
179
+ return redirect(url_for("report.index"))
180
+
181
+ # Check execute permission
182
+ if not can_execute_report(report, current_user.tipo):
183
+ flash(_("No tiene permisos para ejecutar este reporte."), "danger")
184
+ return redirect(url_for("report.index"))
185
+
186
+ # Get metadata for system reports
187
+ metadata = None
188
+ if report.type == ReportType.SYSTEM:
189
+ metadata = get_system_report_metadata(report.system_report_id)
190
+
191
+ return render_template("modules/report/execute.html", report=report, metadata=metadata)
192
+
193
+
194
+ @report_bp.route("/<report_id>/run", methods=["POST"])
195
+ @require_read_access()
196
+ def run_report(report_id: str):
197
+ """Execute report and show results.
198
+
199
+ Processes parameters and executes report.
200
+ """
201
+ report = db.session.get(Report, report_id)
202
+ if not report:
203
+ return jsonify({"error": "Report not found"}), 404
204
+
205
+ # Check permissions
206
+ if report.status != ReportStatus.ENABLED and current_user.tipo != TipoUsuario.ADMIN:
207
+ return jsonify({"error": "Report is disabled"}), 403
208
+
209
+ if not can_execute_report(report, current_user.tipo):
210
+ return jsonify({"error": "Permission denied"}), 403
211
+
212
+ # Get parameters from request
213
+ parameters = request.get_json() or {}
214
+
215
+ # Get pagination parameters
216
+ page = parameters.pop("page", 1)
217
+ per_page = parameters.pop("per_page", 100)
218
+
219
+ try:
220
+ # Execute report
221
+ manager = ReportExecutionManager(report, current_user.usuario)
222
+ results, total_count, execution = manager.execute(parameters, page, per_page)
223
+
224
+ return jsonify(
225
+ {
226
+ "success": True,
227
+ "results": results,
228
+ "total_count": total_count,
229
+ "execution_id": execution.id,
230
+ "execution_time_ms": execution.execution_time_ms,
231
+ }
232
+ )
233
+
234
+ except Exception as e:
235
+ log.error(f"Error executing report: {e}")
236
+ return jsonify({"error": str(e)}), 500
237
+
238
+
239
+ # ============================================================================
240
+ # Report Export
241
+ # ============================================================================
242
+
243
+
244
+ @report_bp.route("/<report_id>/export/<format>", methods=["POST"])
245
+ @require_read_access()
246
+ def export_report(report_id: str, format: str):
247
+ """Export report to specified format.
248
+
249
+ Args:
250
+ report_id: Report ID
251
+ format: Export format (excel, csv)
252
+ """
253
+ report = db.session.get(Report, report_id)
254
+ if not report:
255
+ return jsonify({"error": "Report not found"}), 404
256
+
257
+ # Check export permission
258
+ if not can_export_report(report, current_user.tipo):
259
+ flash(_("No tiene permisos para exportar este reporte."), "danger")
260
+ return redirect(url_for("report.execute_form", report_id=report_id))
261
+
262
+ # Get parameters from request
263
+ parameters = request.get_json() or {}
264
+
265
+ try:
266
+ # Execute report (get all results, no pagination for export)
267
+ manager = ReportExecutionManager(report, current_user.usuario)
268
+ results, total_count, execution = manager.execute(parameters, page=1, per_page=50000)
269
+
270
+ # Export based on format
271
+ if format == "excel":
272
+ file_path = export_report_to_excel(report.name, results)
273
+ elif format == "csv":
274
+ file_path = export_report_to_csv(report.name, results)
275
+ else:
276
+ return jsonify({"error": "Invalid format"}), 400
277
+
278
+ # Update execution record with export info
279
+ execution.export_file_path = file_path
280
+ execution.export_format = format
281
+ db.session.commit()
282
+
283
+ # Send file
284
+ return send_file(file_path, as_attachment=True)
285
+
286
+ except Exception as e:
287
+ log.error(f"Error exporting report: {e}")
288
+ return jsonify({"error": str(e)}), 500
289
+
290
+
291
+ # ============================================================================
292
+ # Report Administration (Admin Only)
293
+ # ============================================================================
294
+
295
+
296
+ @report_bp.route("/<report_id>/toggle-status", methods=["POST"])
297
+ @require_role(TipoUsuario.ADMIN)
298
+ def toggle_status(report_id: str):
299
+ """Toggle report enabled/disabled status.
300
+
301
+ Only accessible to administrators.
302
+ """
303
+ report = db.session.get(Report, report_id)
304
+ if not report:
305
+ flash(_("Reporte no encontrado."), "danger")
306
+ return redirect(url_for("report.admin_index"))
307
+
308
+ # Toggle status
309
+ old_status = report.status
310
+ report.status = ReportStatus.DISABLED if report.status == ReportStatus.ENABLED else ReportStatus.ENABLED
311
+
312
+ # Create audit entry
313
+ audit = ReportAudit(
314
+ report_id=report.id,
315
+ action="status_changed",
316
+ performed_by=current_user.usuario,
317
+ changes={"old_status": old_status, "new_status": report.status},
318
+ )
319
+ db.session.add(audit)
320
+ db.session.commit()
321
+
322
+ flash(_("Estado del reporte actualizado."), "success")
323
+ return redirect(url_for("report.admin_index"))
324
+
325
+
326
+ @report_bp.route("/<report_id>/permissions")
327
+ @require_role(TipoUsuario.ADMIN)
328
+ def permissions_form(report_id: str):
329
+ """Show report permissions form.
330
+
331
+ Allows administrators to configure role-based permissions.
332
+ """
333
+ report = db.session.get(Report, report_id)
334
+ if not report:
335
+ flash(_("Reporte no encontrado."), "danger")
336
+ return redirect(url_for("report.admin_index"))
337
+
338
+ # Get existing permissions
339
+ existing_permissions = {perm.role: perm for perm in report.permissions}
340
+
341
+ return render_template(
342
+ "modules/report/permissions.html", report=report, existing_permissions=existing_permissions, roles=TipoUsuario
343
+ )
344
+
345
+
346
+ @report_bp.route("/<report_id>/permissions", methods=["POST"])
347
+ @require_role(TipoUsuario.ADMIN)
348
+ def update_permissions(report_id: str):
349
+ """Update report permissions.
350
+
351
+ Processes form and updates role-based permissions.
352
+ """
353
+ report = db.session.get(Report, report_id)
354
+ if not report:
355
+ flash(_("Reporte no encontrado."), "danger")
356
+ return redirect(url_for("report.admin_index"))
357
+
358
+ # Process permissions for each role
359
+ for role in [TipoUsuario.ADMIN, TipoUsuario.HHRR, TipoUsuario.AUDIT]:
360
+ can_view = request.form.get(f"{role}_can_view") == "on"
361
+ can_execute = request.form.get(f"{role}_can_execute") == "on"
362
+ can_export = request.form.get(f"{role}_can_export") == "on"
363
+
364
+ # Get or create permission record
365
+ perm = db.session.execute(db.select(ReportRole).filter_by(report_id=report.id, role=role)).scalar_one_or_none()
366
+
367
+ if perm:
368
+ # Update existing
369
+ perm.can_view = can_view
370
+ perm.can_execute = can_execute
371
+ perm.can_export = can_export
372
+ else:
373
+ # Create new
374
+ perm = ReportRole(
375
+ report_id=report.id, role=role, can_view=can_view, can_execute=can_execute, can_export=can_export
376
+ )
377
+ db.session.add(perm)
378
+
379
+ # Create audit entry
380
+ audit = ReportAudit(
381
+ report_id=report.id,
382
+ action="permissions_updated",
383
+ performed_by=current_user.usuario,
384
+ changes={"timestamp": datetime.now().isoformat()},
385
+ )
386
+ db.session.add(audit)
387
+ db.session.commit()
388
+
389
+ flash(_("Permisos actualizados correctamente."), "success")
390
+ return redirect(url_for("report.admin_index"))
391
+
392
+
393
+ # ============================================================================
394
+ # Report Detail
395
+ # ============================================================================
396
+
397
+
398
+ @report_bp.route("/<report_id>")
399
+ @require_read_access()
400
+ def detail(report_id: str):
401
+ """Show report details.
402
+
403
+ Displays report information, definition, and execution history.
404
+ """
405
+ report = db.session.get(Report, report_id)
406
+ if not report:
407
+ flash(_("Reporte no encontrado."), "danger")
408
+ return redirect(url_for("report.index"))
409
+
410
+ # Check view permission
411
+ if not can_view_report(report, current_user.tipo):
412
+ flash(_("No tiene permisos para ver este reporte."), "danger")
413
+ return redirect(url_for("report.index"))
414
+
415
+ # Get recent executions
416
+ executions = (
417
+ db.session.execute(
418
+ db.select(ReportExecution)
419
+ .filter_by(report_id=report.id)
420
+ .order_by(ReportExecution.timestamp.desc())
421
+ .limit(10)
422
+ )
423
+ .scalars()
424
+ .all()
425
+ )
426
+
427
+ # Get metadata for system reports
428
+ metadata = None
429
+ if report.type == ReportType.SYSTEM:
430
+ metadata = get_system_report_metadata(report.system_report_id)
431
+
432
+ return render_template("modules/report/detail.html", report=report, executions=executions, metadata=metadata)
@@ -0,0 +1,29 @@
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
+ """Settings page to consolidate administrative options."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from flask import Blueprint, render_template
19
+
20
+ from coati_payroll.rbac import require_write_access
21
+
22
+ settings_bp = Blueprint("settings", __name__, url_prefix="/settings")
23
+
24
+
25
+ @settings_bp.route("/")
26
+ @require_write_access()
27
+ def index():
28
+ """Display settings page with links to all configuration options."""
29
+ return render_template("modules/settings/index.html")
@@ -0,0 +1,134 @@
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
+ """Payroll Type (TipoPlanilla) 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 TipoPlanillaForm
22
+ from coati_payroll.i18n import _
23
+ from coati_payroll.rbac import require_read_access, require_write_access
24
+ from coati_payroll.model import TipoPlanilla, db
25
+ from coati_payroll.vistas.constants import PER_PAGE
26
+
27
+ tipo_planilla_bp = Blueprint("tipo_planilla", __name__, url_prefix="/tipo-planilla")
28
+
29
+
30
+ @tipo_planilla_bp.route("/")
31
+ @require_read_access()
32
+ def index():
33
+ """List all payroll types with pagination."""
34
+ page = request.args.get("page", 1, type=int)
35
+ pagination = db.paginate(
36
+ db.select(TipoPlanilla).order_by(TipoPlanilla.codigo),
37
+ page=page,
38
+ per_page=PER_PAGE,
39
+ error_out=False,
40
+ )
41
+ return render_template(
42
+ "modules/tipo_planilla/index.html",
43
+ tipos_planilla=pagination.items,
44
+ pagination=pagination,
45
+ )
46
+
47
+
48
+ @tipo_planilla_bp.route("/new", methods=["GET", "POST"])
49
+ @require_write_access()
50
+ def new():
51
+ """Create a new payroll type."""
52
+ form = TipoPlanillaForm()
53
+
54
+ if form.validate_on_submit():
55
+ tipo_planilla = TipoPlanilla()
56
+ tipo_planilla.codigo = form.codigo.data
57
+ tipo_planilla.descripcion = form.descripcion.data
58
+ tipo_planilla.periodicidad = form.periodicidad.data
59
+ tipo_planilla.dias = form.dias.data
60
+ tipo_planilla.mes_inicio_fiscal = form.mes_inicio_fiscal.data
61
+ tipo_planilla.dia_inicio_fiscal = form.dia_inicio_fiscal.data
62
+ tipo_planilla.acumula_anual = form.acumula_anual.data
63
+ tipo_planilla.periodos_por_anio = form.periodos_por_anio.data
64
+ tipo_planilla.activo = form.activo.data
65
+ tipo_planilla.creado_por = current_user.usuario
66
+
67
+ db.session.add(tipo_planilla)
68
+ db.session.commit()
69
+ flash(_("Tipo de planilla creado exitosamente."), "success")
70
+ return redirect(url_for("tipo_planilla.index"))
71
+
72
+ return render_template(
73
+ "modules/tipo_planilla/form.html",
74
+ form=form,
75
+ title=_("Nuevo Tipo de Planilla"),
76
+ )
77
+
78
+
79
+ @tipo_planilla_bp.route("/edit/<string:id>", methods=["GET", "POST"])
80
+ @require_write_access()
81
+ def edit(id: str):
82
+ """Edit an existing payroll type."""
83
+ tipo_planilla = db.session.get(TipoPlanilla, id)
84
+ if not tipo_planilla:
85
+ flash(_("Tipo de planilla no encontrado."), "error")
86
+ return redirect(url_for("tipo_planilla.index"))
87
+
88
+ form = TipoPlanillaForm(obj=tipo_planilla)
89
+
90
+ if form.validate_on_submit():
91
+ tipo_planilla.codigo = form.codigo.data
92
+ tipo_planilla.descripcion = form.descripcion.data
93
+ tipo_planilla.periodicidad = form.periodicidad.data
94
+ tipo_planilla.dias = form.dias.data
95
+ tipo_planilla.mes_inicio_fiscal = form.mes_inicio_fiscal.data
96
+ tipo_planilla.dia_inicio_fiscal = form.dia_inicio_fiscal.data
97
+ tipo_planilla.acumula_anual = form.acumula_anual.data
98
+ tipo_planilla.periodos_por_anio = form.periodos_por_anio.data
99
+ tipo_planilla.activo = form.activo.data
100
+ tipo_planilla.modificado_por = current_user.usuario
101
+
102
+ db.session.commit()
103
+ flash(_("Tipo de planilla actualizado exitosamente."), "success")
104
+ return redirect(url_for("tipo_planilla.index"))
105
+
106
+ return render_template(
107
+ "modules/tipo_planilla/form.html",
108
+ form=form,
109
+ title=_("Editar Tipo de Planilla"),
110
+ tipo_planilla=tipo_planilla,
111
+ )
112
+
113
+
114
+ @tipo_planilla_bp.route("/delete/<string:id>", methods=["POST"])
115
+ @require_write_access()
116
+ def delete(id: str):
117
+ """Delete a payroll type."""
118
+ tipo_planilla = db.session.get(TipoPlanilla, id)
119
+ if not tipo_planilla:
120
+ flash(_("Tipo de planilla no encontrado."), "error")
121
+ return redirect(url_for("tipo_planilla.index"))
122
+
123
+ # Check if this type is used by any planilla
124
+ if tipo_planilla.planillas:
125
+ flash(
126
+ _("No se puede eliminar un tipo de planilla que está siendo usado."),
127
+ "error",
128
+ )
129
+ return redirect(url_for("tipo_planilla.index"))
130
+
131
+ db.session.delete(tipo_planilla)
132
+ db.session.commit()
133
+ flash(_("Tipo de planilla eliminado exitosamente."), "success")
134
+ return redirect(url_for("tipo_planilla.index"))