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,813 @@
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
+ """Demo data loading for manual testing.
15
+
16
+ This module provides comprehensive sample data to facilitate manual testing
17
+ of the payroll system. Data is loaded when COATI_LOAD_DEMO_DATA environment
18
+ variable is set.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ # <-------------------------------------------------------------------------> #
24
+ # Standard library
25
+ # <-------------------------------------------------------------------------> #
26
+ from datetime import date, timedelta
27
+ from decimal import Decimal
28
+ from dateutil.relativedelta import relativedelta
29
+
30
+ # <-------------------------------------------------------------------------> #
31
+ # Third party libraries
32
+ # <-------------------------------------------------------------------------> #
33
+
34
+ # <-------------------------------------------------------------------------> #
35
+ # Local modules
36
+ # <-------------------------------------------------------------------------> #
37
+ from coati_payroll.model import (
38
+ db,
39
+ Empresa,
40
+ Empleado,
41
+ Moneda,
42
+ TipoPlanilla,
43
+ Planilla,
44
+ PlanillaEmpleado,
45
+ PlanillaIngreso,
46
+ PlanillaDeduccion,
47
+ PlanillaPrestacion,
48
+ Percepcion,
49
+ Deduccion,
50
+ Prestacion,
51
+ Nomina,
52
+ NominaNovedad,
53
+ )
54
+ from coati_payroll.log import log
55
+
56
+
57
+ def load_demo_companies() -> tuple[Empresa, Empresa]:
58
+ """Create two demo companies with complete information.
59
+
60
+ Returns:
61
+ tuple: Two Empresa objects (company1, company2)
62
+ """
63
+ log.trace("Loading demo companies...")
64
+
65
+ empresa1 = db.session.execute(db.select(Empresa).filter_by(codigo="DEMO001")).scalar_one_or_none()
66
+ empresa2 = db.session.execute(db.select(Empresa).filter_by(codigo="DEMO002")).scalar_one_or_none()
67
+
68
+ if empresa1 is None:
69
+ empresa1 = Empresa()
70
+ empresa1.codigo = "DEMO001"
71
+ empresa1.razon_social = "Tecnología y Soluciones S.A."
72
+ empresa1.nombre_comercial = "TechSol"
73
+ empresa1.ruc = "J0310000123456"
74
+ empresa1.direccion = "Km 7.5 Carretera a Masaya, Managua"
75
+ empresa1.telefono = "+505 2255-1234"
76
+ empresa1.correo = "info@techsol-demo.com"
77
+ empresa1.sitio_web = "www.techsol-demo.com"
78
+ empresa1.representante_legal = "María Elena Gutiérrez"
79
+ empresa1.activo = True
80
+ db.session.add(empresa1)
81
+ log.trace("Created demo company: Tecnología y Soluciones S.A.")
82
+
83
+ if empresa2 is None:
84
+ empresa2 = Empresa()
85
+ empresa2.codigo = "DEMO002"
86
+ empresa2.razon_social = "Servicios Profesionales BMO Ltda."
87
+ empresa2.nombre_comercial = "BMO Services"
88
+ empresa2.ruc = "J0310000654321"
89
+ empresa2.direccion = "Plaza España, módulo 5, Managua"
90
+ empresa2.telefono = "+505 2277-5678"
91
+ empresa2.correo = "contacto@bmo-services-demo.com"
92
+ empresa2.sitio_web = "www.bmo-services-demo.com"
93
+ empresa2.representante_legal = "Carlos Antonio Ramírez"
94
+ empresa2.activo = True
95
+ db.session.add(empresa2)
96
+ log.trace("Created demo company: Servicios Profesionales BMO Ltda.")
97
+
98
+ db.session.commit()
99
+ return empresa1, empresa2
100
+
101
+
102
+ def load_demo_employees(empresa1: Empresa, empresa2: Empresa) -> list[Empleado]:
103
+ """Create diverse demo employees for testing.
104
+
105
+ Creates 15 employees with varied:
106
+ - Salaries (from minimum wage to executive level)
107
+ - Positions and departments
108
+ - Contract types and employment dates
109
+ - Personal information
110
+
111
+ Args:
112
+ empresa1: First demo company
113
+ empresa2: Second demo company
114
+
115
+ Returns:
116
+ list: List of created Empleado objects
117
+ """
118
+ log.trace("Loading demo employees...")
119
+
120
+ # Get currency (use NIO if available, otherwise first available)
121
+ moneda = db.session.execute(db.select(Moneda).filter_by(codigo="NIO")).scalar_one_or_none()
122
+
123
+ if moneda is None:
124
+ moneda = db.session.execute(db.select(Moneda)).scalars().first()
125
+
126
+ if moneda is None:
127
+ log.trace("No currency available for demo employees")
128
+ return []
129
+
130
+ empleados_data = [
131
+ # Company 1 - Tecnología y Soluciones
132
+ {
133
+ "codigo": "DEMO-EMP001",
134
+ "primer_nombre": "Juan",
135
+ "segundo_nombre": "Carlos",
136
+ "primer_apellido": "Pérez",
137
+ "segundo_apellido": "González",
138
+ "identificacion_personal": "001-150890-0001K",
139
+ "genero": "Masculino",
140
+ "nacionalidad": "Nicaragüense",
141
+ "fecha_nacimiento": date(1990, 8, 15),
142
+ "cargo": "Gerente de Tecnología",
143
+ "area": "Tecnología",
144
+ "salario_base": Decimal("35000.00"),
145
+ "tipo_contrato": "Indefinido",
146
+ "fecha_alta": date.today() - timedelta(days=730), # 2 years ago
147
+ "empresa": empresa1,
148
+ "correo": "jperez@techsol-demo.com",
149
+ "telefono": "+505 8888-1234",
150
+ },
151
+ {
152
+ "codigo": "DEMO-EMP002",
153
+ "primer_nombre": "María",
154
+ "segundo_nombre": "Elena",
155
+ "primer_apellido": "Rodríguez",
156
+ "segundo_apellido": "Morales",
157
+ "identificacion_personal": "001-220685-0002M",
158
+ "genero": "Femenino",
159
+ "nacionalidad": "Nicaragüense",
160
+ "fecha_nacimiento": date(1985, 6, 22),
161
+ "cargo": "Desarrolladora Senior",
162
+ "area": "Tecnología",
163
+ "salario_base": Decimal("28000.00"),
164
+ "tipo_contrato": "Indefinido",
165
+ "fecha_alta": date.today() - timedelta(days=1095), # 3 years ago
166
+ "empresa": empresa1,
167
+ "correo": "mrodriguez@techsol-demo.com",
168
+ "telefono": "+505 8888-2345",
169
+ },
170
+ {
171
+ "codigo": "DEMO-EMP003",
172
+ "primer_nombre": "Carlos",
173
+ "segundo_nombre": "Alberto",
174
+ "primer_apellido": "Martínez",
175
+ "segundo_apellido": "López",
176
+ "identificacion_personal": "001-100992-0003N",
177
+ "genero": "Masculino",
178
+ "nacionalidad": "Nicaragüense",
179
+ "fecha_nacimiento": date(1992, 9, 10),
180
+ "cargo": "Desarrollador Junior",
181
+ "area": "Tecnología",
182
+ "salario_base": Decimal("15000.00"),
183
+ "tipo_contrato": "Temporal",
184
+ "fecha_alta": date.today() - timedelta(days=180), # 6 months ago
185
+ "empresa": empresa1,
186
+ "correo": "cmartinez@techsol-demo.com",
187
+ "telefono": "+505 8888-3456",
188
+ },
189
+ {
190
+ "codigo": "DEMO-EMP004",
191
+ "primer_nombre": "Ana",
192
+ "segundo_nombre": "Patricia",
193
+ "primer_apellido": "García",
194
+ "segundo_apellido": "Hernández",
195
+ "identificacion_personal": "001-050888-0004L",
196
+ "genero": "Femenino",
197
+ "nacionalidad": "Nicaragüense",
198
+ "fecha_nacimiento": date(1988, 8, 5),
199
+ "cargo": "Contador",
200
+ "area": "Finanzas",
201
+ "salario_base": Decimal("22000.00"),
202
+ "tipo_contrato": "Indefinido",
203
+ "fecha_alta": date.today() - timedelta(days=1460), # 4 years ago
204
+ "empresa": empresa1,
205
+ "correo": "agarcia@techsol-demo.com",
206
+ "telefono": "+505 8888-4567",
207
+ },
208
+ {
209
+ "codigo": "DEMO-EMP005",
210
+ "primer_nombre": "Roberto",
211
+ "segundo_nombre": "José",
212
+ "primer_apellido": "Flores",
213
+ "segundo_apellido": "Gutiérrez",
214
+ "identificacion_personal": "001-181195-0005P",
215
+ "genero": "Masculino",
216
+ "nacionalidad": "Nicaragüense",
217
+ "fecha_nacimiento": date(1995, 11, 18),
218
+ "cargo": "Asistente Administrativo",
219
+ "area": "Administración",
220
+ "salario_base": Decimal("12000.00"),
221
+ "tipo_contrato": "Indefinido",
222
+ "fecha_alta": date.today() - timedelta(days=365), # 1 year ago
223
+ "empresa": empresa1,
224
+ "correo": "rflores@techsol-demo.com",
225
+ "telefono": "+505 8888-5678",
226
+ },
227
+ {
228
+ "codigo": "DEMO-EMP006",
229
+ "primer_nombre": "Laura",
230
+ "segundo_nombre": "Isabel",
231
+ "primer_apellido": "Ramírez",
232
+ "segundo_apellido": "Castro",
233
+ "identificacion_personal": "001-250687-0006R",
234
+ "genero": "Femenino",
235
+ "nacionalidad": "Nicaragüense",
236
+ "fecha_nacimiento": date(1987, 6, 25),
237
+ "cargo": "Jefa de Recursos Humanos",
238
+ "area": "Recursos Humanos",
239
+ "salario_base": Decimal("26000.00"),
240
+ "tipo_contrato": "Indefinido",
241
+ "fecha_alta": date.today() - timedelta(days=900), # ~2.5 years ago
242
+ "empresa": empresa1,
243
+ "correo": "lramirez@techsol-demo.com",
244
+ "telefono": "+505 8888-6789",
245
+ },
246
+ {
247
+ "codigo": "DEMO-EMP007",
248
+ "primer_nombre": "Diego",
249
+ "segundo_nombre": "Andrés",
250
+ "primer_apellido": "Sánchez",
251
+ "segundo_apellido": "Vargas",
252
+ "identificacion_personal": "001-120993-0007T",
253
+ "genero": "Masculino",
254
+ "nacionalidad": "Nicaragüense",
255
+ "fecha_nacimiento": date(1993, 9, 12),
256
+ "cargo": "Analista de Soporte",
257
+ "area": "Tecnología",
258
+ "salario_base": Decimal("18000.00"),
259
+ "tipo_contrato": "Indefinido",
260
+ "fecha_alta": date.today() - timedelta(days=545), # ~1.5 years ago
261
+ "empresa": empresa1,
262
+ "correo": "dsanchez@techsol-demo.com",
263
+ "telefono": "+505 8888-7890",
264
+ },
265
+ {
266
+ "codigo": "DEMO-EMP008",
267
+ "primer_nombre": "Gabriela",
268
+ "segundo_nombre": "Sofía",
269
+ "primer_apellido": "Mendoza",
270
+ "segundo_apellido": "Ortiz",
271
+ "identificacion_personal": "001-080391-0008U",
272
+ "genero": "Femenino",
273
+ "nacionalidad": "Nicaragüense",
274
+ "fecha_nacimiento": date(1991, 3, 8),
275
+ "cargo": "Diseñadora UX/UI",
276
+ "area": "Tecnología",
277
+ "salario_base": Decimal("20000.00"),
278
+ "tipo_contrato": "Temporal",
279
+ "fecha_alta": date.today() - timedelta(days=270), # 9 months ago
280
+ "empresa": empresa1,
281
+ "correo": "gmendoza@techsol-demo.com",
282
+ "telefono": "+505 8888-8901",
283
+ },
284
+ # Company 2 - Servicios Profesionales BMO
285
+ {
286
+ "codigo": "DEMO-EMP009",
287
+ "primer_nombre": "Fernando",
288
+ "segundo_nombre": "Luis",
289
+ "primer_apellido": "Torres",
290
+ "segundo_apellido": "Ruiz",
291
+ "identificacion_personal": "001-301284-0009V",
292
+ "genero": "Masculino",
293
+ "nacionalidad": "Nicaragüense",
294
+ "fecha_nacimiento": date(1984, 12, 30),
295
+ "cargo": "Director General",
296
+ "area": "Dirección",
297
+ "salario_base": Decimal("50000.00"),
298
+ "tipo_contrato": "Indefinido",
299
+ "fecha_alta": date.today() - timedelta(days=1825), # 5 years ago
300
+ "empresa": empresa2,
301
+ "correo": "ftorres@bmo-services-demo.com",
302
+ "telefono": "+505 7777-1234",
303
+ },
304
+ {
305
+ "codigo": "DEMO-EMP010",
306
+ "primer_nombre": "Patricia",
307
+ "segundo_nombre": "Mercedes",
308
+ "primer_apellido": "Jiménez",
309
+ "segundo_apellido": "Silva",
310
+ "identificacion_personal": "001-140689-0010W",
311
+ "genero": "Femenino",
312
+ "nacionalidad": "Nicaragüense",
313
+ "fecha_nacimiento": date(1989, 6, 14),
314
+ "cargo": "Consultora Senior",
315
+ "area": "Consultoría",
316
+ "salario_base": Decimal("32000.00"),
317
+ "tipo_contrato": "Indefinido",
318
+ "fecha_alta": date.today() - timedelta(days=1095), # 3 years ago
319
+ "empresa": empresa2,
320
+ "correo": "pjimenez@bmo-services-demo.com",
321
+ "telefono": "+505 7777-2345",
322
+ },
323
+ {
324
+ "codigo": "DEMO-EMP011",
325
+ "primer_nombre": "Miguel",
326
+ "segundo_nombre": "Ángel",
327
+ "primer_apellido": "Herrera",
328
+ "segundo_apellido": "Díaz",
329
+ "identificacion_personal": "001-221090-0011X",
330
+ "genero": "Masculino",
331
+ "nacionalidad": "Nicaragüense",
332
+ "fecha_nacimiento": date(1990, 10, 22),
333
+ "cargo": "Consultor",
334
+ "area": "Consultoría",
335
+ "salario_base": Decimal("24000.00"),
336
+ "tipo_contrato": "Indefinido",
337
+ "fecha_alta": date.today() - timedelta(days=730), # 2 years ago
338
+ "empresa": empresa2,
339
+ "correo": "mherrera@bmo-services-demo.com",
340
+ "telefono": "+505 7777-3456",
341
+ },
342
+ {
343
+ "codigo": "DEMO-EMP012",
344
+ "primer_nombre": "Claudia",
345
+ "segundo_nombre": "Beatriz",
346
+ "primer_apellido": "Moreno",
347
+ "segundo_apellido": "Rivas",
348
+ "identificacion_personal": "001-190992-0012Y",
349
+ "genero": "Femenino",
350
+ "nacionalidad": "Nicaragüense",
351
+ "fecha_nacimiento": date(1992, 9, 19),
352
+ "cargo": "Asistente de Consultoría",
353
+ "area": "Consultoría",
354
+ "salario_base": Decimal("16000.00"),
355
+ "tipo_contrato": "Temporal",
356
+ "fecha_alta": date.today() - timedelta(days=365), # 1 year ago
357
+ "empresa": empresa2,
358
+ "correo": "cmoreno@bmo-services-demo.com",
359
+ "telefono": "+505 7777-4567",
360
+ },
361
+ {
362
+ "codigo": "DEMO-EMP013",
363
+ "primer_nombre": "Sergio",
364
+ "segundo_nombre": "Rafael",
365
+ "primer_apellido": "Vega",
366
+ "segundo_apellido": "Campos",
367
+ "identificacion_personal": "001-051286-0013Z",
368
+ "genero": "Masculino",
369
+ "nacionalidad": "Nicaragüense",
370
+ "fecha_nacimiento": date(1986, 12, 5),
371
+ "cargo": "Contador General",
372
+ "area": "Finanzas",
373
+ "salario_base": Decimal("28000.00"),
374
+ "tipo_contrato": "Indefinido",
375
+ "fecha_alta": date.today() - timedelta(days=1460), # 4 years ago
376
+ "empresa": empresa2,
377
+ "correo": "svega@bmo-services-demo.com",
378
+ "telefono": "+505 7777-5678",
379
+ },
380
+ {
381
+ "codigo": "DEMO-EMP014",
382
+ "primer_nombre": "Lucía",
383
+ "segundo_nombre": "Fernanda",
384
+ "primer_apellido": "Navarro",
385
+ "segundo_apellido": "Pérez",
386
+ "identificacion_personal": "001-280894-0014A",
387
+ "genero": "Femenino",
388
+ "nacionalidad": "Nicaragüense",
389
+ "fecha_nacimiento": date(1994, 8, 28),
390
+ "cargo": "Recepcionista",
391
+ "area": "Administración",
392
+ "salario_base": Decimal("11000.00"),
393
+ "tipo_contrato": "Indefinido",
394
+ "fecha_alta": date.today() - timedelta(days=545), # ~1.5 years ago
395
+ "empresa": empresa2,
396
+ "correo": "lnavarro@bmo-services-demo.com",
397
+ "telefono": "+505 7777-6789",
398
+ },
399
+ {
400
+ "codigo": "DEMO-EMP015",
401
+ "primer_nombre": "Andrés",
402
+ "segundo_nombre": "Mauricio",
403
+ "primer_apellido": "Cruz",
404
+ "segundo_apellido": "Aguilar",
405
+ "identificacion_personal": "001-170791-0015B",
406
+ "genero": "Masculino",
407
+ "nacionalidad": "Nicaragüense",
408
+ "fecha_nacimiento": date(1991, 7, 17),
409
+ "cargo": "Analista Financiero",
410
+ "area": "Finanzas",
411
+ "salario_base": Decimal("19000.00"),
412
+ "tipo_contrato": "Indefinido",
413
+ "fecha_alta": date.today() - timedelta(days=630), # ~1.7 years ago
414
+ "empresa": empresa2,
415
+ "correo": "acruz@bmo-services-demo.com",
416
+ "telefono": "+505 7777-7890",
417
+ },
418
+ ]
419
+
420
+ empleados = []
421
+ for emp_data in empleados_data:
422
+ # Check if employee already exists
423
+ existing = db.session.execute(
424
+ db.select(Empleado).filter_by(codigo_empleado=emp_data["codigo"])
425
+ ).scalar_one_or_none()
426
+
427
+ if existing is None:
428
+ empleado = Empleado()
429
+ empleado.codigo_empleado = emp_data["codigo"]
430
+ empleado.primer_nombre = emp_data["primer_nombre"]
431
+ empleado.segundo_nombre = emp_data.get("segundo_nombre")
432
+ empleado.primer_apellido = emp_data["primer_apellido"]
433
+ empleado.segundo_apellido = emp_data.get("segundo_apellido")
434
+ empleado.identificacion_personal = emp_data["identificacion_personal"]
435
+ empleado.genero = emp_data.get("genero")
436
+ empleado.nacionalidad = emp_data.get("nacionalidad")
437
+ empleado.tipo_identificacion = "Cédula"
438
+ empleado.fecha_nacimiento = emp_data.get("fecha_nacimiento")
439
+ empleado.fecha_alta = emp_data["fecha_alta"]
440
+ empleado.activo = True
441
+ empleado.cargo = emp_data.get("cargo")
442
+ empleado.area = emp_data.get("area")
443
+ empleado.salario_base = emp_data["salario_base"]
444
+ empleado.tipo_contrato = emp_data.get("tipo_contrato")
445
+ empleado.moneda_id = moneda.id
446
+ empleado.empresa_id = emp_data["empresa"].id
447
+ empleado.correo = emp_data.get("correo")
448
+ empleado.telefono = emp_data.get("telefono")
449
+ empleado.estado_civil = "Soltero"
450
+
451
+ db.session.add(empleado)
452
+ empleados.append(empleado)
453
+ log.trace(f"Created demo employee: {empleado.primer_nombre} {empleado.primer_apellido}")
454
+ else:
455
+ empleados.append(existing)
456
+
457
+ db.session.commit()
458
+ return empleados
459
+
460
+
461
+ def load_demo_payrolls(empresa1: Empresa, empresa2: Empresa, empleados: list[Empleado]) -> tuple[Planilla, Planilla]:
462
+ """Create demo payrolls with assigned employees and concepts.
463
+
464
+ Args:
465
+ empresa1: First demo company
466
+ empresa2: Second demo company
467
+ empleados: List of demo employees
468
+
469
+ Returns:
470
+ tuple: Two Planilla objects (planilla1, planilla2)
471
+ """
472
+ log.trace("Loading demo payrolls...")
473
+
474
+ # Get or create required data
475
+ moneda = db.session.execute(db.select(Moneda).filter_by(codigo="NIO")).scalar_one_or_none()
476
+
477
+ if moneda is None:
478
+ moneda = db.session.execute(db.select(Moneda)).scalars().first()
479
+
480
+ tipo_planilla = db.session.execute(db.select(TipoPlanilla).filter_by(codigo="MONTHLY")).scalar_one_or_none()
481
+
482
+ if tipo_planilla is None:
483
+ tipo_planilla = db.session.execute(db.select(TipoPlanilla)).scalars().first()
484
+
485
+ if moneda is None or tipo_planilla is None:
486
+ log.trace("Required data (currency or payroll type) not available")
487
+ return None, None
488
+
489
+ # Create Planilla 1 for Company 1
490
+ planilla1 = db.session.execute(db.select(Planilla).filter_by(nombre="Planilla Demo - TechSol")).scalar_one_or_none()
491
+
492
+ if planilla1 is None:
493
+ planilla1 = Planilla()
494
+ planilla1.nombre = "Planilla Demo - TechSol"
495
+ planilla1.descripcion = "Planilla de demostración para Tecnología y Soluciones S.A."
496
+ planilla1.tipo_planilla_id = tipo_planilla.id
497
+ planilla1.moneda_id = moneda.id
498
+ planilla1.empresa_id = empresa1.id
499
+ planilla1.activo = True
500
+ db.session.add(planilla1)
501
+ log.trace("Created demo payroll: Planilla Demo - TechSol")
502
+
503
+ # Create Planilla 2 for Company 2
504
+ planilla2 = db.session.execute(
505
+ db.select(Planilla).filter_by(nombre="Planilla Demo - BMO Services")
506
+ ).scalar_one_or_none()
507
+
508
+ if planilla2 is None:
509
+ planilla2 = Planilla()
510
+ planilla2.nombre = "Planilla Demo - BMO Services"
511
+ planilla2.descripcion = "Planilla de demostración para Servicios Profesionales BMO Ltda."
512
+ planilla2.tipo_planilla_id = tipo_planilla.id
513
+ planilla2.moneda_id = moneda.id
514
+ planilla2.empresa_id = empresa2.id
515
+ planilla2.activo = True
516
+ db.session.add(planilla2)
517
+ log.trace("Created demo payroll: Planilla Demo - BMO Services")
518
+
519
+ db.session.commit()
520
+
521
+ # Assign employees to payrolls
522
+ for empleado in empleados:
523
+ if empleado.empresa_id == empresa1.id:
524
+ planilla = planilla1
525
+ elif empleado.empresa_id == empresa2.id:
526
+ planilla = planilla2
527
+ else:
528
+ continue
529
+
530
+ # Check if already assigned
531
+ existing = db.session.execute(
532
+ db.select(PlanillaEmpleado).filter_by(planilla_id=planilla.id, empleado_id=empleado.id)
533
+ ).scalar_one_or_none()
534
+
535
+ if existing is None:
536
+ asignacion = PlanillaEmpleado()
537
+ asignacion.planilla_id = planilla.id
538
+ asignacion.empleado_id = empleado.id
539
+ asignacion.activo = True
540
+ asignacion.fecha_inicio = empleado.fecha_alta
541
+ db.session.add(asignacion)
542
+ log.trace(f"Assigned employee {empleado.codigo_empleado} to payroll {planilla.nombre}")
543
+
544
+ db.session.commit()
545
+
546
+ # Assign perceptions, deductions, and benefits
547
+ _assign_concepts_to_payroll(planilla1)
548
+ _assign_concepts_to_payroll(planilla2)
549
+
550
+ return planilla1, planilla2
551
+
552
+
553
+ def _assign_concepts_to_payroll(planilla: Planilla) -> None:
554
+ """Assign common perceptions, deductions, and benefits to a payroll.
555
+
556
+ Args:
557
+ planilla: Planilla to assign concepts to
558
+ """
559
+ # Get some common concepts
560
+ percepciones = (
561
+ db.session.execute(
562
+ db.select(Percepcion).filter(Percepcion.codigo.in_(["OVERTIME", "BONUSES", "TRANSPORT_ALLOWANCE"]))
563
+ )
564
+ .scalars()
565
+ .all()
566
+ )
567
+
568
+ for percepcion in percepciones:
569
+ existing = db.session.execute(
570
+ db.select(PlanillaIngreso).filter_by(planilla_id=planilla.id, percepcion_id=percepcion.id)
571
+ ).scalar_one_or_none()
572
+
573
+ if existing is None:
574
+ asignacion = PlanillaIngreso()
575
+ asignacion.planilla_id = planilla.id
576
+ asignacion.percepcion_id = percepcion.id
577
+ asignacion.activo = True
578
+ asignacion.editable = True
579
+ db.session.add(asignacion)
580
+
581
+ # Assign deductions
582
+ deducciones = (
583
+ db.session.execute(
584
+ db.select(Deduccion).filter(Deduccion.codigo.in_(["UNPAID_ABSENCES", "TARDINESS", "INTERNAL_LOANS"]))
585
+ )
586
+ .scalars()
587
+ .all()
588
+ )
589
+
590
+ for idx, deduccion in enumerate(deducciones):
591
+ existing = db.session.execute(
592
+ db.select(PlanillaDeduccion).filter_by(planilla_id=planilla.id, deduccion_id=deduccion.id)
593
+ ).scalar_one_or_none()
594
+
595
+ if existing is None:
596
+ asignacion = PlanillaDeduccion()
597
+ asignacion.planilla_id = planilla.id
598
+ asignacion.deduccion_id = deduccion.id
599
+ asignacion.prioridad = 300 + (idx * 10) # Set priority
600
+ asignacion.activo = True
601
+ asignacion.editable = True
602
+ db.session.add(asignacion)
603
+
604
+ # Assign benefits
605
+ prestaciones = (
606
+ db.session.execute(
607
+ db.select(Prestacion).filter(
608
+ Prestacion.codigo.in_(["PAID_VACATION_PROVISION", "THIRTEENTH_SALARY_PROVISION"])
609
+ )
610
+ )
611
+ .scalars()
612
+ .all()
613
+ )
614
+
615
+ for prestacion in prestaciones:
616
+ existing = db.session.execute(
617
+ db.select(PlanillaPrestacion).filter_by(planilla_id=planilla.id, prestacion_id=prestacion.id)
618
+ ).scalar_one_or_none()
619
+
620
+ if existing is None:
621
+ asignacion = PlanillaPrestacion()
622
+ asignacion.planilla_id = planilla.id
623
+ asignacion.prestacion_id = prestacion.id
624
+ asignacion.activo = True
625
+ asignacion.editable = True
626
+ db.session.add(asignacion)
627
+
628
+ db.session.commit()
629
+
630
+
631
+ def create_demo_nomina(planilla: Planilla) -> Nomina | None:
632
+ """Create a demo payroll run for next month.
633
+
634
+ Creates a nomina with dynamically generated dates to ensure it always
635
+ occurs in the month immediately following the initial application setup date.
636
+
637
+ Args:
638
+ planilla: Planilla to create nomina for
639
+
640
+ Returns:
641
+ Nomina object or None if creation failed
642
+ """
643
+ log.trace(f"Creating demo nomina for payroll: {planilla.nombre}")
644
+
645
+ # Calculate dates for next month
646
+ today = date.today()
647
+ next_month_start = today.replace(day=1) + relativedelta(months=1)
648
+ next_month_end = (next_month_start + relativedelta(months=1)) - timedelta(days=1)
649
+
650
+ # Check if demo nomina already exists for this period
651
+ existing = db.session.execute(
652
+ db.select(Nomina).filter_by(
653
+ planilla_id=planilla.id, periodo_inicio=next_month_start, periodo_fin=next_month_end
654
+ )
655
+ ).scalar_one_or_none()
656
+
657
+ if existing is not None:
658
+ log.trace(f"Demo nomina already exists for period {next_month_start} to {next_month_end}")
659
+ return existing
660
+
661
+ # Create nomina
662
+ nomina = Nomina()
663
+ nomina.planilla_id = planilla.id
664
+ nomina.periodo_inicio = next_month_start
665
+ nomina.periodo_fin = next_month_end
666
+ nomina.estado = "generado"
667
+ nomina.total_bruto = Decimal("0.00")
668
+ nomina.total_deducciones = Decimal("0.00")
669
+ nomina.total_neto = Decimal("0.00")
670
+ db.session.add(nomina)
671
+ db.session.commit()
672
+
673
+ log.trace(f"Created demo nomina for period {next_month_start} to {next_month_end}")
674
+ return nomina
675
+
676
+
677
+ def create_demo_novelties(empleados: list[Empleado]) -> None:
678
+ """Create demo novelties (overtime, absences) for employees.
679
+
680
+ Creates various types of novelties to demonstrate the system's
681
+ capability to handle employee-specific adjustments.
682
+
683
+ Args:
684
+ empleados: List of employees to create novelties for
685
+ """
686
+ log.trace("Creating demo novelties...")
687
+
688
+ if len(empleados) < 5:
689
+ log.trace("Not enough employees to create demo novelties")
690
+ return
691
+
692
+ # Get perceptions for overtime
693
+ overtime = db.session.execute(db.select(Percepcion).filter_by(codigo="OVERTIME")).scalar_one_or_none()
694
+
695
+ # Get deduction for absences
696
+ absence = db.session.execute(db.select(Deduccion).filter_by(codigo="UNPAID_ABSENCES")).scalar_one_or_none()
697
+
698
+ # Get a demo nomina to associate novelties with
699
+ # (We'll use the first available nomina or None)
700
+ nomina = db.session.execute(db.select(Nomina)).scalars().first()
701
+
702
+ if nomina is None:
703
+ log.trace("No nomina available to associate novelties")
704
+ # Novelties can still be created without a nomina (as pending)
705
+
706
+ # Create overtime for some employees
707
+ if overtime:
708
+ for i in [0, 1, 4]: # First, second, and fifth employee
709
+ if i < len(empleados):
710
+ empleado = empleados[i]
711
+
712
+ # Check if novedad already exists
713
+ if nomina:
714
+ existing = db.session.execute(
715
+ db.select(NominaNovedad).filter_by(
716
+ nomina_id=nomina.id, empleado_id=empleado.id, codigo_concepto=overtime.codigo
717
+ )
718
+ ).scalar_one_or_none()
719
+
720
+ if existing is None:
721
+ novedad = NominaNovedad()
722
+ novedad.nomina_id = nomina.id
723
+ novedad.empleado_id = empleado.id
724
+ novedad.tipo_valor = "horas"
725
+ novedad.codigo_concepto = overtime.codigo
726
+ novedad.valor_cantidad = Decimal("8.0") # 8 hours overtime
727
+ novedad.fecha_novedad = date.today() - timedelta(days=5)
728
+ novedad.percepcion_id = overtime.id
729
+ novedad.estado = "pendiente"
730
+ db.session.add(novedad)
731
+ log.trace(f"Created overtime novedad for {empleado.codigo_empleado}")
732
+
733
+ # Create absences for some employees
734
+ if absence:
735
+ for i in [2, 3]: # Third and fourth employee
736
+ if i < len(empleados):
737
+ empleado = empleados[i]
738
+
739
+ # Check if novedad already exists
740
+ if nomina:
741
+ existing = db.session.execute(
742
+ db.select(NominaNovedad).filter_by(
743
+ nomina_id=nomina.id, empleado_id=empleado.id, codigo_concepto=absence.codigo
744
+ )
745
+ ).scalar_one_or_none()
746
+
747
+ if existing is None:
748
+ novedad = NominaNovedad()
749
+ novedad.nomina_id = nomina.id
750
+ novedad.empleado_id = empleado.id
751
+ novedad.tipo_valor = "dias"
752
+ novedad.codigo_concepto = absence.codigo
753
+ novedad.valor_cantidad = Decimal("1.0") # 1 day absence
754
+ novedad.fecha_novedad = date.today() - timedelta(days=3)
755
+ novedad.deduccion_id = absence.id
756
+ novedad.estado = "pendiente"
757
+ db.session.add(novedad)
758
+ log.trace(f"Created absence novedad for {empleado.codigo_empleado}")
759
+
760
+ db.session.commit()
761
+
762
+
763
+ def load_demo_data() -> None:
764
+ """Load all demo data into the database.
765
+
766
+ This function orchestrates the loading of comprehensive demo data:
767
+ 1. Creates demo companies
768
+ 2. Creates demo employees
769
+ 3. Creates demo payrolls and assigns employees
770
+ 4. Assigns perceptions, deductions, and benefits
771
+ 5. Creates a demo nomina for next month
772
+ 6. Creates demo novelties
773
+
774
+ This function is idempotent - safe to call multiple times.
775
+ """
776
+ try:
777
+ log.trace("=" * 60)
778
+ log.trace("Loading demo data for manual testing...")
779
+ log.trace("=" * 60)
780
+
781
+ # 1. Create demo companies
782
+ empresa1, empresa2 = load_demo_companies()
783
+
784
+ # 2. Create demo employees
785
+ empleados = load_demo_employees(empresa1, empresa2)
786
+
787
+ if not empleados:
788
+ log.trace("No employees created, skipping payroll and nomina creation")
789
+ return
790
+
791
+ # 3. Create demo payrolls with employees and concepts
792
+ planilla1, planilla2 = load_demo_payrolls(empresa1, empresa2, empleados)
793
+
794
+ # 4. Create demo nomina for next month (for first payroll)
795
+ if planilla1:
796
+ create_demo_nomina(planilla1)
797
+
798
+ # 5. Create demo novelties
799
+ create_demo_novelties(empleados)
800
+
801
+ log.trace("=" * 60)
802
+ log.trace("Demo data loading completed successfully!")
803
+ log.trace("=" * 60)
804
+ log.trace(f"Created {len(empleados)} demo employees")
805
+ log.trace("Created 2 demo companies")
806
+ log.trace("Created 2 demo payrolls with assigned concepts")
807
+ log.trace("Created demo novelties (overtime, absences)")
808
+ log.trace("=" * 60)
809
+
810
+ except Exception as exc:
811
+ log.error(f"Error loading demo data: {exc}")
812
+ log.exception("Demo data loading exception")
813
+ db.session.rollback()