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.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- 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
|