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,208 @@
|
|
|
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 export functionality for Excel format.
|
|
15
|
+
|
|
16
|
+
Provides utilities to export report results to Excel files with proper
|
|
17
|
+
formatting and metadata.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# <-------------------------------------------------------------------------> #
|
|
23
|
+
# Standard library
|
|
24
|
+
# <-------------------------------------------------------------------------> #
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import List, Dict, Any, Optional
|
|
28
|
+
|
|
29
|
+
# <-------------------------------------------------------------------------> #
|
|
30
|
+
# Third party libraries
|
|
31
|
+
# <-------------------------------------------------------------------------> #
|
|
32
|
+
try:
|
|
33
|
+
from openpyxl import Workbook
|
|
34
|
+
from openpyxl.styles import Font, Alignment, PatternFill
|
|
35
|
+
from openpyxl.utils import get_column_letter
|
|
36
|
+
|
|
37
|
+
OPENPYXL_AVAILABLE = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
OPENPYXL_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# <-------------------------------------------------------------------------> #
|
|
43
|
+
# Local modules
|
|
44
|
+
# <-------------------------------------------------------------------------> #
|
|
45
|
+
from coati_payroll.config import DIRECTORIO_APP
|
|
46
|
+
from coati_payroll.log import log
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ReportExporter:
|
|
50
|
+
"""Handles exporting report results to various formats."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, report_name: str, results: List[Dict[str, Any]]):
|
|
53
|
+
"""Initialize exporter.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
report_name: Name of the report
|
|
57
|
+
results: List of result dictionaries
|
|
58
|
+
"""
|
|
59
|
+
self.report_name = report_name
|
|
60
|
+
self.results = results
|
|
61
|
+
|
|
62
|
+
def to_excel(self, output_path: Optional[str] = None) -> str:
|
|
63
|
+
"""Export results to Excel format.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
output_path: Optional output file path. If not provided, generates one.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Path to exported file
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ImportError: If openpyxl is not installed
|
|
73
|
+
"""
|
|
74
|
+
if not OPENPYXL_AVAILABLE:
|
|
75
|
+
raise ImportError("openpyxl is required for Excel export. " "Install it with: pip install openpyxl")
|
|
76
|
+
|
|
77
|
+
# Generate output path if not provided
|
|
78
|
+
if not output_path:
|
|
79
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
80
|
+
safe_name = "".join(c for c in self.report_name if c.isalnum() or c in (" ", "_", "-")).strip()
|
|
81
|
+
safe_name = safe_name.replace(" ", "_") # Replace spaces with underscores for Windows compatibility
|
|
82
|
+
filename = f"{safe_name}_{timestamp}.xlsx"
|
|
83
|
+
|
|
84
|
+
# Create exports directory if it doesn't exist
|
|
85
|
+
exports_dir = Path(DIRECTORIO_APP) / "exports" / "reports"
|
|
86
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
output_path = str((exports_dir / filename).absolute())
|
|
89
|
+
|
|
90
|
+
# Create workbook
|
|
91
|
+
wb = Workbook()
|
|
92
|
+
ws = wb.active
|
|
93
|
+
ws.title = self.report_name[:31] # Excel sheet name limit
|
|
94
|
+
|
|
95
|
+
# Add metadata
|
|
96
|
+
ws["A1"] = "Report:"
|
|
97
|
+
ws["B1"] = self.report_name
|
|
98
|
+
ws["A2"] = "Generated:"
|
|
99
|
+
ws["B2"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
100
|
+
ws["A3"] = "Total Records:"
|
|
101
|
+
ws["B3"] = len(self.results)
|
|
102
|
+
|
|
103
|
+
# Style metadata
|
|
104
|
+
for cell in ["A1", "A2", "A3"]:
|
|
105
|
+
ws[cell].font = Font(bold=True)
|
|
106
|
+
|
|
107
|
+
# Add blank row
|
|
108
|
+
start_row = 5
|
|
109
|
+
|
|
110
|
+
if self.results:
|
|
111
|
+
# Add headers
|
|
112
|
+
headers = list(self.results[0].keys())
|
|
113
|
+
for col_idx, header in enumerate(headers, start=1):
|
|
114
|
+
cell = ws.cell(row=start_row, column=col_idx, value=header)
|
|
115
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
116
|
+
cell.fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
|
117
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
118
|
+
|
|
119
|
+
# Add data
|
|
120
|
+
for row_idx, row_data in enumerate(self.results, start=start_row + 1):
|
|
121
|
+
for col_idx, header in enumerate(headers, start=1):
|
|
122
|
+
value = row_data.get(header)
|
|
123
|
+
ws.cell(row=row_idx, column=col_idx, value=value)
|
|
124
|
+
|
|
125
|
+
# Auto-adjust column widths
|
|
126
|
+
for col_idx, header in enumerate(headers, start=1):
|
|
127
|
+
column_letter = get_column_letter(col_idx)
|
|
128
|
+
max_length = len(str(header))
|
|
129
|
+
|
|
130
|
+
for row_data in self.results:
|
|
131
|
+
value = row_data.get(header)
|
|
132
|
+
if value is not None:
|
|
133
|
+
max_length = max(max_length, len(str(value)))
|
|
134
|
+
|
|
135
|
+
# Set width with some padding
|
|
136
|
+
adjusted_width = min(max_length + 2, 50) # Max 50 chars
|
|
137
|
+
ws.column_dimensions[column_letter].width = adjusted_width
|
|
138
|
+
|
|
139
|
+
# Save workbook
|
|
140
|
+
wb.save(output_path)
|
|
141
|
+
log.info(f"Report exported to: {output_path}")
|
|
142
|
+
|
|
143
|
+
return output_path
|
|
144
|
+
|
|
145
|
+
def to_csv(self, output_path: Optional[str] = None) -> str:
|
|
146
|
+
"""Export results to CSV format.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
output_path: Optional output file path
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Path to exported file
|
|
153
|
+
"""
|
|
154
|
+
import csv
|
|
155
|
+
|
|
156
|
+
# Generate output path if not provided
|
|
157
|
+
if not output_path:
|
|
158
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
159
|
+
safe_name = "".join(c for c in self.report_name if c.isalnum() or c in (" ", "_", "-")).strip()
|
|
160
|
+
safe_name = safe_name.replace(" ", "_") # Replace spaces with underscores for Windows compatibility
|
|
161
|
+
filename = f"{safe_name}_{timestamp}.csv"
|
|
162
|
+
|
|
163
|
+
exports_dir = Path(DIRECTORIO_APP) / "exports" / "reports"
|
|
164
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
output_path = str((exports_dir / filename).absolute())
|
|
167
|
+
|
|
168
|
+
# Write CSV
|
|
169
|
+
if self.results:
|
|
170
|
+
headers = list(self.results[0].keys())
|
|
171
|
+
|
|
172
|
+
with open(output_path, "w", newline="", encoding="utf-8") as csvfile:
|
|
173
|
+
writer = csv.DictWriter(csvfile, fieldnames=headers)
|
|
174
|
+
writer.writeheader()
|
|
175
|
+
writer.writerows(self.results)
|
|
176
|
+
|
|
177
|
+
log.info(f"Report exported to: {output_path}")
|
|
178
|
+
return output_path
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def export_report_to_excel(report_name: str, results: List[Dict[str, Any]], output_path: Optional[str] = None) -> str:
|
|
182
|
+
"""Convenience function to export report to Excel.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
report_name: Name of the report
|
|
186
|
+
results: Report results
|
|
187
|
+
output_path: Optional output path
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path to exported file
|
|
191
|
+
"""
|
|
192
|
+
exporter = ReportExporter(report_name, results)
|
|
193
|
+
return exporter.to_excel(output_path)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def export_report_to_csv(report_name: str, results: List[Dict[str, Any]], output_path: Optional[str] = None) -> str:
|
|
197
|
+
"""Convenience function to export report to CSV.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
report_name: Name of the report
|
|
201
|
+
results: Report results
|
|
202
|
+
output_path: Optional output path
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Path to exported file
|
|
206
|
+
"""
|
|
207
|
+
exporter = ReportExporter(report_name, results)
|
|
208
|
+
return exporter.to_csv(output_path)
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
"""Schema validation for formula engine calculation rules.
|
|
15
|
+
|
|
16
|
+
This module provides validation functions for JSON schemas used by the
|
|
17
|
+
FormulaEngine to ensure they have the correct structure before execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# <-------------------------------------------------------------------------> #
|
|
23
|
+
# Standard library
|
|
24
|
+
# <-------------------------------------------------------------------------> #
|
|
25
|
+
import ast
|
|
26
|
+
from typing import Any, Type
|
|
27
|
+
|
|
28
|
+
# <-------------------------------------------------------------------------> #
|
|
29
|
+
# Third party libraries
|
|
30
|
+
# <-------------------------------------------------------------------------> #
|
|
31
|
+
|
|
32
|
+
# <-------------------------------------------------------------------------> #
|
|
33
|
+
# Local modules
|
|
34
|
+
# <-------------------------------------------------------------------------> #
|
|
35
|
+
from coati_payroll.enums import StepType
|
|
36
|
+
|
|
37
|
+
__all__ = ["ValidationError", "validate_schema", "validate_schema_deep"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ValidationError(Exception):
|
|
41
|
+
"""Exception for validation errors in schema or data."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate_schema(schema: dict[str, Any], error_class: Type[Exception] = ValidationError) -> None:
|
|
47
|
+
"""Validate the calculation schema structure.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
schema: The JSON schema to validate
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValidationError: If schema is missing required fields or has invalid structure
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(schema, dict):
|
|
56
|
+
raise error_class("Schema must be a dictionary")
|
|
57
|
+
|
|
58
|
+
# Check for required sections
|
|
59
|
+
if "steps" not in schema:
|
|
60
|
+
raise error_class("Schema must contain 'steps' section")
|
|
61
|
+
|
|
62
|
+
# Validate steps structure
|
|
63
|
+
for i, step in enumerate(schema.get("steps", [])):
|
|
64
|
+
if not isinstance(step, dict):
|
|
65
|
+
raise error_class(f"Step {i} must be a dictionary")
|
|
66
|
+
if "name" not in step:
|
|
67
|
+
raise error_class(f"Step {i} must have a 'name' field")
|
|
68
|
+
if "type" not in step:
|
|
69
|
+
raise error_class(f"Step {i} must have a 'type' field")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_schema_deep(schema: dict[str, Any], error_class: Type[Exception] = ValidationError) -> None:
|
|
73
|
+
"""Validate the calculation schema structure including formula safety.
|
|
74
|
+
|
|
75
|
+
This performs deeper validation than validate_schema, including checking
|
|
76
|
+
formulas for unsafe operations. Use this for API validation endpoints.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
schema: The JSON schema to validate
|
|
80
|
+
error_class: Exception class to raise on error
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValidationError: If schema is invalid or contains unsafe formulas
|
|
84
|
+
"""
|
|
85
|
+
# First do basic validation
|
|
86
|
+
validate_schema(schema, error_class)
|
|
87
|
+
|
|
88
|
+
# Get valid step types
|
|
89
|
+
valid_step_types = {step_type.value for step_type in StepType}
|
|
90
|
+
|
|
91
|
+
# Then validate step types and formulas for safety
|
|
92
|
+
for i, step in enumerate(schema.get("steps", [])):
|
|
93
|
+
step_type = step.get("type")
|
|
94
|
+
|
|
95
|
+
# Validate step type
|
|
96
|
+
if step_type not in valid_step_types:
|
|
97
|
+
raise error_class(
|
|
98
|
+
f"Step {i} has invalid type '{step_type}'. Valid types are: {', '.join(valid_step_types)}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Validate formulas for calculation steps
|
|
102
|
+
if step_type == StepType.CALCULATION:
|
|
103
|
+
formula = step.get("formula", "")
|
|
104
|
+
if formula:
|
|
105
|
+
_validate_formula_safety(formula, error_class)
|
|
106
|
+
|
|
107
|
+
# Validate conditional step formulas
|
|
108
|
+
if step_type == StepType.CONDITIONAL:
|
|
109
|
+
if_true = step.get("if_true", "")
|
|
110
|
+
if_false = step.get("if_false", "")
|
|
111
|
+
if if_true:
|
|
112
|
+
_validate_formula_safety(if_true, error_class)
|
|
113
|
+
if if_false:
|
|
114
|
+
_validate_formula_safety(if_false, error_class)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_formula_safety(formula: str, error_class: Type[Exception] = ValidationError) -> None:
|
|
118
|
+
"""Validate that a formula doesn't contain unsafe operations.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
formula: Formula string to validate
|
|
122
|
+
error_class: Exception class to raise on error
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValidationError: If formula contains unsafe operations
|
|
126
|
+
"""
|
|
127
|
+
if not formula or not isinstance(formula, str):
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Parse the formula into an AST
|
|
132
|
+
tree = ast.parse(formula, mode="eval")
|
|
133
|
+
|
|
134
|
+
# Check for unsafe operations
|
|
135
|
+
for node in ast.walk(tree.body):
|
|
136
|
+
# Block dangerous operations
|
|
137
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
138
|
+
raise error_class("Import statements are not allowed in formulas")
|
|
139
|
+
|
|
140
|
+
# Block attribute access (prevents __import__, etc.)
|
|
141
|
+
if isinstance(node, ast.Attribute):
|
|
142
|
+
raise error_class("Attribute access is not allowed in formulas")
|
|
143
|
+
|
|
144
|
+
# Block function calls to __import__ and other dangerous builtins
|
|
145
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
146
|
+
func_name = node.func.id
|
|
147
|
+
dangerous_functions = {
|
|
148
|
+
"__import__",
|
|
149
|
+
"eval",
|
|
150
|
+
"exec",
|
|
151
|
+
"compile",
|
|
152
|
+
"open",
|
|
153
|
+
"input",
|
|
154
|
+
"__builtins__",
|
|
155
|
+
"globals",
|
|
156
|
+
"locals",
|
|
157
|
+
"vars",
|
|
158
|
+
"dir",
|
|
159
|
+
"getattr",
|
|
160
|
+
"setattr",
|
|
161
|
+
"delattr",
|
|
162
|
+
"hasattr",
|
|
163
|
+
}
|
|
164
|
+
if func_name in dangerous_functions:
|
|
165
|
+
raise error_class(f"Function '{func_name}' is not allowed in formulas")
|
|
166
|
+
except SyntaxError as e:
|
|
167
|
+
raise error_class(f"Invalid formula syntax: {e}") from e
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
"""Security module - HTTP Security Headers and Security Utilities.
|
|
15
|
+
|
|
16
|
+
This module implements security best practices for Flask applications,
|
|
17
|
+
including HTTP security headers to protect against common web vulnerabilities.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure_security_headers(app):
|
|
24
|
+
"""Configure HTTP security headers for the Flask application.
|
|
25
|
+
|
|
26
|
+
Implements OWASP recommendations for secure HTTP headers:
|
|
27
|
+
- Content-Security-Policy: Prevents XSS and data injection attacks
|
|
28
|
+
- X-Frame-Options: Prevents clickjacking attacks
|
|
29
|
+
- X-Content-Type-Options: Prevents MIME sniffing
|
|
30
|
+
- Strict-Transport-Security: Forces HTTPS connections (production only)
|
|
31
|
+
- X-XSS-Protection: Legacy XSS protection for older browsers
|
|
32
|
+
- Referrer-Policy: Controls referrer information leakage
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
app: Flask application instance
|
|
36
|
+
"""
|
|
37
|
+
from coati_payroll.config import DESARROLLO
|
|
38
|
+
|
|
39
|
+
@app.after_request
|
|
40
|
+
def add_security_headers(response):
|
|
41
|
+
"""Add security headers to all responses."""
|
|
42
|
+
|
|
43
|
+
# Content Security Policy - Restricts resource loading
|
|
44
|
+
# Allows self-hosted resources and specific external sources
|
|
45
|
+
csp = (
|
|
46
|
+
"default-src 'self'; "
|
|
47
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; "
|
|
48
|
+
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
|
49
|
+
"img-src 'self' data: https:; "
|
|
50
|
+
"font-src 'self' data: https://cdn.jsdelivr.net; "
|
|
51
|
+
"connect-src 'self'; "
|
|
52
|
+
"frame-ancestors 'none'; "
|
|
53
|
+
"base-uri 'self'; "
|
|
54
|
+
"form-action 'self'"
|
|
55
|
+
)
|
|
56
|
+
response.headers["Content-Security-Policy"] = csp
|
|
57
|
+
|
|
58
|
+
# Prevent clickjacking - don't allow framing by any site
|
|
59
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
60
|
+
|
|
61
|
+
# Prevent MIME type sniffing
|
|
62
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
63
|
+
|
|
64
|
+
# Legacy XSS protection (for older browsers)
|
|
65
|
+
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
66
|
+
|
|
67
|
+
# Control referrer information
|
|
68
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
69
|
+
|
|
70
|
+
# HSTS - Force HTTPS (only in production, never in testing)
|
|
71
|
+
enable_hsts = not DESARROLLO and not app.config.get("TESTING", False)
|
|
72
|
+
if enable_hsts:
|
|
73
|
+
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
return app
|