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,573 @@
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
+ """System reports definitions and implementations.
15
+
16
+ This module contains all pre-defined system reports that are optimized
17
+ and built into the application core. Each system report has a unique
18
+ identifier and an implementation function.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ # <-------------------------------------------------------------------------> #
24
+ # Standard library
25
+ # <-------------------------------------------------------------------------> #
26
+ from datetime import datetime
27
+ from typing import Dict, Any, List, Optional, Callable
28
+
29
+ # <-------------------------------------------------------------------------> #
30
+ # Third party libraries
31
+ # <-------------------------------------------------------------------------> #
32
+ from sqlalchemy import func
33
+
34
+ # <-------------------------------------------------------------------------> #
35
+ # Local modules
36
+ # <-------------------------------------------------------------------------> #
37
+ from coati_payroll.model import (
38
+ db,
39
+ Empleado,
40
+ Nomina,
41
+ NominaEmpleado,
42
+ NominaDetalle,
43
+ VacationAccount,
44
+ VacationLedger,
45
+ )
46
+ from coati_payroll.enums import TipoDetalle
47
+
48
+
49
+ # ============================================================================
50
+ # System Report Registry
51
+ # ============================================================================
52
+
53
+ # Registry of all system reports
54
+ # Key: system_report_id
55
+ # Value: implementation function
56
+ SYSTEM_REPORTS: Dict[str, Callable] = {}
57
+
58
+
59
+ def register_system_report(report_id: str):
60
+ """Decorator to register a system report implementation.
61
+
62
+ Args:
63
+ report_id: Unique identifier for the system report
64
+ """
65
+
66
+ def decorator(func: Callable):
67
+ SYSTEM_REPORTS[report_id] = func
68
+ return func
69
+
70
+ return decorator
71
+
72
+
73
+ def get_system_report(report_id: str) -> Optional[Callable]:
74
+ """Get system report implementation by ID.
75
+
76
+ Args:
77
+ report_id: System report identifier
78
+
79
+ Returns:
80
+ Report implementation function or None
81
+ """
82
+ return SYSTEM_REPORTS.get(report_id)
83
+
84
+
85
+ # ============================================================================
86
+ # Employee Reports
87
+ # ============================================================================
88
+
89
+
90
+ @register_system_report("employee_list")
91
+ def employee_list_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
92
+ """General list of employees.
93
+
94
+ Parameters:
95
+ - activo (optional): Filter by active status
96
+ - empresa_id (optional): Filter by company
97
+ """
98
+ stmt = db.select(Empleado)
99
+
100
+ # Apply filters
101
+ if "activo" in parameters:
102
+ stmt = stmt.filter(Empleado.activo == parameters["activo"])
103
+
104
+ if "empresa_id" in parameters:
105
+ stmt = stmt.filter(Empleado.empresa_id == parameters["empresa_id"])
106
+
107
+ results = db.session.execute(stmt.order_by(Empleado.primer_apellido, Empleado.primer_nombre)).scalars().all()
108
+
109
+ return [
110
+ {
111
+ "Código": emp.codigo_empleado,
112
+ "Nombres": f"{emp.primer_nombre} {emp.segundo_nombre or ''}".strip(),
113
+ "Apellidos": f"{emp.primer_apellido} {emp.segundo_apellido or ''}".strip(),
114
+ "Identificación": emp.identificacion_personal,
115
+ "Cargo": emp.cargo or "",
116
+ "Área": emp.area or "",
117
+ "Salario Base": float(emp.salario_base) if emp.salario_base else 0.0,
118
+ "Fecha Alta": emp.fecha_alta.isoformat() if emp.fecha_alta else "",
119
+ "Activo": "Sí" if emp.activo else "No",
120
+ }
121
+ for emp in results
122
+ ]
123
+
124
+
125
+ @register_system_report("employee_active_inactive")
126
+ def employee_active_inactive_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
127
+ """Active and inactive employees report.
128
+
129
+ Shows current status of all employees with relevant dates.
130
+ """
131
+ results = (
132
+ db.session.execute(db.select(Empleado).order_by(Empleado.activo.desc(), Empleado.primer_apellido))
133
+ .scalars()
134
+ .all()
135
+ )
136
+
137
+ return [
138
+ {
139
+ "Código": emp.codigo_empleado,
140
+ "Nombre Completo": f"{emp.primer_nombre} {emp.primer_apellido}",
141
+ "Identificación": emp.identificacion_personal,
142
+ "Estado": "Activo" if emp.activo else "Inactivo",
143
+ "Fecha Alta": emp.fecha_alta.isoformat() if emp.fecha_alta else "",
144
+ "Fecha Baja": emp.fecha_baja.isoformat() if emp.fecha_baja else "",
145
+ "Cargo": emp.cargo or "",
146
+ }
147
+ for emp in results
148
+ ]
149
+
150
+
151
+ @register_system_report("employee_by_department")
152
+ def employee_by_department_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
153
+ """Employees by department/area.
154
+
155
+ Groups employees by area and provides summary statistics.
156
+ """
157
+ results = (
158
+ db.session.execute(
159
+ db.select(Empleado)
160
+ .filter(Empleado.activo == True) # noqa: E712
161
+ .order_by(Empleado.area, Empleado.primer_apellido)
162
+ )
163
+ .scalars()
164
+ .all()
165
+ )
166
+
167
+ return [
168
+ {
169
+ "Área": emp.area or "Sin Asignar",
170
+ "Código": emp.codigo_empleado,
171
+ "Nombre": f"{emp.primer_nombre} {emp.primer_apellido}",
172
+ "Cargo": emp.cargo or "",
173
+ "Centro de Costos": emp.centro_costos or "",
174
+ "Salario Base": float(emp.salario_base) if emp.salario_base else 0.0,
175
+ }
176
+ for emp in results
177
+ ]
178
+
179
+
180
+ @register_system_report("employee_hires_terminations")
181
+ def employee_hires_terminations_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
182
+ """Hires and terminations by period.
183
+
184
+ Parameters:
185
+ - fecha_inicio: Start date
186
+ - fecha_fin: End date
187
+ """
188
+ fecha_inicio = parameters.get("fecha_inicio")
189
+ fecha_fin = parameters.get("fecha_fin")
190
+
191
+ if isinstance(fecha_inicio, str):
192
+ fecha_inicio = datetime.fromisoformat(fecha_inicio).date()
193
+ if isinstance(fecha_fin, str):
194
+ fecha_fin = datetime.fromisoformat(fecha_fin).date()
195
+
196
+ # Get hires
197
+ hires_stmt = db.select(Empleado)
198
+ if fecha_inicio:
199
+ hires_stmt = hires_stmt.filter(Empleado.fecha_alta >= fecha_inicio)
200
+ if fecha_fin:
201
+ hires_stmt = hires_stmt.filter(Empleado.fecha_alta <= fecha_fin)
202
+
203
+ hires = db.session.execute(hires_stmt).scalars().all()
204
+
205
+ # Get terminations
206
+ terminations_stmt = db.select(Empleado).filter(Empleado.fecha_baja.isnot(None))
207
+ if fecha_inicio:
208
+ terminations_stmt = terminations_stmt.filter(Empleado.fecha_baja >= fecha_inicio)
209
+ if fecha_fin:
210
+ terminations_stmt = terminations_stmt.filter(Empleado.fecha_baja <= fecha_fin)
211
+
212
+ terminations = db.session.execute(terminations_stmt).scalars().all()
213
+
214
+ results = []
215
+
216
+ # Add hires
217
+ for emp in hires:
218
+ results.append(
219
+ {
220
+ "Tipo": "Alta",
221
+ "Fecha": emp.fecha_alta.isoformat() if emp.fecha_alta else "",
222
+ "Código": emp.codigo_empleado,
223
+ "Nombre": f"{emp.primer_nombre} {emp.primer_apellido}",
224
+ "Cargo": emp.cargo or "",
225
+ "Área": emp.area or "",
226
+ }
227
+ )
228
+
229
+ # Add terminations
230
+ for emp in terminations:
231
+ results.append(
232
+ {
233
+ "Tipo": "Baja",
234
+ "Fecha": emp.fecha_baja.isoformat() if emp.fecha_baja else "",
235
+ "Código": emp.codigo_empleado,
236
+ "Nombre": f"{emp.primer_nombre} {emp.primer_apellido}",
237
+ "Cargo": emp.cargo or "",
238
+ "Área": emp.area or "",
239
+ }
240
+ )
241
+
242
+ # Sort by date
243
+ results.sort(key=lambda x: x["Fecha"])
244
+
245
+ return results
246
+
247
+
248
+ # ============================================================================
249
+ # Payroll Reports
250
+ # ============================================================================
251
+
252
+
253
+ @register_system_report("payroll_by_period")
254
+ def payroll_by_period_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
255
+ """Payroll summary by period.
256
+
257
+ Parameters:
258
+ - periodo_inicio: Start date of period
259
+ - periodo_fin: End date of period
260
+ - planilla_id (optional): Filter by payroll template
261
+ """
262
+ stmt = db.select(Nomina)
263
+
264
+ if "periodo_inicio" in parameters:
265
+ fecha = parameters["periodo_inicio"]
266
+ if isinstance(fecha, str):
267
+ fecha = datetime.fromisoformat(fecha).date()
268
+ stmt = stmt.filter(Nomina.periodo_inicio >= fecha)
269
+
270
+ if "periodo_fin" in parameters:
271
+ fecha = parameters["periodo_fin"]
272
+ if isinstance(fecha, str):
273
+ fecha = datetime.fromisoformat(fecha).date()
274
+ stmt = stmt.filter(Nomina.periodo_fin <= fecha)
275
+
276
+ if "planilla_id" in parameters:
277
+ stmt = stmt.filter(Nomina.planilla_id == parameters["planilla_id"])
278
+
279
+ results = db.session.execute(stmt.order_by(Nomina.periodo_inicio.desc())).scalars().all()
280
+
281
+ return [
282
+ {
283
+ "Código": nomina.codigo_nomina,
284
+ "Descripción": nomina.descripcion or "",
285
+ "Período Inicio": nomina.periodo_inicio.isoformat() if nomina.periodo_inicio else "",
286
+ "Período Fin": nomina.periodo_fin.isoformat() if nomina.periodo_fin else "",
287
+ "Fecha Pago": nomina.fecha_pago.isoformat() if nomina.fecha_pago else "",
288
+ "Estado": nomina.estado,
289
+ "Total Bruto": float(nomina.total_bruto) if nomina.total_bruto else 0.0,
290
+ "Total Deducciones": float(nomina.total_deducciones) if nomina.total_deducciones else 0.0,
291
+ "Total Neto": float(nomina.total_neto) if nomina.total_neto else 0.0,
292
+ }
293
+ for nomina in results
294
+ ]
295
+
296
+
297
+ @register_system_report("payroll_employee_detail")
298
+ def payroll_employee_detail_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
299
+ """Detailed payroll by employee.
300
+
301
+ Parameters:
302
+ - nomina_id: Payroll run ID
303
+ """
304
+ nomina_id = parameters.get("nomina_id")
305
+
306
+ if not nomina_id:
307
+ return []
308
+
309
+ results = db.session.execute(
310
+ db.select(NominaEmpleado, Empleado)
311
+ .join(Empleado, NominaEmpleado.empleado_id == Empleado.id)
312
+ .filter(NominaEmpleado.nomina_id == nomina_id)
313
+ .order_by(Empleado.primer_apellido, Empleado.primer_nombre)
314
+ ).all()
315
+
316
+ return [
317
+ {
318
+ "Código Empleado": empleado.codigo_empleado,
319
+ "Nombre": f"{empleado.primer_nombre} {empleado.primer_apellido}",
320
+ "Salario Base": float(nomina_emp.salario_base) if nomina_emp.salario_base else 0.0,
321
+ "Total Percepciones": float(nomina_emp.total_percepciones) if nomina_emp.total_percepciones else 0.0,
322
+ "Salario Bruto": float(nomina_emp.salario_bruto) if nomina_emp.salario_bruto else 0.0,
323
+ "Total Deducciones": float(nomina_emp.total_deducciones) if nomina_emp.total_deducciones else 0.0,
324
+ "Salario Neto": float(nomina_emp.salario_neto) if nomina_emp.salario_neto else 0.0,
325
+ "Total Prestaciones": float(nomina_emp.total_prestaciones) if nomina_emp.total_prestaciones else 0.0,
326
+ }
327
+ for nomina_emp, empleado in results
328
+ ]
329
+
330
+
331
+ @register_system_report("payroll_perceptions_summary")
332
+ def payroll_perceptions_summary_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
333
+ """Summary of perceptions by period.
334
+
335
+ Parameters:
336
+ - nomina_id: Payroll run ID
337
+ """
338
+ nomina_id = parameters.get("nomina_id")
339
+
340
+ if not nomina_id:
341
+ return []
342
+
343
+ results = db.session.execute(
344
+ db.select(
345
+ NominaDetalle.concepto_codigo,
346
+ NominaDetalle.concepto_nombre,
347
+ func.count(NominaDetalle.id).label("cantidad_empleados"),
348
+ func.sum(NominaDetalle.monto).label("total"),
349
+ )
350
+ .filter(NominaDetalle.nomina_id == nomina_id, NominaDetalle.tipo == TipoDetalle.INGRESO)
351
+ .group_by(NominaDetalle.concepto_codigo, NominaDetalle.concepto_nombre)
352
+ .order_by(NominaDetalle.concepto_codigo)
353
+ ).all()
354
+
355
+ return [
356
+ {
357
+ "Código Concepto": row.concepto_codigo,
358
+ "Concepto": row.concepto_nombre,
359
+ "Cantidad Empleados": row.cantidad_empleados,
360
+ "Total": float(row.total) if row.total else 0.0,
361
+ }
362
+ for row in results
363
+ ]
364
+
365
+
366
+ @register_system_report("payroll_deductions_summary")
367
+ def payroll_deductions_summary_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
368
+ """Summary of deductions by period.
369
+
370
+ Parameters:
371
+ - nomina_id: Payroll run ID
372
+ """
373
+ nomina_id = parameters.get("nomina_id")
374
+
375
+ if not nomina_id:
376
+ return []
377
+
378
+ results = db.session.execute(
379
+ db.select(
380
+ NominaDetalle.concepto_codigo,
381
+ NominaDetalle.concepto_nombre,
382
+ func.count(NominaDetalle.id).label("cantidad_empleados"),
383
+ func.sum(NominaDetalle.monto).label("total"),
384
+ )
385
+ .filter(NominaDetalle.nomina_id == nomina_id, NominaDetalle.tipo == TipoDetalle.DEDUCCION)
386
+ .group_by(NominaDetalle.concepto_codigo, NominaDetalle.concepto_nombre)
387
+ .order_by(NominaDetalle.concepto_codigo)
388
+ ).all()
389
+
390
+ return [
391
+ {
392
+ "Código Concepto": row.concepto_codigo,
393
+ "Concepto": row.concepto_nombre,
394
+ "Cantidad Empleados": row.cantidad_empleados,
395
+ "Total": float(row.total) if row.total else 0.0,
396
+ }
397
+ for row in results
398
+ ]
399
+
400
+
401
+ # ============================================================================
402
+ # Vacation Reports
403
+ # ============================================================================
404
+
405
+
406
+ @register_system_report("vacation_balance_by_employee")
407
+ def vacation_balance_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
408
+ """Vacation balance by employee.
409
+
410
+ Shows current vacation balances for all employees.
411
+ """
412
+ results = db.session.execute(
413
+ db.select(VacationAccount, Empleado)
414
+ .join(Empleado, VacationAccount.empleado_id == Empleado.id)
415
+ .filter(Empleado.activo == True) # noqa: E712
416
+ .order_by(Empleado.primer_apellido, Empleado.primer_nombre)
417
+ ).all()
418
+
419
+ return [
420
+ {
421
+ "Código Empleado": empleado.codigo_empleado,
422
+ "Nombre": f"{empleado.primer_nombre} {empleado.primer_apellido}",
423
+ "Días Acumulados": float(account.accrued_days) if account.accrued_days else 0.0,
424
+ "Días Usados": float(account.used_days) if account.used_days else 0.0,
425
+ "Balance Días": float(account.balance_days) if account.balance_days else 0.0,
426
+ }
427
+ for account, empleado in results
428
+ ]
429
+
430
+
431
+ @register_system_report("vacation_taken_by_period")
432
+ def vacation_taken_by_period_report(parameters: Dict[str, Any]) -> List[Dict[str, Any]]:
433
+ """Vacations taken by period.
434
+
435
+ Parameters:
436
+ - fecha_inicio: Start date
437
+ - fecha_fin: End date
438
+ """
439
+ fecha_inicio = parameters.get("fecha_inicio")
440
+ fecha_fin = parameters.get("fecha_fin")
441
+
442
+ if isinstance(fecha_inicio, str):
443
+ fecha_inicio = datetime.fromisoformat(fecha_inicio).date()
444
+ if isinstance(fecha_fin, str):
445
+ fecha_fin = datetime.fromisoformat(fecha_fin).date()
446
+
447
+ stmt = (
448
+ db.select(VacationLedger, Empleado)
449
+ .join(Empleado, VacationLedger.empleado_id == Empleado.id)
450
+ .filter(VacationLedger.entry_type == "usage")
451
+ )
452
+
453
+ if fecha_inicio:
454
+ stmt = stmt.filter(VacationLedger.entry_date >= fecha_inicio)
455
+ if fecha_fin:
456
+ stmt = stmt.filter(VacationLedger.entry_date <= fecha_fin)
457
+
458
+ results = db.session.execute(stmt.order_by(VacationLedger.entry_date.desc())).all()
459
+
460
+ return [
461
+ {
462
+ "Fecha": entry.entry_date.isoformat() if entry.entry_date else "",
463
+ "Código Empleado": empleado.codigo_empleado,
464
+ "Nombre": f"{empleado.primer_nombre} {empleado.primer_apellido}",
465
+ "Días Usados": float(abs(entry.days_change)) if entry.days_change else 0.0,
466
+ "Descripción": entry.description or "",
467
+ }
468
+ for entry, empleado in results
469
+ ]
470
+
471
+
472
+ # ============================================================================
473
+ # Report Metadata
474
+ # ============================================================================
475
+
476
+ # Metadata for each system report
477
+ SYSTEM_REPORT_METADATA = {
478
+ "employee_list": {
479
+ "name": "Listado General de Empleados",
480
+ "description": "Lista completa de empleados con información básica",
481
+ "category": "employee",
482
+ "base_entity": "Employee",
483
+ "parameters": [
484
+ {"name": "activo", "type": "boolean", "required": False},
485
+ {"name": "empresa_id", "type": "string", "required": False},
486
+ ],
487
+ },
488
+ "employee_active_inactive": {
489
+ "name": "Empleados Activos e Inactivos",
490
+ "description": "Reporte de estado de empleados activos e inactivos",
491
+ "category": "employee",
492
+ "base_entity": "Employee",
493
+ "parameters": [],
494
+ },
495
+ "employee_by_department": {
496
+ "name": "Empleados por Departamento",
497
+ "description": "Listado de empleados agrupados por área o departamento",
498
+ "category": "employee",
499
+ "base_entity": "Employee",
500
+ "parameters": [],
501
+ },
502
+ "employee_hires_terminations": {
503
+ "name": "Altas y Bajas de Empleados",
504
+ "description": "Reporte de contrataciones y terminaciones por período",
505
+ "category": "employee",
506
+ "base_entity": "Employee",
507
+ "parameters": [
508
+ {"name": "fecha_inicio", "type": "date", "required": True},
509
+ {"name": "fecha_fin", "type": "date", "required": True},
510
+ ],
511
+ },
512
+ "payroll_by_period": {
513
+ "name": "Nómina por Período",
514
+ "description": "Resumen de nóminas por período de pago",
515
+ "category": "payroll",
516
+ "base_entity": "Nomina",
517
+ "parameters": [
518
+ {"name": "periodo_inicio", "type": "date", "required": False},
519
+ {"name": "periodo_fin", "type": "date", "required": False},
520
+ {"name": "planilla_id", "type": "string", "required": False},
521
+ ],
522
+ },
523
+ "payroll_employee_detail": {
524
+ "name": "Detalle de Nómina por Empleado",
525
+ "description": "Detalle completo de nómina desglosado por empleado",
526
+ "category": "payroll",
527
+ "base_entity": "NominaEmpleado",
528
+ "parameters": [{"name": "nomina_id", "type": "string", "required": True}],
529
+ },
530
+ "payroll_perceptions_summary": {
531
+ "name": "Resumen de Percepciones",
532
+ "description": "Resumen agregado de percepciones por concepto",
533
+ "category": "payroll",
534
+ "base_entity": "NominaDetalle",
535
+ "parameters": [{"name": "nomina_id", "type": "string", "required": True}],
536
+ },
537
+ "payroll_deductions_summary": {
538
+ "name": "Resumen de Deducciones",
539
+ "description": "Resumen agregado de deducciones por concepto",
540
+ "category": "payroll",
541
+ "base_entity": "NominaDetalle",
542
+ "parameters": [{"name": "nomina_id", "type": "string", "required": True}],
543
+ },
544
+ "vacation_balance_by_employee": {
545
+ "name": "Balance de Vacaciones por Empleado",
546
+ "description": "Saldo actual de vacaciones para cada empleado",
547
+ "category": "vacation",
548
+ "base_entity": "VacationAccount",
549
+ "parameters": [],
550
+ },
551
+ "vacation_taken_by_period": {
552
+ "name": "Vacaciones Tomadas por Período",
553
+ "description": "Registro de vacaciones utilizadas en un período",
554
+ "category": "vacation",
555
+ "base_entity": "VacationLedger",
556
+ "parameters": [
557
+ {"name": "fecha_inicio", "type": "date", "required": True},
558
+ {"name": "fecha_fin", "type": "date", "required": True},
559
+ ],
560
+ },
561
+ }
562
+
563
+
564
+ def get_system_report_metadata(report_id: str) -> Optional[Dict[str, Any]]:
565
+ """Get metadata for a system report.
566
+
567
+ Args:
568
+ report_id: System report identifier
569
+
570
+ Returns:
571
+ Report metadata or None
572
+ """
573
+ return SYSTEM_REPORT_METADATA.get(report_id)