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,512 @@
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 execution engine for system and custom reports.
15
+
16
+ This module provides the core functionality for executing reports:
17
+ - Query building for custom reports with security constraints
18
+ - Expression evaluation for calculated columns
19
+ - Pagination and result limiting
20
+ - Integration with system report implementations
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ # <-------------------------------------------------------------------------> #
26
+ # Standard library
27
+ # <-------------------------------------------------------------------------> #
28
+ from datetime import datetime, timezone
29
+ from decimal import Decimal
30
+ from typing import Any, Dict, List, Optional, Tuple
31
+
32
+ # <-------------------------------------------------------------------------> #
33
+ # Third party libraries
34
+ # <-------------------------------------------------------------------------> #
35
+
36
+
37
+ # <-------------------------------------------------------------------------> #
38
+ # Third party libraries
39
+ # <-------------------------------------------------------------------------> #
40
+ from coati_payroll.enums import ReportType, ReportExecutionStatus
41
+ from coati_payroll.model import (
42
+ db,
43
+ Report,
44
+ ReportExecution,
45
+ Empleado,
46
+ Nomina,
47
+ NominaEmpleado,
48
+ VacationAccount,
49
+ Empresa,
50
+ Planilla,
51
+ )
52
+ from coati_payroll.log import log
53
+
54
+
55
+ # ============================================================================
56
+ # Whitelisted entities and fields for custom reports
57
+ # ============================================================================
58
+
59
+ # Entities that can be used as base for custom reports
60
+ ALLOWED_ENTITIES = {
61
+ "Employee": Empleado,
62
+ "Nomina": Nomina,
63
+ "NominaEmpleado": NominaEmpleado,
64
+ "VacationAccount": VacationAccount,
65
+ "Empresa": Empresa,
66
+ "Planilla": Planilla,
67
+ }
68
+
69
+ # Whitelisted fields per entity (for security)
70
+ ALLOWED_FIELDS = {
71
+ "Employee": [
72
+ "codigo_empleado",
73
+ "primer_nombre",
74
+ "segundo_nombre",
75
+ "primer_apellido",
76
+ "segundo_apellido",
77
+ "genero",
78
+ "nacionalidad",
79
+ "identificacion_personal",
80
+ "fecha_nacimiento",
81
+ "fecha_alta",
82
+ "fecha_baja",
83
+ "activo",
84
+ "cargo",
85
+ "area",
86
+ "centro_costos",
87
+ "salario_base",
88
+ "correo",
89
+ "telefono",
90
+ "tipo_contrato",
91
+ ],
92
+ "Nomina": [
93
+ "codigo_nomina",
94
+ "descripcion",
95
+ "periodo_inicio",
96
+ "periodo_fin",
97
+ "fecha_pago",
98
+ "estado",
99
+ "total_bruto",
100
+ "total_deducciones",
101
+ "total_neto",
102
+ ],
103
+ "NominaEmpleado": [
104
+ "salario_base",
105
+ "total_percepciones",
106
+ "salario_bruto",
107
+ "total_deducciones",
108
+ "salario_neto",
109
+ "total_prestaciones",
110
+ ],
111
+ "VacationAccount": [
112
+ "balance_days",
113
+ "balance_hours",
114
+ "accrued_days",
115
+ "accrued_hours",
116
+ "used_days",
117
+ "used_hours",
118
+ ],
119
+ "Empresa": [
120
+ "codigo",
121
+ "razon_social",
122
+ "nombre_comercial",
123
+ "ruc",
124
+ "activo",
125
+ ],
126
+ "Planilla": [
127
+ "nombre",
128
+ "descripcion",
129
+ "activo",
130
+ ],
131
+ }
132
+
133
+ # Whitelisted operators for filters
134
+ ALLOWED_OPERATORS = {
135
+ "=": lambda field, value: field == value,
136
+ "!=": lambda field, value: field != value,
137
+ ">": lambda field, value: field > value,
138
+ ">=": lambda field, value: field >= value,
139
+ "<": lambda field, value: field < value,
140
+ "<=": lambda field, value: field <= value,
141
+ "like": lambda field, value: field.like(f"%{value}%"),
142
+ "in": lambda field, value: field.in_(value if isinstance(value, list) else [value]),
143
+ "is_null": lambda field, value: field.is_(None),
144
+ "is_not_null": lambda field, value: field.isnot(None),
145
+ }
146
+
147
+ # Maximum rows per report execution
148
+ MAX_ROWS_PER_EXECUTION = 50000
149
+
150
+
151
+ # ============================================================================
152
+ # Custom Report Query Builder
153
+ # ============================================================================
154
+
155
+
156
+ class CustomReportBuilder:
157
+ """Builds and executes queries for custom reports.
158
+
159
+ Uses a whitelist-based approach for security:
160
+ - Only allowed entities can be queried
161
+ - Only allowed fields can be selected
162
+ - Only allowed operators can be used in filters
163
+ - No raw SQL or arbitrary code execution
164
+ """
165
+
166
+ def __init__(self, report: Report):
167
+ """Initialize the report builder.
168
+
169
+ Args:
170
+ report: Report instance to build query for
171
+ """
172
+ self.report = report
173
+ self.definition = report.definition or {}
174
+ self.base_entity_name = report.base_entity
175
+ self.base_entity = ALLOWED_ENTITIES.get(self.base_entity_name)
176
+
177
+ if not self.base_entity:
178
+ raise ValueError(f"Invalid base entity: {self.base_entity_name}")
179
+
180
+ def validate_definition(self) -> List[str]:
181
+ """Validate report definition for security.
182
+
183
+ Returns:
184
+ List of validation errors (empty if valid)
185
+ """
186
+ errors = []
187
+
188
+ # Validate base entity
189
+ if self.base_entity_name not in ALLOWED_ENTITIES:
190
+ errors.append(f"Base entity '{self.base_entity_name}' is not allowed")
191
+
192
+ # Validate columns
193
+ columns = self.definition.get("columns", [])
194
+ for col in columns:
195
+ col_type = col.get("type")
196
+ if col_type == "field":
197
+ entity = col.get("entity", self.base_entity_name)
198
+ field = col.get("field")
199
+
200
+ if entity not in ALLOWED_FIELDS:
201
+ errors.append(f"Entity '{entity}' is not allowed")
202
+ elif field not in ALLOWED_FIELDS[entity]:
203
+ errors.append(f"Field '{field}' is not allowed for entity '{entity}'")
204
+
205
+ elif col_type == "expression":
206
+ # For now, we don't support custom expressions to maintain security
207
+ # This would require a safe expression evaluator
208
+ errors.append("Custom expressions are not yet supported")
209
+
210
+ # Validate filters
211
+ filters = self.definition.get("filters", [])
212
+ for filt in filters:
213
+ field = filt.get("field")
214
+ operator = filt.get("operator")
215
+
216
+ if field not in ALLOWED_FIELDS.get(self.base_entity_name, []):
217
+ errors.append(f"Filter field '{field}' is not allowed")
218
+
219
+ if operator not in ALLOWED_OPERATORS:
220
+ errors.append(f"Filter operator '{operator}' is not allowed")
221
+
222
+ return errors
223
+
224
+ def build_query(self, filters: Optional[Dict[str, Any]] = None, page: int = 1, per_page: int = 100):
225
+ """Build SQLAlchemy select statement for the report.
226
+
227
+ Args:
228
+ filters: Additional runtime filters from user
229
+ page: Page number for pagination
230
+ per_page: Results per page
231
+
232
+ Returns:
233
+ SQLAlchemy Select statement
234
+ """
235
+ # Start with base entity
236
+ stmt = db.select(self.base_entity)
237
+
238
+ # Apply filters from definition
239
+ definition_filters = self.definition.get("filters", [])
240
+ for filt in definition_filters:
241
+ field_name = filt.get("field")
242
+ operator = filt.get("operator")
243
+ value = filt.get("value")
244
+
245
+ if field_name and operator in ALLOWED_OPERATORS:
246
+ field = getattr(self.base_entity, field_name, None)
247
+ if field is not None:
248
+ filter_func = ALLOWED_OPERATORS[operator]
249
+ stmt = stmt.filter(filter_func(field, value))
250
+
251
+ # Apply runtime filters
252
+ if filters:
253
+ for field_name, value in filters.items():
254
+ if field_name in ALLOWED_FIELDS.get(self.base_entity_name, []):
255
+ field = getattr(self.base_entity, field_name, None)
256
+ if field is not None:
257
+ stmt = stmt.filter(field == value)
258
+
259
+ # Apply sorting
260
+ sorting = self.definition.get("sorting", [])
261
+ for sort in sorting:
262
+ field_name = sort.get("field")
263
+ direction = sort.get("direction", "asc")
264
+
265
+ if field_name in ALLOWED_FIELDS.get(self.base_entity_name, []):
266
+ field = getattr(self.base_entity, field_name, None)
267
+ if field is not None:
268
+ if direction.lower() == "desc":
269
+ stmt = stmt.order_by(field.desc())
270
+ else:
271
+ stmt = stmt.order_by(field.asc())
272
+
273
+ # Apply pagination
274
+ stmt = stmt.limit(min(per_page, MAX_ROWS_PER_EXECUTION))
275
+ if page > 1:
276
+ stmt = stmt.offset((page - 1) * per_page)
277
+
278
+ return stmt
279
+
280
+ def execute(
281
+ self, filters: Optional[Dict[str, Any]] = None, page: int = 1, per_page: int = 100
282
+ ) -> Tuple[List[Dict[str, Any]], int]:
283
+ """Execute the report and return results.
284
+
285
+ Args:
286
+ filters: Additional runtime filters
287
+ page: Page number
288
+ per_page: Results per page
289
+
290
+ Returns:
291
+ Tuple of (results as list of dicts, total count)
292
+ """
293
+ # Build base query without pagination for count
294
+ from sqlalchemy import func
295
+
296
+ # Start with base entity
297
+ count_stmt = db.select(self.base_entity)
298
+
299
+ # Apply filters from definition
300
+ definition_filters = self.definition.get("filters", [])
301
+ for filt in definition_filters:
302
+ field_name = filt.get("field")
303
+ operator = filt.get("operator")
304
+ value = filt.get("value")
305
+
306
+ if field_name and operator in ALLOWED_OPERATORS:
307
+ field = getattr(self.base_entity, field_name, None)
308
+ if field is not None:
309
+ filter_func = ALLOWED_OPERATORS[operator]
310
+ count_stmt = count_stmt.filter(filter_func(field, value))
311
+
312
+ # Apply runtime filters
313
+ if filters:
314
+ for field_name, value in filters.items():
315
+ if field_name in ALLOWED_FIELDS.get(self.base_entity_name, []):
316
+ field = getattr(self.base_entity, field_name, None)
317
+ if field is not None:
318
+ count_stmt = count_stmt.filter(field == value)
319
+
320
+ # Get total count (without pagination or sorting)
321
+ total_count = db.session.execute(db.select(func.count()).select_from(count_stmt.subquery())).scalar() or 0
322
+
323
+ # Build query with pagination for results
324
+ stmt = self.build_query(filters, page, per_page)
325
+
326
+ # Execute statement
327
+ results = db.session.execute(stmt).scalars().all()
328
+
329
+ # Convert to list of dicts
330
+ columns = self.definition.get("columns", [])
331
+ output = []
332
+
333
+ for row in results:
334
+ row_dict = {}
335
+ for col in columns:
336
+ if col.get("type") == "field":
337
+ field_name = col.get("field")
338
+ label = col.get("label", field_name)
339
+ value = getattr(row, field_name, None)
340
+
341
+ # Convert Decimal to float for JSON serialization
342
+ if isinstance(value, Decimal):
343
+ value = float(value)
344
+ elif isinstance(value, datetime):
345
+ value = value.isoformat()
346
+
347
+ row_dict[label] = value
348
+
349
+ output.append(row_dict)
350
+
351
+ return output, total_count
352
+
353
+
354
+ # ============================================================================
355
+ # Report Execution Manager
356
+ # ============================================================================
357
+
358
+
359
+ class ReportExecutionManager:
360
+ """Manages report execution lifecycle and tracking."""
361
+
362
+ def __init__(self, report: Report, user: str):
363
+ """Initialize execution manager.
364
+
365
+ Args:
366
+ report: Report to execute
367
+ user: Username of person executing the report
368
+ """
369
+ self.report = report
370
+ self.user = user
371
+
372
+ def execute(
373
+ self, parameters: Optional[Dict[str, Any]] = None, page: int = 1, per_page: int = 100
374
+ ) -> Tuple[List[Dict[str, Any]], int, ReportExecution]:
375
+ """Execute report and track execution.
376
+
377
+ Args:
378
+ parameters: Runtime parameters/filters
379
+ page: Page number
380
+ per_page: Results per page
381
+
382
+ Returns:
383
+ Tuple of (results, total_count, execution_record)
384
+ """
385
+ # Create execution record
386
+ execution = ReportExecution(
387
+ report_id=self.report.id,
388
+ status=ReportExecutionStatus.RUNNING,
389
+ parameters=parameters or {},
390
+ executed_by=self.user,
391
+ started_at=datetime.now(timezone.utc),
392
+ )
393
+ db.session.add(execution)
394
+ db.session.commit()
395
+
396
+ start_time = datetime.now(timezone.utc)
397
+
398
+ try:
399
+ if self.report.type == ReportType.CUSTOM:
400
+ # Execute custom report
401
+ builder = CustomReportBuilder(self.report)
402
+ results, total_count = builder.execute(parameters, page, per_page)
403
+ else:
404
+ # Execute system report
405
+ from coati_payroll.system_reports import get_system_report
406
+
407
+ system_report_func = get_system_report(self.report.system_report_id)
408
+ if not system_report_func:
409
+ raise ValueError(f"System report '{self.report.system_report_id}' not found")
410
+
411
+ # Execute system report (they handle their own pagination)
412
+ results = system_report_func(parameters or {})
413
+ total_count = len(results)
414
+
415
+ # Apply pagination to system report results
416
+ if page > 1 or per_page < len(results):
417
+ start_idx = (page - 1) * per_page
418
+ end_idx = start_idx + per_page
419
+ results = results[start_idx:end_idx]
420
+
421
+ # Update execution record
422
+ end_time = datetime.now(timezone.utc)
423
+ execution.status = ReportExecutionStatus.COMPLETED
424
+ execution.completed_at = end_time
425
+ execution.row_count = len(results)
426
+ execution.execution_time_ms = int((end_time - start_time).total_seconds() * 1000)
427
+
428
+ db.session.commit()
429
+
430
+ return results, total_count, execution
431
+
432
+ except Exception as e:
433
+ # Update execution record with error
434
+ execution.status = ReportExecutionStatus.FAILED
435
+ execution.completed_at = datetime.now(timezone.utc)
436
+ execution.error_message = str(e)[:1000] # Truncate to fit column
437
+
438
+ db.session.commit()
439
+
440
+ log.error(f"Report execution failed: {e}")
441
+ raise
442
+
443
+
444
+ # ============================================================================
445
+ # Permission Checking
446
+ # ============================================================================
447
+
448
+
449
+ def can_view_report(report: Report, user_role: str) -> bool:
450
+ """Check if user role can view the report.
451
+
452
+ Args:
453
+ report: Report to check
454
+ user_role: User's role (admin, hhrr, audit)
455
+
456
+ Returns:
457
+ True if user can view report
458
+ """
459
+ # Admin can always view
460
+ if user_role == "admin":
461
+ return True
462
+
463
+ # Check report permissions
464
+ for perm in report.permissions:
465
+ if perm.role == user_role and perm.can_view:
466
+ return True
467
+
468
+ return False
469
+
470
+
471
+ def can_execute_report(report: Report, user_role: str) -> bool:
472
+ """Check if user role can execute the report.
473
+
474
+ Args:
475
+ report: Report to check
476
+ user_role: User's role
477
+
478
+ Returns:
479
+ True if user can execute report
480
+ """
481
+ # Admin can always execute
482
+ if user_role == "admin":
483
+ return True
484
+
485
+ # Check report permissions
486
+ for perm in report.permissions:
487
+ if perm.role == user_role and perm.can_execute:
488
+ return True
489
+
490
+ return False
491
+
492
+
493
+ def can_export_report(report: Report, user_role: str) -> bool:
494
+ """Check if user role can export the report.
495
+
496
+ Args:
497
+ report: Report to check
498
+ user_role: User's role
499
+
500
+ Returns:
501
+ True if user can export report
502
+ """
503
+ # Admin can always export
504
+ if user_role == "admin":
505
+ return True
506
+
507
+ # Check report permissions
508
+ for perm in report.permissions:
509
+ if perm.role == user_role and perm.can_export:
510
+ return True
511
+
512
+ return False