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,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