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
coati_payroll/model.py ADDED
@@ -0,0 +1,2410 @@
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
+ """Data model for the payroll module."""
15
+
16
+ from __future__ import annotations
17
+
18
+ # <-------------------------------------------------------------------------> #
19
+ # Standard library
20
+ # <-------------------------------------------------------------------------> #
21
+ from decimal import Decimal
22
+ from datetime import date, datetime, timezone
23
+
24
+ # <-------------------------------------------------------------------------> #
25
+ # Third party libraries
26
+ # <-------------------------------------------------------------------------> #
27
+ import orjson
28
+ from flask_login import UserMixin
29
+ from flask_sqlalchemy import SQLAlchemy
30
+ from sqlalchemy import TypeDecorator, JSON
31
+ from sqlalchemy.ext.mutable import MutableDict
32
+ from ulid import ULID
33
+
34
+ # <-------------------------------------------------------------------------> #
35
+ # Local modules
36
+ # <-------------------------------------------------------------------------> #
37
+
38
+ db = SQLAlchemy()
39
+ database = db
40
+
41
+
42
+ # Funciones de ayuda
43
+ def generador_de_codigos_unicos() -> str:
44
+ """Genera codigo unicos basados en ULID."""
45
+
46
+ codigo_aleatorio = ULID()
47
+ id_unico = str(codigo_aleatorio)
48
+
49
+ return id_unico
50
+
51
+
52
+ def generador_codigo_empleado() -> str:
53
+ """Genera código único de empleado.
54
+
55
+ Formato: EMP-XXXXXX donde X es alfanumérico.
56
+ Usa los últimos 6 caracteres del ULID para unicidad.
57
+ """
58
+ codigo_aleatorio = ULID()
59
+ sufijo = str(codigo_aleatorio)[-6:].upper()
60
+ return f"EMP-{sufijo}"
61
+
62
+
63
+ def utc_now() -> datetime:
64
+ """Generate timezone-aware UTC datetime.
65
+
66
+ Replacement for deprecated datetime.utcnow() with timezone-aware alternative.
67
+ """
68
+ return datetime.now(timezone.utc)
69
+
70
+
71
+ # Utiliza orjon para serializar/deserializar JSON
72
+ class OrjsonType(TypeDecorator):
73
+ impl = JSON
74
+ cache_ok = True
75
+
76
+ def process_bind_param(self, value, dialect):
77
+ if value is not None:
78
+ return orjson.dumps(value).decode("utf-8")
79
+ return value
80
+
81
+ def process_result_value(self, value, dialect):
82
+ if value is not None:
83
+ if isinstance(value, (dict, list)):
84
+ return value # PostgreSQL ya lo deserializó
85
+ return orjson.loads(value)
86
+ return value
87
+
88
+
89
+ # Clase base para todas las tablas
90
+ class BaseTabla:
91
+ """Columnas estandar para todas las tablas de la base de datos."""
92
+
93
+ # Pistas de auditoria comunes a todas las tablas.
94
+ id = database.Column(
95
+ database.String(26),
96
+ primary_key=True,
97
+ nullable=False,
98
+ index=True,
99
+ default=generador_de_codigos_unicos,
100
+ )
101
+ timestamp = database.Column(database.DateTime, default=utc_now, nullable=False)
102
+ creado = database.Column(database.Date, default=date.today, nullable=False)
103
+ creado_por = database.Column(database.String(150), nullable=True)
104
+ modificado = database.Column(database.DateTime, onupdate=utc_now, nullable=True)
105
+ modificado_por = database.Column(database.String(150), nullable=True)
106
+
107
+
108
+ class PluginRegistry(database.Model, BaseTabla):
109
+ __tablename__ = "plugin_registry"
110
+ __table_args__ = (database.UniqueConstraint("distribution_name", name="uq_plugin_distribution_name"),)
111
+
112
+ distribution_name = database.Column(database.String(200), nullable=False, unique=True, index=True)
113
+ plugin_id = database.Column(database.String(200), nullable=False, index=True)
114
+ version = database.Column(database.String(50), nullable=True)
115
+ active = database.Column(database.Boolean(), default=False, nullable=False)
116
+ installed = database.Column(database.Boolean(), default=True, nullable=False)
117
+
118
+
119
+ # Gestión de usuarios con acceso a la aplicación
120
+ class Usuario(database.Model, BaseTabla, UserMixin):
121
+ __tablename__ = "usuario"
122
+ __table_args__ = (
123
+ database.UniqueConstraint("usuario", name="id_usuario_unico"),
124
+ database.UniqueConstraint("correo_electronico", name="correo_usuario_unico"),
125
+ )
126
+
127
+ usuario = database.Column(database.String(150), nullable=False, index=True, unique=True)
128
+ acceso = database.Column(database.LargeBinary(), nullable=False)
129
+ nombre = database.Column(database.String(100))
130
+ apellido = database.Column(database.String(100))
131
+ correo_electronico = database.Column(database.String(150))
132
+ tipo = database.Column(database.String(20))
133
+ activo = database.Column(database.Boolean(), default=True)
134
+ ultimo_acceso = database.Column(database.DateTime, nullable=True)
135
+
136
+
137
+ # Gestión de empresas/entidades
138
+ class Empresa(database.Model, BaseTabla):
139
+ """Company/Entity model for multi-company support.
140
+
141
+ Allows the payroll system to handle multiple companies/entities.
142
+ Employees and Payrolls are associated with a company.
143
+ Deductions, Benefits, and Perceptions remain independent and can be used
144
+ across multiple companies.
145
+ """
146
+
147
+ __tablename__ = "empresa"
148
+ __table_args__ = (
149
+ database.UniqueConstraint("codigo", name="uq_empresa_codigo"),
150
+ database.UniqueConstraint("ruc", name="uq_empresa_ruc"),
151
+ )
152
+
153
+ # Unique company code
154
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
155
+
156
+ # Company legal name
157
+ razon_social = database.Column(database.String(200), nullable=False)
158
+
159
+ # Commercial/trade name
160
+ nombre_comercial = database.Column(database.String(200), nullable=True)
161
+
162
+ # Tax identification number (jurisdiction-specific format)
163
+ ruc = database.Column(database.String(50), unique=True, nullable=False)
164
+
165
+ # Contact information
166
+ direccion = database.Column(database.String(255), nullable=True)
167
+ telefono = database.Column(database.String(50), nullable=True)
168
+ correo = database.Column(database.String(150), nullable=True)
169
+ sitio_web = database.Column(database.String(200), nullable=True)
170
+
171
+ # Legal representative
172
+ representante_legal = database.Column(database.String(150), nullable=True)
173
+
174
+ # Status
175
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
176
+
177
+ # Relationships
178
+ empleados = database.relationship("Empleado", back_populates="empresa")
179
+ planillas = database.relationship("Planilla", back_populates="empresa")
180
+
181
+
182
+ # Gestión de monedas y tipos de cambio
183
+ class Moneda(database.Model, BaseTabla):
184
+ __tablename__ = "moneda"
185
+
186
+ codigo = database.Column(database.String(10), unique=True, nullable=False, index=True)
187
+ nombre = database.Column(database.String(100), nullable=False)
188
+ simbolo = database.Column(database.String(10), nullable=True)
189
+ activo = database.Column(database.Boolean(), default=True)
190
+
191
+ # relaciones
192
+ planillas = database.relationship("Planilla", back_populates="moneda")
193
+ empleados = database.relationship("Empleado", back_populates="moneda")
194
+ tipo_cambio_origen = database.relationship(
195
+ "TipoCambio",
196
+ back_populates="moneda_origen",
197
+ foreign_keys="TipoCambio.moneda_origen_id",
198
+ )
199
+ tipo_cambio_destino = database.relationship(
200
+ "TipoCambio",
201
+ back_populates="moneda_destino",
202
+ foreign_keys="TipoCambio.moneda_destino_id",
203
+ )
204
+
205
+
206
+ class TipoCambio(database.Model, BaseTabla):
207
+ __tablename__ = "tipo_cambio"
208
+ __table_args__ = (
209
+ database.UniqueConstraint(
210
+ "moneda_origen_id",
211
+ "moneda_destino_id",
212
+ "fecha",
213
+ name="uq_tc_origen_destino_fecha",
214
+ ),
215
+ )
216
+
217
+ fecha = database.Column(database.Date, nullable=False, default=date.today, index=True)
218
+ moneda_origen_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=False)
219
+ moneda_destino_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=False)
220
+ tasa = database.Column(database.Numeric(24, 10), nullable=False)
221
+
222
+ moneda_origen = database.relationship(
223
+ "Moneda", back_populates="tipo_cambio_origen", foreign_keys=[moneda_origen_id]
224
+ )
225
+ moneda_destino = database.relationship(
226
+ "Moneda", back_populates="tipo_cambio_destino", foreign_keys=[moneda_destino_id]
227
+ )
228
+
229
+
230
+ # Registro maestro de empleados
231
+ class Empleado(database.Model, BaseTabla):
232
+ __tablename__ = "empleado"
233
+ __table_args__ = (
234
+ database.UniqueConstraint("identificacion_personal", name="uq_empleado_identificacion"),
235
+ database.UniqueConstraint("codigo_empleado", name="uq_empleado_codigo"),
236
+ )
237
+
238
+ # Código único de empleado (auto-generado si no se proporciona)
239
+ codigo_empleado = database.Column(
240
+ database.String(20),
241
+ unique=True,
242
+ nullable=False,
243
+ index=True,
244
+ default=generador_codigo_empleado,
245
+ )
246
+
247
+ primer_nombre = database.Column(database.String(100), nullable=False)
248
+ segundo_nombre = database.Column(database.String(100), nullable=True)
249
+ primer_apellido = database.Column(database.String(100), nullable=False)
250
+ segundo_apellido = database.Column(database.String(100), nullable=True)
251
+
252
+ genero = database.Column(database.String(20), nullable=True)
253
+ nacionalidad = database.Column(database.String(100), nullable=True)
254
+ tipo_identificacion = database.Column(database.String(50), nullable=True)
255
+ identificacion_personal = database.Column(database.String(50), unique=True, nullable=False)
256
+ id_seguridad_social = database.Column(database.String(50), nullable=True)
257
+ id_fiscal = database.Column(database.String(50), nullable=True)
258
+ tipo_sangre = database.Column(database.String(10), nullable=True)
259
+ fecha_nacimiento = database.Column(database.Date, nullable=True)
260
+
261
+ fecha_alta = database.Column(database.Date, nullable=False, default=date.today)
262
+ fecha_baja = database.Column(database.Date, nullable=True)
263
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
264
+
265
+ cargo = database.Column(database.String(150), nullable=True)
266
+ area = database.Column(database.String(150), nullable=True)
267
+ centro_costos = database.Column(database.String(150), nullable=True)
268
+
269
+ salario_base = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
270
+
271
+ # Moneda del sueldo: FK hacia moneda.id (consistencia)
272
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=True)
273
+ moneda = database.relationship("Moneda", back_populates="empleados")
274
+
275
+ # Empresa a la que pertenece el empleado
276
+ empresa_id = database.Column(database.String(26), database.ForeignKey("empresa.id"), nullable=True)
277
+ empresa = database.relationship("Empresa", back_populates="empleados")
278
+
279
+ correo = database.Column(database.String(150), nullable=True, index=True)
280
+ telefono = database.Column(database.String(50), nullable=True)
281
+ direccion = database.Column(database.String(255), nullable=True)
282
+ estado_civil = database.Column(database.String(50), nullable=True)
283
+ banco = database.Column(database.String(100), nullable=True)
284
+ numero_cuenta_bancaria = database.Column(database.String(100), nullable=True)
285
+
286
+ tipo_contrato = database.Column(database.String(50), nullable=True)
287
+ fecha_ultimo_aumento = database.Column(database.Date, nullable=True)
288
+
289
+ # Datos iniciales de implementación
290
+ # Estos campos almacenan saldos acumulados cuando el sistema se implementa
291
+ # a mitad de un período fiscal
292
+ anio_implementacion_inicial = database.Column(database.Integer, nullable=True)
293
+ mes_ultimo_cierre = database.Column(database.Integer, nullable=True)
294
+ salario_acumulado = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
295
+ impuesto_acumulado = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
296
+ ultimos_tres_salarios = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
297
+
298
+ # relaciones
299
+ planilla_asociaciones = database.relationship(
300
+ "PlanillaEmpleado",
301
+ back_populates="empleado",
302
+ )
303
+ nominas = database.relationship(
304
+ "NominaEmpleado",
305
+ back_populates="empleado",
306
+ )
307
+ novedades_registradas = database.relationship(
308
+ "NominaNovedad", back_populates="empleado", cascade="all,delete-orphan"
309
+ )
310
+ historial_salarios = database.relationship(
311
+ "HistorialSalario", back_populates="empleado", cascade="all,delete-orphan"
312
+ )
313
+ vacaciones = database.relationship("VacacionEmpleado", back_populates="empleado", cascade="all,delete-orphan")
314
+ vacaciones_descansadas = database.relationship(
315
+ "VacacionDescansada", back_populates="empleado", cascade="all,delete-orphan"
316
+ )
317
+ adelantos = database.relationship("Adelanto", back_populates="empleado", cascade="all,delete-orphan")
318
+
319
+ # Datos adicionales (JSON)
320
+ datos_adicionales = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
321
+
322
+
323
+ # Gestión de planillas
324
+ class TipoPlanilla(database.Model, BaseTabla):
325
+ """Payroll type configuration.
326
+
327
+ Defines the type of payroll (monthly, biweekly, weekly, etc.) and its
328
+ fiscal period parameters. The fiscal period can be different from the
329
+ calendar year (Jan-Dec) and is defined here to support various accounting
330
+ requirements.
331
+ """
332
+
333
+ __tablename__ = "tipo_planilla"
334
+
335
+ codigo = database.Column(database.String(20), unique=True, nullable=False)
336
+ descripcion = database.Column(database.String(150), nullable=True)
337
+ dias = database.Column(database.Integer, nullable=False, default=30) # días usados para prorrateos
338
+ periodicidad = database.Column(
339
+ database.String(20), nullable=False, default="mensual"
340
+ ) # ej. mensual, quincenal, semanal
341
+
342
+ # Fiscal period configuration
343
+ # mes_inicio_fiscal: Month when the fiscal year starts (1-12)
344
+ mes_inicio_fiscal = database.Column(database.Integer, nullable=False, default=1) # 1 = January
345
+ # dia_inicio_fiscal: Day of month when fiscal year starts
346
+ dia_inicio_fiscal = database.Column(database.Integer, nullable=False, default=1)
347
+
348
+ # Accumulated calculation settings
349
+ # acumula_anual: Whether this payroll type accumulates values annually
350
+ acumula_anual = database.Column(database.Boolean(), default=True, nullable=False)
351
+ # periodos_por_anio: Number of payroll periods per fiscal year
352
+ periodos_por_anio = database.Column(database.Integer, nullable=False, default=12)
353
+
354
+ # Tax calculation parameters (stored as JSON for flexibility)
355
+ parametros_calculo = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
356
+
357
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
358
+
359
+ planillas = database.relationship("Planilla", back_populates="tipo_planilla")
360
+ acumulados = database.relationship("AcumuladoAnual", back_populates="tipo_planilla")
361
+
362
+
363
+ class Planilla(database.Model, BaseTabla):
364
+ """Master payroll record that connects employees, perceptions, deductions, and benefits.
365
+
366
+ The Planilla acts as the central hub for payroll configuration:
367
+ - Employees assigned via PlanillaEmpleado
368
+ - Perceptions via PlanillaIngreso
369
+ - Deductions via PlanillaDeduccion (with priority order)
370
+ - Benefits via PlanillaPrestacion
371
+ - Calculation rules via PlanillaReglaCalculo
372
+
373
+ Automatic Deductions:
374
+ The payroll engine automatically applies loan installments and salary advances
375
+ from the Adelanto table for employees with active loans, regardless of whether
376
+ those deductions are explicitly configured in planilla_deducciones.
377
+ The priority for these automatic deductions is controlled by:
378
+ - prioridad_prestamos: Priority for loan installments
379
+ - prioridad_adelantos: Priority for salary advances
380
+ """
381
+
382
+ __tablename__ = "planilla"
383
+
384
+ nombre = database.Column(database.String(150), nullable=False, unique=True)
385
+ descripcion = database.Column(database.String(255), nullable=True)
386
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
387
+
388
+ parametros = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
389
+
390
+ tipo_planilla_id = database.Column(database.String(26), database.ForeignKey("tipo_planilla.id"), nullable=False)
391
+ tipo_planilla = database.relationship("TipoPlanilla", back_populates="planillas")
392
+
393
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=False)
394
+ moneda = database.relationship("Moneda", back_populates="planillas")
395
+
396
+ # Empresa a la que pertenece la planilla
397
+ empresa_id = database.Column(database.String(26), database.ForeignKey("empresa.id"), nullable=True)
398
+ empresa = database.relationship("Empresa", back_populates="planillas")
399
+
400
+ # Período Fiscal
401
+ periodo_fiscal_inicio = database.Column(database.Date, nullable=True)
402
+ periodo_fiscal_fin = database.Column(database.Date, nullable=True)
403
+
404
+ # Ultima ejecución
405
+ ultima_ejecucion = database.Column(database.DateTime, nullable=True)
406
+
407
+ # Automatic deduction priorities (applied even if not in planilla_deducciones)
408
+ # Loans and advances from Adelanto table are automatically deducted
409
+ # Lower number = higher priority (applied first)
410
+ prioridad_prestamos = database.Column(
411
+ database.Integer, nullable=False, default=250
412
+ ) # Default: after mandatory deductions
413
+ prioridad_adelantos = database.Column(database.Integer, nullable=False, default=251) # Default: right after loans
414
+
415
+ # Whether to apply automatic loan/advance deductions
416
+ aplicar_prestamos_automatico = database.Column(database.Boolean(), default=True, nullable=False)
417
+ aplicar_adelantos_automatico = database.Column(database.Boolean(), default=True, nullable=False)
418
+
419
+ # Accounting control for base salary
420
+ # Base salary is the foundation of payroll calculation and needs its own
421
+ # accounting accounts to generate proper accounting vouchers
422
+ codigo_cuenta_debe_salario = database.Column(database.String(64), nullable=True)
423
+ descripcion_cuenta_debe_salario = database.Column(database.String(255), nullable=True)
424
+ codigo_cuenta_haber_salario = database.Column(database.String(64), nullable=True)
425
+ descripcion_cuenta_haber_salario = database.Column(database.String(255), nullable=True)
426
+
427
+ # relaciones con componentes configurados
428
+ planilla_percepciones = database.relationship(
429
+ "PlanillaIngreso",
430
+ back_populates="planilla",
431
+ )
432
+ planilla_deducciones = database.relationship(
433
+ "PlanillaDeduccion",
434
+ back_populates="planilla",
435
+ )
436
+ planilla_prestaciones = database.relationship(
437
+ "PlanillaPrestacion",
438
+ back_populates="planilla",
439
+ )
440
+
441
+ # reglas de cálculo asociadas (impuestos, fórmulas complejas)
442
+ planilla_reglas_calculo = database.relationship(
443
+ "PlanillaReglaCalculo",
444
+ back_populates="planilla",
445
+ )
446
+
447
+ # empleados asignados a la planilla (config)
448
+ planilla_empleados = database.relationship(
449
+ "PlanillaEmpleado",
450
+ back_populates="planilla",
451
+ )
452
+
453
+ # Audit and governance fields
454
+ estado_aprobacion = database.Column(database.String(20), nullable=False, default="borrador", index=True)
455
+ aprobado_por = database.Column(database.String(150), nullable=True)
456
+ aprobado_en = database.Column(database.DateTime, nullable=True)
457
+ creado_por_plugin = database.Column(database.Boolean(), default=False, nullable=False)
458
+ plugin_source = database.Column(database.String(200), nullable=True)
459
+
460
+ # ejecuciones históricas (nominas)
461
+ nominas = database.relationship(
462
+ "Nomina",
463
+ back_populates="planilla",
464
+ )
465
+ audit_logs = database.relationship(
466
+ "PlanillaAuditLog",
467
+ back_populates="planilla",
468
+ cascade="all, delete-orphan",
469
+ )
470
+
471
+
472
+ # Percepciones y deducciones - THESE AFFECT EMPLOYEE'S NET PAY
473
+ # Percepciones (ingresos) se SUMAN al salario
474
+ # Deducciones se RESTAN del salario
475
+ class Percepcion(database.Model, BaseTabla):
476
+ """Income items that ADD to employee's pay.
477
+
478
+ Percepciones are income items that increase the employee's gross salary.
479
+ Examples: base salary, overtime, bonuses, commissions, allowances.
480
+
481
+ Together with Deducciones, these determine the employee's net pay.
482
+ (Prestaciones do NOT affect employee pay - they are employer costs.)
483
+ """
484
+
485
+ __tablename__ = "percepcion"
486
+
487
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
488
+ nombre = database.Column(database.String(150), nullable=False)
489
+ descripcion = database.Column(database.String(255), nullable=True)
490
+ unidad_calculo = database.Column(database.String(20), nullable=True) # ej. 'hora', 'dia', 'mes', etc.
491
+
492
+ # tipo de cálculo: 'fijo', 'porcentaje_salario', 'porcentaje_bruto', 'formula', 'horas', etc.
493
+ formula_tipo = database.Column(database.String(50), nullable=False, default="fijo")
494
+ monto_default = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
495
+ formula = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
496
+ condicion = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
497
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
498
+ gravable = database.Column(database.Boolean(), default=True)
499
+ recurrente = database.Column(database.Boolean(), default=False)
500
+ activo = database.Column(database.Boolean(), default=True)
501
+
502
+ # Vigencia: hasta cuándo es válida esta percepción (opcional)
503
+ vigente_desde = database.Column(database.Date, nullable=True) # opcional, si quieres rango
504
+ valido_hasta = database.Column(database.Date, nullable=True)
505
+
506
+ # Especificidad de cálculo
507
+ base_calculo = database.Column( # ej: 'salario_base', 'gravable', 'bruto', 'neto'
508
+ database.String(50), nullable=True
509
+ )
510
+ unidad_calculo = database.Column(database.String(20), nullable=True) # ej: 'horas', 'dias', None
511
+
512
+ # Control contable
513
+ contabilizable = database.Column(database.Boolean(), default=True, nullable=False)
514
+ codigo_cuenta_debe = database.Column(database.String(64), nullable=True)
515
+ descripcion_cuenta_debe = database.Column(database.String(255), nullable=True)
516
+ codigo_cuenta_haber = database.Column(database.String(64), nullable=True)
517
+ descripcion_cuenta_haber = database.Column(database.String(255), nullable=True)
518
+
519
+ # Control edición en nómina
520
+ editable_en_nomina = database.Column(database.Boolean(), default=False, nullable=False)
521
+
522
+ # Audit and governance fields
523
+ estado_aprobacion = database.Column(database.String(20), nullable=False, default="borrador", index=True)
524
+ aprobado_por = database.Column(database.String(150), nullable=True)
525
+ aprobado_en = database.Column(database.DateTime, nullable=True)
526
+ creado_por_plugin = database.Column(database.Boolean(), default=False, nullable=False)
527
+ plugin_source = database.Column(database.String(200), nullable=True)
528
+
529
+ planillas = database.relationship(
530
+ "PlanillaIngreso",
531
+ back_populates="percepcion",
532
+ )
533
+ nomina_detalles = database.relationship("NominaDetalle", back_populates="percepcion")
534
+ audit_logs = database.relationship(
535
+ "ConceptoAuditLog",
536
+ back_populates="percepcion",
537
+ foreign_keys="ConceptoAuditLog.percepcion_id",
538
+ cascade="all, delete-orphan",
539
+ )
540
+
541
+
542
+ class Deduccion(database.Model, BaseTabla):
543
+ __tablename__ = "deduccion"
544
+
545
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
546
+ nombre = database.Column(database.String(150), nullable=False)
547
+ descripcion = database.Column(database.String(255), nullable=True)
548
+
549
+ tipo = database.Column(database.String(30), nullable=False, default="general")
550
+ es_impuesto = database.Column(database.Boolean(), default=False)
551
+
552
+ formula_tipo = database.Column(database.String(50), nullable=False, default="fijo")
553
+ monto_default = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
554
+ formula = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
555
+ condicion = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
556
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
557
+ antes_impuesto = database.Column(database.Boolean(), default=True)
558
+ recurrente = database.Column(database.Boolean(), default=False)
559
+ activo = database.Column(database.Boolean(), default=True)
560
+
561
+ # Vigencia
562
+ vigente_desde = database.Column(database.Date, nullable=True)
563
+ valido_hasta = database.Column(database.Date, nullable=True)
564
+
565
+ # Base y unidad de cálculo
566
+ base_calculo = database.Column(database.String(50), nullable=True)
567
+ unidad_calculo = database.Column(database.String(20), nullable=True)
568
+
569
+ # Control contable
570
+ contabilizable = database.Column(database.Boolean(), default=True, nullable=False)
571
+ codigo_cuenta_debe = database.Column(database.String(64), nullable=True)
572
+ descripcion_cuenta_debe = database.Column(database.String(255), nullable=True)
573
+ codigo_cuenta_haber = database.Column(database.String(64), nullable=True)
574
+ descripcion_cuenta_haber = database.Column(database.String(255), nullable=True)
575
+
576
+ # Control edición en nómina
577
+ editable_en_nomina = database.Column(database.Boolean(), default=False, nullable=False)
578
+
579
+ # Audit and governance fields
580
+ estado_aprobacion = database.Column(database.String(20), nullable=False, default="borrador", index=True)
581
+ aprobado_por = database.Column(database.String(150), nullable=True)
582
+ aprobado_en = database.Column(database.DateTime, nullable=True)
583
+ creado_por_plugin = database.Column(database.Boolean(), default=False, nullable=False)
584
+ plugin_source = database.Column(database.String(200), nullable=True)
585
+
586
+ planillas = database.relationship(
587
+ "PlanillaDeduccion",
588
+ back_populates="deduccion",
589
+ )
590
+ nomina_detalles = database.relationship("NominaDetalle", back_populates="deduccion")
591
+ tablas_impuesto = database.relationship("TablaImpuesto", back_populates="deduccion")
592
+ adelantos = database.relationship("Adelanto", back_populates="deduccion")
593
+ audit_logs = database.relationship(
594
+ "ConceptoAuditLog",
595
+ back_populates="deduccion",
596
+ foreign_keys="ConceptoAuditLog.deduccion_id",
597
+ cascade="all, delete-orphan",
598
+ )
599
+
600
+
601
+ # Prestaciones (aportes del empleador: seguridad social, etc.)
602
+ # NOTA: Las prestaciones son costos patronales que NO afectan el pago al empleado.
603
+ # Solo las percepciones y deducciones afectan el salario neto del empleado.
604
+ # Ejemplos de prestaciones: INSS patronal, provisión de vacaciones, aguinaldo, indemnización.
605
+ class Prestacion(database.Model, BaseTabla):
606
+ """Employer contributions and provisions.
607
+
608
+ Prestaciones are employer costs that do NOT affect the employee's net pay.
609
+ They represent the company's obligations such as:
610
+ - Social security employer contributions (INSS patronal)
611
+ - Vacation provisions
612
+ - 13th month (aguinaldo) provisions
613
+ - Severance provisions (indemnización)
614
+
615
+ Only Percepciones (income) and Deducciones affect the employee's net salary.
616
+ """
617
+
618
+ __tablename__ = "prestacion"
619
+
620
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
621
+ nombre = database.Column(database.String(150), nullable=False)
622
+ descripcion = database.Column(database.String(255), nullable=True)
623
+
624
+ tipo = database.Column(database.String(30), nullable=False, default="patronal")
625
+
626
+ formula_tipo = database.Column(database.String(50), nullable=False, default="fijo")
627
+ monto_default = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
628
+ formula = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
629
+ condicion = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
630
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
631
+ recurrente = database.Column(database.Boolean(), default=False)
632
+ activo = database.Column(database.Boolean(), default=True)
633
+
634
+ vigente_desde = database.Column(database.Date, nullable=True)
635
+ valido_hasta = database.Column(database.Date, nullable=True)
636
+
637
+ base_calculo = database.Column(database.String(50), nullable=True)
638
+ unidad_calculo = database.Column(database.String(20), nullable=True)
639
+
640
+ tope_aplicacion = database.Column(database.Numeric(14, 2), nullable=True)
641
+
642
+ contabilizable = database.Column(database.Boolean(), default=True, nullable=False)
643
+ codigo_cuenta_debe = database.Column(database.String(64), nullable=True)
644
+ descripcion_cuenta_debe = database.Column(database.String(255), nullable=True)
645
+ codigo_cuenta_haber = database.Column(database.String(64), nullable=True)
646
+ descripcion_cuenta_haber = database.Column(database.String(255), nullable=True)
647
+
648
+ editable_en_nomina = database.Column(database.Boolean(), default=False, nullable=False)
649
+
650
+ # Accumulation configuration
651
+ # Defines how this benefit accumulates: monthly settlement, annually, or lifetime
652
+ tipo_acumulacion = database.Column(
653
+ database.String(20), nullable=False, default="mensual"
654
+ ) # mensual | anual | vida_laboral
655
+
656
+ # Audit and governance fields
657
+ estado_aprobacion = database.Column(database.String(20), nullable=False, default="borrador", index=True)
658
+ aprobado_por = database.Column(database.String(150), nullable=True)
659
+ aprobado_en = database.Column(database.DateTime, nullable=True)
660
+ creado_por_plugin = database.Column(database.Boolean(), default=False, nullable=False)
661
+ plugin_source = database.Column(database.String(200), nullable=True)
662
+
663
+ planillas = database.relationship(
664
+ "PlanillaPrestacion",
665
+ back_populates="prestacion",
666
+ )
667
+ nomina_detalles = database.relationship("NominaDetalle", back_populates="prestacion")
668
+ prestaciones_acumuladas = database.relationship(
669
+ "PrestacionAcumulada", back_populates="prestacion", cascade="all,delete-orphan"
670
+ )
671
+ cargas_iniciales = database.relationship(
672
+ "CargaInicialPrestacion", back_populates="prestacion", cascade="all,delete-orphan"
673
+ )
674
+ audit_logs = database.relationship(
675
+ "ConceptoAuditLog",
676
+ back_populates="prestacion",
677
+ foreign_keys="ConceptoAuditLog.prestacion_id",
678
+ cascade="all, delete-orphan",
679
+ )
680
+
681
+
682
+ # Definición de componentes de planilla
683
+ class PlanillaIngreso(database.Model, BaseTabla):
684
+ __tablename__ = "planilla_ingreso"
685
+ __table_args__ = (database.UniqueConstraint("planilla_id", "percepcion_id", name="uq_planilla_percepcion"),)
686
+
687
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
688
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=False)
689
+
690
+ orden = database.Column(database.Integer, nullable=True, default=0)
691
+ editable = database.Column(database.Boolean(), default=True)
692
+ monto_predeterminado = database.Column(database.Numeric(14, 2), nullable=True)
693
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
694
+ activo = database.Column(database.Boolean(), default=True)
695
+
696
+ planilla = database.relationship("Planilla", back_populates="planilla_percepciones")
697
+ percepcion = database.relationship("Percepcion", back_populates="planillas")
698
+
699
+
700
+ class PlanillaDeduccion(database.Model, BaseTabla):
701
+ """Association between Planilla and Deduccion with priority ordering.
702
+
703
+ The 'prioridad' field determines the order in which deductions are applied.
704
+ This is critical when the net salary doesn't cover all deductions.
705
+
706
+ Priority guidelines:
707
+ - 1-100: Legal/mandatory deductions (taxes, social security)
708
+ - 101-200: Court-ordered deductions (alimony, garnishments)
709
+ - 201-300: Company loans and salary advances
710
+ - 301-400: Voluntary deductions (savings, insurance)
711
+ - 401+: Other deductions
712
+
713
+ Note: Loan installments from the Adelanto table are handled automatically
714
+ by the payroll engine, not through JSON calculation rules.
715
+ """
716
+
717
+ __tablename__ = "planilla_deduccion"
718
+ __table_args__ = (database.UniqueConstraint("planilla_id", "deduccion_id", name="uq_planilla_deduccion"),)
719
+
720
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
721
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=False)
722
+
723
+ # Priority order for applying deductions (lower = higher priority)
724
+ prioridad = database.Column(database.Integer, nullable=False, default=100)
725
+
726
+ # Legacy field kept for backward compatibility, use 'prioridad' instead
727
+ orden = database.Column(database.Integer, nullable=True, default=0)
728
+
729
+ editable = database.Column(database.Boolean(), default=True)
730
+ monto_predeterminado = database.Column(database.Numeric(14, 2), nullable=True)
731
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
732
+ activo = database.Column(database.Boolean(), default=True)
733
+
734
+ # Whether this deduction is mandatory (cannot be skipped if salary insufficient)
735
+ es_obligatoria = database.Column(database.Boolean(), default=False)
736
+
737
+ # Whether to stop processing if salary is insufficient for this deduction
738
+ detener_si_insuficiente = database.Column(database.Boolean(), default=False)
739
+
740
+ planilla = database.relationship("Planilla", back_populates="planilla_deducciones")
741
+ deduccion = database.relationship("Deduccion", back_populates="planillas")
742
+
743
+
744
+ class PlanillaPrestacion(database.Model, BaseTabla):
745
+ __tablename__ = "planilla_prestacion"
746
+ __table_args__ = (database.UniqueConstraint("planilla_id", "prestacion_id", name="uq_planilla_prestacion"),)
747
+
748
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
749
+ prestacion_id = database.Column(database.String(26), database.ForeignKey("prestacion.id"), nullable=False)
750
+
751
+ orden = database.Column(database.Integer, nullable=True, default=0)
752
+ editable = database.Column(database.Boolean(), default=True)
753
+ monto_predeterminado = database.Column(database.Numeric(14, 2), nullable=True)
754
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=True)
755
+ activo = database.Column(database.Boolean(), default=True)
756
+
757
+ planilla = database.relationship("Planilla", back_populates="planilla_prestaciones")
758
+ prestacion = database.relationship("Prestacion", back_populates="planillas")
759
+
760
+
761
+ class PlanillaReglaCalculo(database.Model, BaseTabla):
762
+ """Association between Planilla and ReglaCalculo (calculation rules/tax tables).
763
+
764
+ This allows a payroll to have multiple calculation rules associated,
765
+ such as income tax rules, social security rules, etc.
766
+ """
767
+
768
+ __tablename__ = "planilla_regla_calculo"
769
+ __table_args__ = (database.UniqueConstraint("planilla_id", "regla_calculo_id", name="uq_planilla_regla"),)
770
+
771
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
772
+ regla_calculo_id = database.Column(database.String(26), database.ForeignKey("regla_calculo.id"), nullable=False)
773
+
774
+ # Order of execution (important for dependent calculations)
775
+ orden = database.Column(database.Integer, nullable=False, default=0)
776
+
777
+ # Whether this rule is active for this payroll
778
+ activo = database.Column(database.Boolean(), default=True)
779
+
780
+ # Optional: override parameters for this specific payroll
781
+ parametros_override = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
782
+
783
+ planilla = database.relationship("Planilla", back_populates="planilla_reglas_calculo")
784
+ regla_calculo = database.relationship("ReglaCalculo", back_populates="planillas")
785
+
786
+
787
+ class PlanillaEmpleado(database.Model, BaseTabla):
788
+ __tablename__ = "planilla_empleado"
789
+ __table_args__ = (database.UniqueConstraint("planilla_id", "empleado_id", name="uq_planilla_empleado"),)
790
+
791
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
792
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False)
793
+
794
+ activo = database.Column(database.Boolean(), default=True)
795
+ fecha_inicio = database.Column(database.Date, nullable=False, default=date.today)
796
+ fecha_fin = database.Column(database.Date, nullable=True) # si deja de estar en la planilla
797
+
798
+ planilla = database.relationship("Planilla", back_populates="planilla_empleados")
799
+ empleado = database.relationship("Empleado", back_populates="planilla_asociaciones")
800
+
801
+
802
+ # Nominas (ejecuciones de planillas)
803
+ class Nomina(database.Model, BaseTabla):
804
+ __tablename__ = "nomina"
805
+
806
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
807
+ fecha_generacion = database.Column(database.DateTime, nullable=False, default=utc_now)
808
+ periodo_inicio = database.Column(database.Date, nullable=False)
809
+ periodo_fin = database.Column(database.Date, nullable=False)
810
+ generado_por = database.Column(database.String(150), nullable=True)
811
+ estado = database.Column(
812
+ database.String(30), nullable=False, default="generado"
813
+ ) # calculando, generado, aprobado, aplicado, pagado, anulado, error (all are valid permanent states)
814
+
815
+ total_bruto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
816
+ total_deducciones = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
817
+ total_neto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
818
+
819
+ # Progress tracking for background processing
820
+ total_empleados = database.Column(database.Integer, nullable=True, default=0)
821
+ empleados_procesados = database.Column(database.Integer, nullable=True, default=0)
822
+ empleados_con_error = database.Column(database.Integer, nullable=True, default=0)
823
+ errores_calculo = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
824
+ procesamiento_en_background = database.Column(database.Boolean, nullable=False, default=False)
825
+ log_procesamiento = database.Column(JSON, nullable=True) # Stores list of log entries as JSON
826
+ empleado_actual = database.Column(database.String(255), nullable=True)
827
+
828
+ # Audit fields for state changes
829
+ aprobado_por = database.Column(database.String(150), nullable=True)
830
+ aprobado_en = database.Column(database.DateTime, nullable=True)
831
+ aplicado_por = database.Column(database.String(150), nullable=True)
832
+ aplicado_en = database.Column(database.DateTime, nullable=True)
833
+ anulado_por = database.Column(database.String(150), nullable=True)
834
+ anulado_en = database.Column(database.DateTime, nullable=True)
835
+ razon_anulacion = database.Column(database.String(500), nullable=True)
836
+
837
+ # Recalculation consistency: Snapshot of calculation context
838
+ # Stores immutable copy of all data needed to reproduce exact same calculation
839
+ fecha_calculo_original = database.Column(database.Date, nullable=True) # Original calculation date
840
+ configuracion_snapshot = database.Column(JSON, nullable=True) # Company config at calculation time
841
+ tipos_cambio_snapshot = database.Column(JSON, nullable=True) # Exchange rates used
842
+ catalogos_snapshot = database.Column(JSON, nullable=True) # Percepciones/Deducciones/Prestaciones formulas
843
+ es_recalculo = database.Column(database.Boolean, nullable=False, default=False) # Flag if this is a recalculation
844
+ nomina_original_id = database.Column(database.String(26), nullable=True) # Reference to original if recalculated
845
+
846
+ planilla = database.relationship("Planilla", back_populates="nominas")
847
+ nomina_empleados = database.relationship(
848
+ "NominaEmpleado",
849
+ back_populates="nomina",
850
+ )
851
+ novedades = database.relationship(
852
+ "NominaNovedad",
853
+ back_populates="nomina",
854
+ )
855
+ comprobante_contable = database.relationship(
856
+ "ComprobanteContable",
857
+ back_populates="nomina",
858
+ uselist=False,
859
+ )
860
+ audit_logs = database.relationship(
861
+ "NominaAuditLog",
862
+ back_populates="nomina",
863
+ cascade="all, delete-orphan",
864
+ )
865
+
866
+
867
+ class NominaEmpleado(database.Model, BaseTabla):
868
+ __tablename__ = "nomina_empleado"
869
+
870
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=False)
871
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False)
872
+
873
+ salario_bruto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
874
+ total_ingresos = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
875
+ total_deducciones = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
876
+ salario_neto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
877
+
878
+ # datos para auditoria/moneda
879
+ moneda_origen_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=True)
880
+ tipo_cambio_aplicado = database.Column(database.Numeric(24, 10), nullable=True)
881
+
882
+ nomina = database.relationship("Nomina", back_populates="nomina_empleados")
883
+ empleado = database.relationship("Empleado", back_populates="nominas")
884
+
885
+ nomina_detalles = database.relationship(
886
+ "NominaDetalle",
887
+ back_populates="nomina_empleado",
888
+ )
889
+
890
+ # Backup de datos del empleado al momento de la generación de la nómina
891
+ cargo_snapshot = database.Column(database.String(150), nullable=True)
892
+ area_snapshot = database.Column(database.String(150), nullable=True)
893
+ centro_costos_snapshot = database.Column(database.String(150), nullable=True)
894
+ sueldo_base_historico = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
895
+
896
+
897
+ class NominaDetalle(database.Model, BaseTabla):
898
+ __tablename__ = "nomina_detalle"
899
+
900
+ nomina_empleado_id = database.Column(database.String(26), database.ForeignKey("nomina_empleado.id"), nullable=False)
901
+ tipo = database.Column(database.String(15), nullable=False) # 'ingreso' | 'deduccion' | 'prestacion'
902
+ codigo = database.Column(database.String(50), nullable=False)
903
+ descripcion = database.Column(database.String(255), nullable=True)
904
+ monto = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
905
+ orden = database.Column(database.Integer, nullable=True, default=0)
906
+
907
+ # referencias opcionales a catálogo original (si aplica)
908
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=True)
909
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
910
+ prestacion_id = database.Column(database.String(26), database.ForeignKey("prestacion.id"), nullable=True)
911
+
912
+ nomina_empleado = database.relationship("NominaEmpleado", back_populates="nomina_detalles")
913
+ percepcion = database.relationship("Percepcion", back_populates="nomina_detalles", foreign_keys=[percepcion_id])
914
+ deduccion = database.relationship("Deduccion", back_populates="nomina_detalles", foreign_keys=[deduccion_id])
915
+ prestacion = database.relationship("Prestacion", back_populates="nomina_detalles", foreign_keys=[prestacion_id])
916
+
917
+
918
+ # Liquidaciones (terminaciones laborales)
919
+ class LiquidacionConcepto(database.Model, BaseTabla):
920
+ __tablename__ = "liquidacion_concepto"
921
+
922
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
923
+ nombre = database.Column(database.String(150), nullable=False)
924
+ descripcion = database.Column(database.String(255), nullable=True)
925
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
926
+
927
+
928
+ class Liquidacion(database.Model, BaseTabla):
929
+ __tablename__ = "liquidacion"
930
+
931
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False, index=True)
932
+ concepto_id = database.Column(
933
+ database.String(26), database.ForeignKey("liquidacion_concepto.id"), nullable=True, index=True
934
+ )
935
+
936
+ fecha_calculo = database.Column(database.Date, nullable=False, default=date.today)
937
+ ultimo_dia_pagado = database.Column(database.Date, nullable=True)
938
+ dias_por_pagar = database.Column(database.Integer, nullable=False, default=0)
939
+
940
+ estado = database.Column(database.String(30), nullable=False, default="borrador") # borrador, aplicada, pagada
941
+
942
+ total_bruto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
943
+ total_deducciones = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
944
+ total_neto = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
945
+
946
+ errores_calculo = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
947
+ advertencias_calculo = database.Column(JSON, nullable=True, default=list)
948
+
949
+ empleado = database.relationship("Empleado")
950
+ concepto = database.relationship("LiquidacionConcepto")
951
+ detalles = database.relationship("LiquidacionDetalle", back_populates="liquidacion", cascade="all,delete-orphan")
952
+
953
+
954
+ class LiquidacionDetalle(database.Model, BaseTabla):
955
+ __tablename__ = "liquidacion_detalle"
956
+
957
+ liquidacion_id = database.Column(
958
+ database.String(26), database.ForeignKey("liquidacion.id"), nullable=False, index=True
959
+ )
960
+ tipo = database.Column(database.String(15), nullable=False) # 'ingreso' | 'deduccion' | 'prestacion'
961
+ codigo = database.Column(database.String(50), nullable=False)
962
+ descripcion = database.Column(database.String(255), nullable=True)
963
+ monto = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
964
+ orden = database.Column(database.Integer, nullable=True, default=0)
965
+
966
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=True)
967
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
968
+ prestacion_id = database.Column(database.String(26), database.ForeignKey("prestacion.id"), nullable=True)
969
+
970
+ liquidacion = database.relationship("Liquidacion", back_populates="detalles")
971
+ percepcion = database.relationship("Percepcion", foreign_keys=[percepcion_id])
972
+ deduccion = database.relationship("Deduccion", foreign_keys=[deduccion_id])
973
+ prestacion = database.relationship("Prestacion", foreign_keys=[prestacion_id])
974
+
975
+
976
+ class NominaNovedad(database.Model, BaseTabla):
977
+ __tablename__ = "nomina_novedad"
978
+
979
+ # FK a la ejecución de Nómina (el ID que solicitaste)
980
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=False)
981
+ # FK al empleado afectado
982
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False)
983
+
984
+ tipo_valor = database.Column(database.String(20), nullable=True) # horas | dias | cantidad | monto | porcentaje
985
+
986
+ # El código del concepto que se está modificando/aplicando
987
+ codigo_concepto = database.Column(database.String(50), nullable=False)
988
+
989
+ # Valor/cantidad de la novedad (ej. 5 horas, 1500 de comisión, 1 día de ausencia)
990
+ valor_cantidad = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
991
+
992
+ # Fecha de ocurrencia del evento (útil para auditoría y prorrateo)
993
+ fecha_novedad = database.Column(database.Date, nullable=True)
994
+
995
+ # Referencia opcional al maestro para saber qué regla aplica
996
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=True)
997
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
998
+
999
+ # ---- Vacation Module Integration ----
1000
+ # Flag to mark this novelty as vacation/time-off
1001
+ es_descanso_vacaciones = database.Column(database.Boolean(), default=False, nullable=False)
1002
+
1003
+ # Reference to VacationNovelty if this is a vacation leave
1004
+ vacation_novelty_id = database.Column(
1005
+ database.String(26), database.ForeignKey("vacation_novelty.id"), nullable=True, index=True
1006
+ )
1007
+
1008
+ # Dates for vacation period (when es_descanso_vacaciones=True)
1009
+ fecha_inicio_descanso = database.Column(database.Date, nullable=True)
1010
+ fecha_fin_descanso = database.Column(database.Date, nullable=True)
1011
+
1012
+ # Estado de la novedad: 'pendiente' | 'ejecutada'
1013
+ # Se marca como 'ejecutada' cuando la nómina cambia a estado 'aplicado'
1014
+ estado = database.Column(database.String(20), nullable=False, default="pendiente") # Use NovedadEstado enum values
1015
+
1016
+ nomina = database.relationship("Nomina", back_populates="novedades")
1017
+ empleado = database.relationship("Empleado", back_populates="novedades_registradas")
1018
+ vacation_novelty = database.relationship("VacationNovelty", foreign_keys=[vacation_novelty_id])
1019
+
1020
+
1021
+ # Comprobante Contable (Accounting Voucher)
1022
+ class ComprobanteContable(database.Model, BaseTabla):
1023
+ """Stores the accounting voucher header for audit purposes.
1024
+
1025
+ This model preserves the accounting voucher header generated at the time of payroll
1026
+ calculation, preventing configuration changes from affecting historical records.
1027
+ Detail lines are stored in ComprobanteContableLinea.
1028
+
1029
+ Audit Trail:
1030
+ - aplicado_por: User who applied the nomina (immutable)
1031
+ - fecha_aplicacion: Date when nomina was applied (immutable)
1032
+ - modificado_por: User who last regenerated the voucher
1033
+ - fecha_modificacion: Date when voucher was last regenerated
1034
+ """
1035
+
1036
+ __tablename__ = "comprobante_contable"
1037
+
1038
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=False, unique=True)
1039
+
1040
+ # Header information
1041
+ fecha_calculo = database.Column(database.Date, nullable=False, default=date.today)
1042
+ concepto = database.Column(database.String(255), nullable=True) # Description/concept of the voucher
1043
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=True)
1044
+
1045
+ # Summary totals (calculated from lines)
1046
+ total_debitos = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1047
+ total_creditos = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1048
+ balance = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1049
+
1050
+ # Warnings about incomplete configurations
1051
+ advertencias = database.Column(JSON, nullable=True, default=list)
1052
+
1053
+ # Audit trail - immutable fields (set once when nomina is applied)
1054
+ aplicado_por = database.Column(database.String(150), nullable=True)
1055
+ fecha_aplicacion = database.Column(database.DateTime, nullable=True)
1056
+
1057
+ # Audit trail - modification tracking (updated each time voucher is regenerated)
1058
+ modificado_por = database.Column(database.String(150), nullable=True)
1059
+ fecha_modificacion = database.Column(database.DateTime, nullable=True)
1060
+ veces_modificado = database.Column(database.Integer, nullable=False, default=0)
1061
+
1062
+ nomina = database.relationship("Nomina", back_populates="comprobante_contable")
1063
+ moneda = database.relationship("Moneda")
1064
+ lineas = database.relationship(
1065
+ "ComprobanteContableLinea",
1066
+ back_populates="comprobante",
1067
+ cascade="all, delete-orphan",
1068
+ order_by="ComprobanteContableLinea.orden",
1069
+ )
1070
+
1071
+
1072
+ class ComprobanteContableLinea(database.Model, BaseTabla):
1073
+ """Stores individual accounting entry lines for each employee's payroll calculation.
1074
+
1075
+ Each line represents a single accounting entry (debit or credit) for a specific
1076
+ employee, concept, account, and cost center. Provides complete audit trail.
1077
+
1078
+ Audit Fields:
1079
+ - Empleado: empleado_id, empleado_codigo, empleado_nombre
1080
+ - Cuenta: codigo_cuenta, descripcion_cuenta
1081
+ - Centro de costos: centro_costos
1082
+ - Concepto origen: concepto_codigo (código de percepción/deducción/prestación)
1083
+ - Tipo: tipo_debito_credito ('debito' o 'credito')
1084
+ - Monto: debito o credito (solo uno tiene valor, el otro es 0)
1085
+ """
1086
+
1087
+ __tablename__ = "comprobante_contable_linea"
1088
+
1089
+ comprobante_id = database.Column(
1090
+ database.String(26), database.ForeignKey("comprobante_contable.id"), nullable=False, index=True
1091
+ )
1092
+ nomina_empleado_id = database.Column(
1093
+ database.String(26), database.ForeignKey("nomina_empleado.id"), nullable=False, index=True
1094
+ )
1095
+
1096
+ # Employee information for audit trail (denormalized for easier reporting)
1097
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False, index=True)
1098
+ empleado_codigo = database.Column(database.String(20), nullable=False, index=True)
1099
+ empleado_nombre = database.Column(database.String(255), nullable=False)
1100
+
1101
+ # Accounting account information (nullable to support incomplete configuration)
1102
+ codigo_cuenta = database.Column(database.String(64), nullable=True, index=True)
1103
+ descripcion_cuenta = database.Column(database.String(255), nullable=True)
1104
+
1105
+ # Cost center for cost allocation
1106
+ centro_costos = database.Column(database.String(150), nullable=True, index=True)
1107
+
1108
+ # Amount (only one should be non-zero: debito OR credito)
1109
+ tipo_debito_credito = database.Column(database.String(10), nullable=False, index=True) # 'debito' or 'credito'
1110
+ debito = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1111
+ credito = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1112
+
1113
+ # Calculated amount (the actual value, duplicated for convenience)
1114
+ monto_calculado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1115
+
1116
+ # Source concept information for complete audit trail
1117
+ concepto = database.Column(database.String(255), nullable=False) # Description of the concept
1118
+ tipo_concepto = database.Column(
1119
+ database.String(20), nullable=False, index=True
1120
+ ) # 'salario_base', 'percepcion', 'deduccion', 'prestacion', 'prestamo'
1121
+ concepto_codigo = database.Column(database.String(50), nullable=False, index=True) # Code from source concept
1122
+
1123
+ # Order for consistent display
1124
+ orden = database.Column(database.Integer, nullable=False, default=0, index=True)
1125
+
1126
+ comprobante = database.relationship("ComprobanteContable", back_populates="lineas")
1127
+ nomina_empleado = database.relationship("NominaEmpleado")
1128
+ empleado = database.relationship("Empleado")
1129
+
1130
+
1131
+ # Historial de cambios de salario
1132
+ class HistorialSalario(database.Model, BaseTabla):
1133
+ __tablename__ = "historial_salario"
1134
+
1135
+ empleado_id = database.Column(
1136
+ database.String(26),
1137
+ database.ForeignKey("empleado.id"),
1138
+ nullable=False,
1139
+ index=True,
1140
+ )
1141
+ fecha_efectiva = database.Column(database.Date, nullable=False, index=True)
1142
+ salario_anterior = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1143
+ salario_nuevo = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1144
+ motivo = database.Column(database.String(255), nullable=True)
1145
+ autorizado_por = database.Column(database.String(150), nullable=True)
1146
+
1147
+ empleado = database.relationship("Empleado", back_populates="historial_salarios")
1148
+
1149
+
1150
+ # Configuración de vacaciones por país/empresa
1151
+ class ConfiguracionVacaciones(database.Model, BaseTabla):
1152
+ __tablename__ = "configuracion_vacaciones"
1153
+
1154
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
1155
+ descripcion = database.Column(database.String(255), nullable=True)
1156
+ dias_por_mes = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("2.50"))
1157
+ dias_minimos_descanso = database.Column(database.Integer, nullable=False, default=1)
1158
+ dias_maximos_acumulables = database.Column(database.Integer, nullable=True)
1159
+ meses_minimos_para_devengar = database.Column(database.Integer, nullable=False, default=1)
1160
+ activo = database.Column(database.Boolean(), default=True)
1161
+
1162
+ vacaciones_empleados = database.relationship("VacacionEmpleado", back_populates="configuracion")
1163
+
1164
+
1165
+ # Saldo y control de vacaciones por empleado
1166
+ class VacacionEmpleado(database.Model, BaseTabla):
1167
+ __tablename__ = "vacacion_empleado"
1168
+ __table_args__ = (database.UniqueConstraint("empleado_id", "anio", name="uq_vacacion_empleado_anio"),)
1169
+
1170
+ empleado_id = database.Column(
1171
+ database.String(26),
1172
+ database.ForeignKey("empleado.id"),
1173
+ nullable=False,
1174
+ index=True,
1175
+ )
1176
+ configuracion_id = database.Column(
1177
+ database.String(26),
1178
+ database.ForeignKey("configuracion_vacaciones.id"),
1179
+ nullable=False,
1180
+ )
1181
+ anio = database.Column(database.Integer, nullable=False)
1182
+ dias_devengados = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1183
+ dias_tomados = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1184
+ dias_pendientes = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1185
+ dias_pagados = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1186
+ fecha_ultimo_calculo = database.Column(database.Date, nullable=True)
1187
+
1188
+ empleado = database.relationship("Empleado", back_populates="vacaciones")
1189
+ configuracion = database.relationship("ConfiguracionVacaciones", back_populates="vacaciones_empleados")
1190
+
1191
+
1192
+ # Registro de vacaciones descansadas
1193
+ class VacacionDescansada(database.Model, BaseTabla):
1194
+ __tablename__ = "vacacion_descansada"
1195
+
1196
+ empleado_id = database.Column(
1197
+ database.String(26),
1198
+ database.ForeignKey("empleado.id"),
1199
+ nullable=False,
1200
+ index=True,
1201
+ )
1202
+ fecha_inicio = database.Column(database.Date, nullable=False)
1203
+ fecha_fin = database.Column(database.Date, nullable=False)
1204
+ dias_tomados = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1205
+ estado = database.Column(database.String(30), nullable=False, default="pendiente")
1206
+ autorizado_por = database.Column(database.String(150), nullable=True)
1207
+ fecha_autorizacion = database.Column(database.Date, nullable=True)
1208
+ observaciones = database.Column(database.String(500), nullable=True)
1209
+
1210
+ empleado = database.relationship("Empleado", back_populates="vacaciones_descansadas")
1211
+
1212
+
1213
+ # Tabla de impuestos (tramos fiscales)
1214
+ class TablaImpuesto(database.Model, BaseTabla):
1215
+ __tablename__ = "tabla_impuesto"
1216
+ __table_args__ = (
1217
+ database.UniqueConstraint(
1218
+ "deduccion_id",
1219
+ "limite_inferior",
1220
+ "vigente_desde",
1221
+ name="uq_impuesto_tramo_vigencia",
1222
+ ),
1223
+ )
1224
+
1225
+ deduccion_id = database.Column(
1226
+ database.String(26),
1227
+ database.ForeignKey("deduccion.id"),
1228
+ nullable=False,
1229
+ index=True,
1230
+ )
1231
+ limite_inferior = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1232
+ limite_superior = database.Column(database.Numeric(14, 2), nullable=True)
1233
+ porcentaje = database.Column(database.Numeric(5, 2), nullable=False, default=Decimal("0.00"))
1234
+ cuota_fija = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1235
+ sobre_excedente_de = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1236
+ vigente_desde = database.Column(database.Date, nullable=False)
1237
+ vigente_hasta = database.Column(database.Date, nullable=True)
1238
+ activo = database.Column(database.Boolean(), default=True)
1239
+
1240
+ deduccion = database.relationship("Deduccion", back_populates="tablas_impuesto")
1241
+
1242
+
1243
+ # Adelantos de salario y préstamos
1244
+ class Adelanto(database.Model, BaseTabla):
1245
+ """Loan and salary advance management.
1246
+
1247
+ Supports both loans (préstamos) with interest rates and salary advances (adelantos).
1248
+ Can handle multi-currency loans with automatic conversion tracking.
1249
+ """
1250
+
1251
+ __tablename__ = "adelanto"
1252
+
1253
+ empleado_id = database.Column(
1254
+ database.String(26),
1255
+ database.ForeignKey("empleado.id"),
1256
+ nullable=False,
1257
+ index=True,
1258
+ )
1259
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
1260
+
1261
+ # Tipo: prestamo o adelanto
1262
+ tipo = database.Column(database.String(20), nullable=False, default="adelanto") # adelanto, prestamo
1263
+
1264
+ # Fechas
1265
+ fecha_solicitud = database.Column(database.Date, nullable=False, default=date.today)
1266
+ fecha_aprobacion = database.Column(database.Date, nullable=True)
1267
+ fecha_desembolso = database.Column(database.Date, nullable=True)
1268
+
1269
+ # Montos
1270
+ monto_solicitado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1271
+ monto_aprobado = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1272
+ saldo_pendiente = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1273
+
1274
+ # Currency support - loan can be in different currency than payroll
1275
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=True)
1276
+ # Track amounts in both loan currency and payroll currency
1277
+ monto_deducido_moneda_planilla = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1278
+ monto_aplicado_moneda_prestamo = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1279
+
1280
+ # Cuotas
1281
+ cuotas_pactadas = database.Column(database.Integer, nullable=True)
1282
+ monto_por_cuota = database.Column(database.Numeric(14, 2), nullable=True, default=Decimal("0.00"))
1283
+
1284
+ # Interest rates (for loans)
1285
+ tasa_interes = database.Column(
1286
+ database.Numeric(5, 4), nullable=True, default=Decimal("0.0000")
1287
+ ) # e.g., 0.0500 = 5%
1288
+ tipo_interes = database.Column(database.String(20), nullable=True, default="ninguno") # ninguno, simple, compuesto
1289
+
1290
+ # Amortization method (for loans with interest)
1291
+ metodo_amortizacion = database.Column(
1292
+ database.String(20), nullable=True, default="frances"
1293
+ ) # frances (constant payment), aleman (constant amortization)
1294
+
1295
+ # Interest tracking
1296
+ interes_acumulado = database.Column(
1297
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1298
+ ) # Total interest accumulated
1299
+ fecha_ultimo_calculo_interes = database.Column(database.Date, nullable=True) # Last date interest was calculated
1300
+
1301
+ # Accounting fields for initial disbursement
1302
+ cuenta_debe = database.Column(database.String(64), nullable=True)
1303
+ descripcion_cuenta_debe = database.Column(database.String(255), nullable=True)
1304
+ cuenta_haber = database.Column(database.String(64), nullable=True)
1305
+ descripcion_cuenta_haber = database.Column(database.String(255), nullable=True)
1306
+
1307
+ # Estado: borrador, pendiente, aprobado, aplicado (pagado), rechazado, cancelado
1308
+ estado = database.Column(database.String(30), nullable=False, default="borrador")
1309
+ motivo = database.Column(database.String(500), nullable=True)
1310
+ aprobado_por = database.Column(database.String(150), nullable=True)
1311
+ rechazado_por = database.Column(database.String(150), nullable=True)
1312
+ motivo_rechazo = database.Column(database.String(500), nullable=True)
1313
+
1314
+ empleado = database.relationship("Empleado", back_populates="adelantos")
1315
+ deduccion = database.relationship("Deduccion", back_populates="adelantos")
1316
+ moneda = database.relationship("Moneda")
1317
+ abonos = database.relationship("AdelantoAbono", back_populates="adelanto", cascade="all,delete-orphan")
1318
+ intereses = database.relationship("InteresAdelanto", back_populates="adelanto", cascade="all,delete-orphan")
1319
+
1320
+
1321
+ # Control de abonos/pagos a adelantos
1322
+ class AdelantoAbono(database.Model, BaseTabla):
1323
+ """Payment record for loans and advances.
1324
+
1325
+ Tracks all payments made against a loan, whether automatic (from payroll)
1326
+ or manual (cash, bank transfer, etc.). For manual payments, includes
1327
+ comprehensive audit trail information.
1328
+ """
1329
+
1330
+ __tablename__ = "adelanto_abono"
1331
+
1332
+ adelanto_id = database.Column(
1333
+ database.String(26),
1334
+ database.ForeignKey("adelanto.id"),
1335
+ nullable=False,
1336
+ index=True,
1337
+ )
1338
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=True)
1339
+ liquidacion_id = database.Column(database.String(26), database.ForeignKey("liquidacion.id"), nullable=True)
1340
+ fecha_abono = database.Column(database.Date, nullable=False, default=date.today)
1341
+ monto_abonado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1342
+ saldo_anterior = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1343
+ saldo_posterior = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1344
+ tipo_abono = database.Column(database.String(30), nullable=False, default="nomina") # nomina, manual, condonacion
1345
+ observaciones = database.Column(database.String(500), nullable=True)
1346
+
1347
+ # Audit trail fields for manual payments
1348
+ # These fields ensure compliance and traceability for financial audits
1349
+ tipo_comprobante = database.Column(
1350
+ database.String(50), nullable=True
1351
+ ) # recibo_caja, minuta_deposito, transferencia, cheque, otro
1352
+ numero_comprobante = database.Column(database.String(100), nullable=True) # Receipt/document number
1353
+ referencia_bancaria = database.Column(database.String(100), nullable=True) # Bank reference for electronic payments
1354
+
1355
+ # Accounting entries for manual payments/deductions
1356
+ # Optional for payments/forgiveness, but useful for financial reconciliation
1357
+ cuenta_debe = database.Column(database.String(64), nullable=True) # Debit account for payment/deduction
1358
+ descripcion_cuenta_debe = database.Column(database.String(255), nullable=True)
1359
+ cuenta_haber = database.Column(database.String(64), nullable=True) # Credit account for payment/deduction
1360
+ descripcion_cuenta_haber = database.Column(database.String(255), nullable=True)
1361
+
1362
+ # Loan forgiveness/write-off fields (condonación)
1363
+ # Used when company decides not to collect part or all of the loan
1364
+ autorizado_por = database.Column(database.String(150), nullable=True) # Name/title of authorizing person
1365
+ documento_soporte = database.Column(
1366
+ database.String(50), nullable=True
1367
+ ) # Type: correo, memorandum, acta, resolucion, carta, otro
1368
+ referencia_documento = database.Column(
1369
+ database.String(200), nullable=True
1370
+ ) # Document reference (date, number, etc.)
1371
+ justificacion = database.Column(database.Text, nullable=True) # Full justification text for audit trail
1372
+
1373
+ adelanto = database.relationship("Adelanto", back_populates="abonos")
1374
+ nomina = database.relationship("Nomina")
1375
+ liquidacion = database.relationship("Liquidacion")
1376
+
1377
+
1378
+ # Interest journal for loans
1379
+ class InteresAdelanto(database.Model, BaseTabla):
1380
+ """Interest calculation journal for loans.
1381
+
1382
+ Tracks interest calculations for each loan during payroll processing.
1383
+ Each payroll execution calculates interest for the days elapsed since
1384
+ the last calculation and records it here.
1385
+
1386
+ This provides a complete audit trail of interest calculations and ensures
1387
+ interest is properly tracked and applied to the loan balance.
1388
+ """
1389
+
1390
+ __tablename__ = "interes_adelanto"
1391
+
1392
+ adelanto_id = database.Column(
1393
+ database.String(26),
1394
+ database.ForeignKey("adelanto.id"),
1395
+ nullable=False,
1396
+ index=True,
1397
+ )
1398
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=True)
1399
+
1400
+ # Calculation period
1401
+ fecha_desde = database.Column(database.Date, nullable=False)
1402
+ fecha_hasta = database.Column(database.Date, nullable=False)
1403
+ dias_transcurridos = database.Column(database.Integer, nullable=False)
1404
+
1405
+ # Interest calculation
1406
+ saldo_base = database.Column(
1407
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1408
+ ) # Balance used for interest calculation
1409
+ tasa_aplicada = database.Column(
1410
+ database.Numeric(5, 4), nullable=False, default=Decimal("0.0000")
1411
+ ) # Interest rate applied
1412
+ interes_calculado = database.Column(
1413
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1414
+ ) # Interest amount calculated
1415
+
1416
+ # Balance tracking
1417
+ saldo_anterior = database.Column(
1418
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1419
+ ) # Balance before interest
1420
+ saldo_posterior = database.Column(
1421
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1422
+ ) # Balance after interest
1423
+
1424
+ observaciones = database.Column(database.String(500), nullable=True)
1425
+
1426
+ adelanto = database.relationship("Adelanto", back_populates="intereses")
1427
+ nomina = database.relationship("Nomina")
1428
+
1429
+
1430
+ # Definición de campos personalizados para empleados
1431
+ class CampoPersonalizado(database.Model, BaseTabla):
1432
+ """Custom field definition for employee records.
1433
+
1434
+ Stores the definition of custom fields that can be added to employee records.
1435
+ The actual values are stored in the `datos_adicionales` JSON column of Empleado.
1436
+
1437
+ Field types:
1438
+ - texto: String/text field
1439
+ - entero: Integer field
1440
+ - decimal: Decimal/float field
1441
+ - booleano: Boolean (true/false) field
1442
+ """
1443
+
1444
+ __tablename__ = "campo_personalizado"
1445
+ __table_args__ = (database.UniqueConstraint("nombre_campo", name="uq_campo_nombre"),)
1446
+
1447
+ nombre_campo = database.Column(database.String(100), unique=True, nullable=False, index=True)
1448
+ etiqueta = database.Column(database.String(150), nullable=False)
1449
+ tipo_dato = database.Column(
1450
+ database.String(20), nullable=False, default="texto"
1451
+ ) # texto, entero, decimal, booleano
1452
+ descripcion = database.Column(database.String(255), nullable=True)
1453
+ orden = database.Column(database.Integer, nullable=False, default=0)
1454
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
1455
+
1456
+
1457
+ # Reglas de cálculo (impuestos, percepciones, deducciones complejas)
1458
+ class ReglaCalculo(database.Model, BaseTabla):
1459
+ """Calculation rules for taxes, perceptions, and deductions.
1460
+
1461
+ Stores the complete JSON schema for calculating complex rules like
1462
+ income tax (IR), social security deductions, etc. The schema defines
1463
+ variables, conditions, formulas, and tax lookup tables.
1464
+
1465
+ This allows country-agnostic configuration of tax rules that can be
1466
+ versioned and applied based on effective dates.
1467
+ """
1468
+
1469
+ __tablename__ = "regla_calculo"
1470
+ __table_args__ = (database.UniqueConstraint("codigo", "version", name="uq_regla_codigo_version"),)
1471
+
1472
+ codigo = database.Column(
1473
+ database.String(50), nullable=False, index=True
1474
+ ) # e.g., 'INCOME_TAX_001', 'SOCIAL_SEC_001'
1475
+ nombre = database.Column(database.String(150), nullable=False)
1476
+ descripcion = database.Column(database.Text, nullable=True)
1477
+ jurisdiccion = database.Column(database.String(100), nullable=True) # e.g., 'Country A', 'Region B'
1478
+
1479
+ # Reference currency for the tax rule calculations.
1480
+ # The rule is currency-agnostic - the actual payroll currency is defined
1481
+ # in TipoPlanilla. When the payroll currency differs from the reference
1482
+ # currency, exchange rates are applied during calculation.
1483
+ # Example: A tax rule may use local currency as reference, but payroll can be in another currency.
1484
+ moneda_referencia = database.Column(
1485
+ database.String(10), nullable=True
1486
+ ) # e.g., 'USD', 'EUR' - reference currency for rule calculations
1487
+
1488
+ version = database.Column(database.String(20), nullable=False, default="1.0.0") # Semantic versioning
1489
+
1490
+ # Type of rule: 'impuesto', 'deduccion', 'percepcion', 'prestacion'
1491
+ tipo_regla = database.Column(database.String(30), nullable=False, default="impuesto")
1492
+
1493
+ # The complete JSON schema defining the calculation logic
1494
+ # Structure includes: meta, inputs, steps, tax_tables, output
1495
+ esquema_json = database.Column(MutableDict.as_mutable(OrjsonType), nullable=False, default=dict)
1496
+
1497
+ # Validity period
1498
+ vigente_desde = database.Column(database.Date, nullable=False)
1499
+ vigente_hasta = database.Column(database.Date, nullable=True)
1500
+
1501
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
1502
+
1503
+ # Optional relationship to specific deduction/perception/benefit
1504
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
1505
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=True)
1506
+ prestacion_id = database.Column(database.String(26), database.ForeignKey("prestacion.id"), nullable=True)
1507
+
1508
+ # Audit and governance fields
1509
+ estado_aprobacion = database.Column(database.String(20), nullable=False, default="borrador", index=True)
1510
+ aprobado_por = database.Column(database.String(150), nullable=True)
1511
+ aprobado_en = database.Column(database.DateTime, nullable=True)
1512
+ creado_por_plugin = database.Column(database.Boolean(), default=False, nullable=False)
1513
+ plugin_source = database.Column(database.String(200), nullable=True)
1514
+
1515
+ # Relationship to planillas that use this rule
1516
+ planillas = database.relationship(
1517
+ "PlanillaReglaCalculo",
1518
+ back_populates="regla_calculo",
1519
+ )
1520
+ audit_logs = database.relationship(
1521
+ "ReglaCalculoAuditLog",
1522
+ back_populates="regla_calculo",
1523
+ cascade="all, delete-orphan",
1524
+ )
1525
+
1526
+
1527
+ # Acumulados anuales por empleado (para cálculos de impuestos progresivos)
1528
+ class AcumuladoAnual(database.Model, BaseTabla):
1529
+ """Annual accumulated values per employee per payroll type per company.
1530
+
1531
+ Stores running totals of salary, deductions, and taxes for each employee
1532
+ per fiscal year, payroll type, and company. This is essential for progressive tax
1533
+ calculations which require annual accumulated values.
1534
+
1535
+ IMPORTANT: Accumulated values are tracked per company (empresa_id) to support
1536
+ employees who change companies mid-year. Each company maintains separate
1537
+ accumulated values since they are distinct legal entities. For tax calculations
1538
+ that require total annual income across all employers, the initial accumulated
1539
+ values in the Empleado model represent the sum from all previous employers.
1540
+
1541
+ The fiscal year period is defined in the TipoPlanilla (payroll type) to
1542
+ support different fiscal periods (not just Jan-Dec).
1543
+
1544
+ Updated after each payroll run to maintain accurate year-to-date totals.
1545
+ """
1546
+
1547
+ __tablename__ = "acumulado_anual"
1548
+ __table_args__ = (
1549
+ database.UniqueConstraint(
1550
+ "empleado_id",
1551
+ "tipo_planilla_id",
1552
+ "empresa_id",
1553
+ "periodo_fiscal_inicio",
1554
+ name="uq_acumulado_empleado_tipo_empresa_periodo",
1555
+ ),
1556
+ )
1557
+
1558
+ empleado_id = database.Column(
1559
+ database.String(26),
1560
+ database.ForeignKey("empleado.id"),
1561
+ nullable=False,
1562
+ index=True,
1563
+ )
1564
+
1565
+ # Reference to payroll type (which defines the fiscal period)
1566
+ tipo_planilla_id = database.Column(
1567
+ database.String(26),
1568
+ database.ForeignKey("tipo_planilla.id"),
1569
+ nullable=False,
1570
+ index=True,
1571
+ )
1572
+
1573
+ # Company association - critical for employees who change companies
1574
+ # Each company tracks accumulated values separately as they are distinct legal entities
1575
+ empresa_id = database.Column(
1576
+ database.String(26),
1577
+ database.ForeignKey("empresa.id"),
1578
+ nullable=False,
1579
+ index=True,
1580
+ )
1581
+
1582
+ # Fiscal period start date (calculated from TipoPlanilla settings)
1583
+ # This allows tracking accumulated values per fiscal year
1584
+ periodo_fiscal_inicio = database.Column(database.Date, nullable=False, index=True)
1585
+ periodo_fiscal_fin = database.Column(database.Date, nullable=False)
1586
+
1587
+ # Accumulated salary values
1588
+ salario_bruto_acumulado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1589
+ salario_gravable_acumulado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1590
+
1591
+ # Accumulated deductions (before tax)
1592
+ deducciones_antes_impuesto_acumulado = database.Column(
1593
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1594
+ )
1595
+
1596
+ # Accumulated taxes
1597
+ impuesto_retenido_acumulado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1598
+
1599
+ # Number of payrolls processed
1600
+ periodos_procesados = database.Column(database.Integer, nullable=False, default=0)
1601
+
1602
+ # Last processed period
1603
+ ultimo_periodo_procesado = database.Column(database.Date, nullable=True)
1604
+
1605
+ # Monthly accumulated salary (for biweekly/weekly payrolls)
1606
+ # This tracks the accumulated salary in the current calendar month
1607
+ # Essential for calculations that require month-to-date totals
1608
+ salario_acumulado_mes = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1609
+ mes_actual = database.Column(database.Integer, nullable=True) # Current month (1-12) for tracking monthly resets
1610
+
1611
+ # Additional accumulated data (JSON for flexibility)
1612
+ # Can store: inss_acumulado, otras_deducciones_acumuladas, percepciones_acumuladas, etc.
1613
+ datos_adicionales = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
1614
+
1615
+ empleado = database.relationship("Empleado", backref="acumulados_anuales")
1616
+ tipo_planilla = database.relationship("TipoPlanilla", back_populates="acumulados")
1617
+ empresa = database.relationship("Empresa")
1618
+
1619
+ def reset_mes_acumulado_if_needed(self, periodo_fin: date) -> None:
1620
+ """Reset monthly accumulated salary if entering a new month.
1621
+
1622
+ Args:
1623
+ periodo_fin: End date of the current payroll period
1624
+ """
1625
+ if self.mes_actual != periodo_fin.month:
1626
+ self.salario_acumulado_mes = Decimal("0.00")
1627
+ self.mes_actual = periodo_fin.month
1628
+
1629
+
1630
+ # Global configuration settings
1631
+ class ConfiguracionGlobal(database.Model, BaseTabla):
1632
+ """Global configuration settings for the application.
1633
+
1634
+ Stores system-wide settings like default language, theme preferences, etc.
1635
+ Only one record should exist in this table (singleton pattern).
1636
+ """
1637
+
1638
+ __tablename__ = "configuracion_global"
1639
+
1640
+ # Language setting: 'en' for English, 'es' for Spanish
1641
+ idioma = database.Column(database.String(10), nullable=False, default="en")
1642
+
1643
+ # Additional global settings can be stored as JSON
1644
+ configuracion_adicional = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
1645
+
1646
+
1647
+ # Configuration for calculation parameters
1648
+ class ConfiguracionCalculos(database.Model, BaseTabla):
1649
+ """Configuration table for calculation parameters.
1650
+
1651
+ Stores configurable values for payroll calculations that were previously hardcoded.
1652
+ Supports company-specific and country-specific configurations.
1653
+ """
1654
+
1655
+ __tablename__ = "config_calculos"
1656
+ __table_args__ = (database.UniqueConstraint("empresa_id", "pais_id", name="uq_config_empresa_pais"),)
1657
+
1658
+ # Optional relationships - can be None for global defaults
1659
+ empresa_id = database.Column(database.String(26), database.ForeignKey("empresa.id"), nullable=True, index=True)
1660
+ pais_id = database.Column(database.String(26), nullable=True, index=True) # Future: ForeignKey("pais.id")
1661
+
1662
+ # Días base para nómina
1663
+ dias_mes_nomina = database.Column(database.Integer, nullable=False, default=30) # 28, 29, 30, 31
1664
+ dias_anio_nomina = database.Column(database.Integer, nullable=False, default=365) # 360, 365, 366
1665
+ horas_jornada_diaria = database.Column(database.Numeric(4, 2), nullable=False, default=Decimal("8.00"))
1666
+
1667
+ # Vacaciones
1668
+ dias_mes_vacaciones = database.Column(database.Integer, nullable=False, default=30)
1669
+ dias_anio_vacaciones = database.Column(database.Integer, nullable=False, default=365)
1670
+ considerar_bisiesto_vacaciones = database.Column(database.Boolean, nullable=False, default=True)
1671
+
1672
+ # Intereses
1673
+ dias_anio_financiero = database.Column(
1674
+ database.Integer, nullable=False, default=365
1675
+ ) # 360 o 365 (default 365 for compatibility)
1676
+ meses_anio_financiero = database.Column(database.Integer, nullable=False, default=12)
1677
+
1678
+ # Quincenas
1679
+ dias_quincena = database.Column(database.Integer, nullable=False, default=15) # 14 o 15
1680
+
1681
+ # Liquidaciones
1682
+ liquidacion_modo_dias = database.Column(database.String(20), nullable=False, default="calendario")
1683
+ liquidacion_factor_calendario = database.Column(database.Integer, nullable=False, default=30)
1684
+ liquidacion_factor_laboral = database.Column(database.Integer, nullable=False, default=28)
1685
+
1686
+ # Antigüedad
1687
+ dias_mes_antiguedad = database.Column(database.Integer, nullable=False, default=30)
1688
+ dias_anio_antiguedad = database.Column(database.Integer, nullable=False, default=365)
1689
+
1690
+ activo = database.Column(database.Boolean, nullable=False, default=True)
1691
+
1692
+ # Relationships
1693
+ empresa = database.relationship("Empresa", backref="configuraciones_calculos")
1694
+
1695
+
1696
+ # Prestaciones Acumuladas - Accumulated Benefits Tracking (Transactional)
1697
+ class PrestacionAcumulada(database.Model, BaseTabla):
1698
+ """Transactional log of accumulated employee benefits over time.
1699
+
1700
+ IMPORTANT: This is a transactional (append-only) table for audit purposes.
1701
+ Each record represents a single transaction that affects the benefit balance.
1702
+ Never update or delete records - always insert new transactions.
1703
+
1704
+ This table maintains a complete audit trail of each benefit (prestacion) for each employee,
1705
+ independent of which payroll (planilla) they are assigned to. This is critical because
1706
+ employees can change payrolls while their benefit accumulations continue.
1707
+
1708
+ Transaction types:
1709
+ - saldo_inicial: Initial balance loading
1710
+ - adicion: Addition (increase) - typically from payroll provisions
1711
+ - disminucion: Decrease (reduction) - typically from settlements/payments
1712
+ - ajuste: Adjustment (can be positive or negative) - manual corrections
1713
+
1714
+ The accumulation respects the tipo_acumulacion setting:
1715
+ - mensual: Settled and reset monthly (e.g., INSS, INATEC)
1716
+ - anual: Accumulated annually based on payroll configuration
1717
+ - vida_laboral: Accumulated over the employee's entire tenure (e.g., severance)
1718
+ """
1719
+
1720
+ __tablename__ = "prestacion_acumulada"
1721
+ __table_args__ = (
1722
+ database.Index("ix_prestacion_acum_empleado_prestacion", "empleado_id", "prestacion_id"),
1723
+ database.Index("ix_prestacion_acum_fecha", "fecha_transaccion"),
1724
+ database.Index("ix_prestacion_acum_periodo", "anio", "mes"),
1725
+ )
1726
+
1727
+ empleado_id = database.Column(
1728
+ database.String(26),
1729
+ database.ForeignKey("empleado.id"),
1730
+ nullable=False,
1731
+ index=True,
1732
+ )
1733
+ prestacion_id = database.Column(
1734
+ database.String(26),
1735
+ database.ForeignKey("prestacion.id"),
1736
+ nullable=False,
1737
+ index=True,
1738
+ )
1739
+
1740
+ # Transaction details
1741
+ fecha_transaccion = database.Column(database.Date, nullable=False, default=date.today, index=True)
1742
+ tipo_transaccion = database.Column(
1743
+ database.String(20), nullable=False, index=True
1744
+ ) # saldo_inicial | adicion | disminucion | ajuste
1745
+
1746
+ # Period tracking (for reporting and grouping)
1747
+ anio = database.Column(database.Integer, nullable=False, index=True)
1748
+ mes = database.Column(database.Integer, nullable=False) # 1-12
1749
+
1750
+ # Currency tracking
1751
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=False)
1752
+
1753
+ # Transaction amounts
1754
+ # For audit clarity, we store the transaction amount and running balance separately
1755
+ monto_transaccion = database.Column(
1756
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1757
+ ) # Can be positive (addition) or negative (deduction)
1758
+ saldo_anterior = database.Column(
1759
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1760
+ ) # Balance before this transaction
1761
+ saldo_nuevo = database.Column(
1762
+ database.Numeric(14, 2), nullable=False, default=Decimal("0.00")
1763
+ ) # Balance after this transaction
1764
+
1765
+ # Reference to source document that created this transaction
1766
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=True)
1767
+ carga_inicial_id = database.Column(
1768
+ database.String(26), database.ForeignKey("carga_inicial_prestacion.id"), nullable=True
1769
+ )
1770
+
1771
+ # Company association - for employees who change companies
1772
+ # Balances accumulate per company as they are distinct legal entities
1773
+ empresa_id = database.Column(
1774
+ database.String(26),
1775
+ database.ForeignKey("empresa.id"),
1776
+ nullable=True,
1777
+ index=True,
1778
+ )
1779
+
1780
+ # Reversal tracking (if this transaction reverses another)
1781
+ transaccion_reversada_id = database.Column(database.String(26), nullable=True) # FK to another transaction
1782
+
1783
+ # Audit trail
1784
+ observaciones = database.Column(database.String(500), nullable=True)
1785
+ procesado_por = database.Column(database.String(150), nullable=True)
1786
+
1787
+ # Relationships
1788
+ empleado = database.relationship("Empleado")
1789
+ prestacion = database.relationship("Prestacion", back_populates="prestaciones_acumuladas")
1790
+ moneda = database.relationship("Moneda")
1791
+ nomina = database.relationship("Nomina")
1792
+ carga_inicial = database.relationship("CargaInicialPrestacion", back_populates="transacciones")
1793
+ empresa = database.relationship("Empresa")
1794
+
1795
+
1796
+ # Carga Inicial de Prestaciones - Initial Benefit Balance Loading
1797
+ class CargaInicialPrestacion(database.Model, BaseTabla):
1798
+ """Initial benefit balance loading for system implementation.
1799
+
1800
+ When implementing the system mid-year or mid-employment, this table allows
1801
+ loading existing accumulated balances for employees. Once applied, these
1802
+ balances are transferred to the PrestacionAcumulada table.
1803
+
1804
+ Workflow:
1805
+ 1. Create entry in 'borrador' (draft) status
1806
+ 2. Review and validate the data
1807
+ 3. Change status to 'aplicado' (applied)
1808
+ 4. System automatically creates corresponding PrestacionAcumulada record
1809
+ """
1810
+
1811
+ __tablename__ = "carga_inicial_prestacion"
1812
+ __table_args__ = (
1813
+ database.UniqueConstraint(
1814
+ "empleado_id",
1815
+ "prestacion_id",
1816
+ "anio_corte",
1817
+ "mes_corte",
1818
+ name="uq_carga_inicial_emp_prest_periodo",
1819
+ ),
1820
+ )
1821
+
1822
+ empleado_id = database.Column(
1823
+ database.String(26),
1824
+ database.ForeignKey("empleado.id"),
1825
+ nullable=False,
1826
+ index=True,
1827
+ )
1828
+ prestacion_id = database.Column(
1829
+ database.String(26),
1830
+ database.ForeignKey("prestacion.id"),
1831
+ nullable=False,
1832
+ index=True,
1833
+ )
1834
+
1835
+ # Cutoff period (when this balance was calculated)
1836
+ anio_corte = database.Column(database.Integer, nullable=False)
1837
+ mes_corte = database.Column(database.Integer, nullable=False) # 1-12
1838
+
1839
+ # Currency and exchange rate
1840
+ moneda_id = database.Column(database.String(26), database.ForeignKey("moneda.id"), nullable=False)
1841
+ saldo_acumulado = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1842
+ tipo_cambio = database.Column(database.Numeric(24, 10), nullable=True, default=Decimal("1.0000000000"))
1843
+ saldo_convertido = database.Column(database.Numeric(14, 2), nullable=False, default=Decimal("0.00"))
1844
+
1845
+ # Status: borrador (draft) or aplicado (applied)
1846
+ estado = database.Column(database.String(20), nullable=False, default="borrador") # borrador | aplicado
1847
+
1848
+ # Application tracking
1849
+ fecha_aplicacion = database.Column(database.DateTime, nullable=True)
1850
+ aplicado_por = database.Column(database.String(150), nullable=True)
1851
+
1852
+ # Notes
1853
+ observaciones = database.Column(database.String(500), nullable=True)
1854
+
1855
+ # Relationships
1856
+ empleado = database.relationship("Empleado")
1857
+ prestacion = database.relationship("Prestacion", back_populates="cargas_iniciales")
1858
+ moneda = database.relationship("Moneda")
1859
+ transacciones = database.relationship("PrestacionAcumulada", back_populates="carga_inicial")
1860
+
1861
+
1862
+ # ============================================================================
1863
+ # Vacation Module - Robust, Flexible, Country-Agnostic
1864
+ # ============================================================================
1865
+
1866
+
1867
+ class VacationPolicy(database.Model, BaseTabla):
1868
+ """Vacation policy configuration (payroll/company-specific).
1869
+
1870
+ This model represents the policy engine for vacation management.
1871
+ Policies are configurable and define how vacation is accrued, used, and expired.
1872
+ Completely agnostic to country-specific laws - all rules are configuration-based.
1873
+
1874
+ Design principles:
1875
+ - Policies are declarative, not hardcoded
1876
+ - Support for all Americas (LATAM, USA, Canada)
1877
+ - Flexible enough to handle diverse legal requirements
1878
+ - Associated with Planillas (payrolls) to support multiple countries in consolidated companies
1879
+ """
1880
+
1881
+ __tablename__ = "vacation_policy"
1882
+ __table_args__ = (
1883
+ database.UniqueConstraint("codigo", name="uq_vacation_policy_codigo"),
1884
+ database.Index("ix_vacation_policy_empresa", "empresa_id"),
1885
+ database.Index("ix_vacation_policy_planilla", "planilla_id"),
1886
+ )
1887
+
1888
+ # Policy identification
1889
+ codigo = database.Column(database.String(50), unique=True, nullable=False, index=True)
1890
+ nombre = database.Column(database.String(200), nullable=False)
1891
+ descripcion = database.Column(database.String(500), nullable=True)
1892
+
1893
+ # Payroll association (primary) - policies are tied to specific payrolls
1894
+ # This allows different vacation rules for different payrolls in consolidated companies
1895
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=True, index=True)
1896
+ planilla = database.relationship("Planilla", backref="vacation_policies")
1897
+
1898
+ # Company association (secondary, optional) - for policies that apply to entire company
1899
+ empresa_id = database.Column(database.String(26), database.ForeignKey("empresa.id"), nullable=True, index=True)
1900
+ empresa = database.relationship("Empresa")
1901
+
1902
+ # Status
1903
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
1904
+
1905
+ # ---- Accrual Configuration ----
1906
+ # How vacation is earned
1907
+ accrual_method = database.Column(
1908
+ database.String(20), nullable=False, default="periodic"
1909
+ ) # periodic | proportional | seniority
1910
+
1911
+ # Amount earned per period (for periodic method)
1912
+ accrual_rate = database.Column(database.Numeric(10, 4), nullable=False, default=Decimal("0.0000"))
1913
+
1914
+ # How often accrual happens
1915
+ accrual_frequency = database.Column(
1916
+ database.String(20), nullable=False, default="monthly"
1917
+ ) # monthly | biweekly | annual
1918
+
1919
+ # Basis for proportional calculation
1920
+ accrual_basis = database.Column(
1921
+ database.String(20), nullable=True
1922
+ ) # days_worked | hours_worked (used when accrual_method=proportional)
1923
+
1924
+ # Minimum service days before accrual begins
1925
+ min_service_days = database.Column(database.Integer, nullable=False, default=0)
1926
+
1927
+ # Seniority tiers (JSON format for flexibility)
1928
+ # Example: [{"years": 0, "rate": 10}, {"years": 2, "rate": 15}, {"years": 5, "rate": 20}]
1929
+ seniority_tiers = database.Column(JSON, nullable=True)
1930
+
1931
+ # ---- Balance Limits ----
1932
+ # Maximum balance allowed
1933
+ max_balance = database.Column(database.Numeric(10, 4), nullable=True)
1934
+
1935
+ # Maximum that can be carried over to next period
1936
+ carryover_limit = database.Column(database.Numeric(10, 4), nullable=True)
1937
+
1938
+ # Allow negative balance (advance vacation)
1939
+ allow_negative = database.Column(database.Boolean(), default=False, nullable=False)
1940
+
1941
+ # ---- Expiration Rules ----
1942
+ # When does unused vacation expire
1943
+ expiration_rule = database.Column(
1944
+ database.String(20), nullable=False, default="never"
1945
+ ) # never | fiscal_year_end | anniversary | custom_date
1946
+
1947
+ # Months after accrual when vacation expires (used with fiscal_year_end, anniversary)
1948
+ expiration_months = database.Column(database.Integer, nullable=True)
1949
+
1950
+ # Custom expiration date (used with custom_date rule)
1951
+ expiration_date = database.Column(database.Date, nullable=True)
1952
+
1953
+ # ---- Termination Rules ----
1954
+ # Pay out unused vacation on termination
1955
+ payout_on_termination = database.Column(database.Boolean(), default=True, nullable=False)
1956
+
1957
+ # ---- Usage Configuration ----
1958
+ # Unit type for vacation balances
1959
+ unit_type = database.Column(database.String(10), nullable=False, default="days") # days | hours
1960
+
1961
+ # Count weekends when calculating vacation days
1962
+ count_weekends = database.Column(database.Boolean(), default=True, nullable=False)
1963
+
1964
+ # Count holidays when calculating vacation days
1965
+ count_holidays = database.Column(database.Boolean(), default=True, nullable=False)
1966
+
1967
+ # Allow partial days/hours
1968
+ partial_units_allowed = database.Column(database.Boolean(), default=False, nullable=False)
1969
+
1970
+ # Rounding rule (for partial units): up | down | nearest
1971
+ rounding_rule = database.Column(database.String(10), nullable=True, default="nearest")
1972
+
1973
+ # Continue accruing during vacation leave
1974
+ accrue_during_leave = database.Column(database.Boolean(), default=True, nullable=False)
1975
+
1976
+ # Additional configuration (JSON for future extensibility)
1977
+ configuracion_adicional = database.Column(JSON, nullable=True)
1978
+
1979
+ # Relationships
1980
+ accounts = database.relationship("VacationAccount", back_populates="policy")
1981
+
1982
+
1983
+ class VacationAccount(database.Model, BaseTabla):
1984
+ """Vacation account per employee.
1985
+
1986
+ Represents the vacation balance for a single employee under a specific policy.
1987
+ This is the control record that tracks current balance and last accrual.
1988
+
1989
+ IMPORTANT: Never update balance directly. All changes must go through VacationLedger.
1990
+ """
1991
+
1992
+ __tablename__ = "vacation_account"
1993
+ __table_args__ = (
1994
+ database.UniqueConstraint("empleado_id", "policy_id", name="uq_vacation_account_emp_policy"),
1995
+ database.Index("ix_vacation_account_empleado", "empleado_id"),
1996
+ database.Index("ix_vacation_account_policy", "policy_id"),
1997
+ )
1998
+
1999
+ # Employee and policy association
2000
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False, index=True)
2001
+ empleado = database.relationship("Empleado")
2002
+
2003
+ policy_id = database.Column(
2004
+ database.String(26), database.ForeignKey("vacation_policy.id"), nullable=False, index=True
2005
+ )
2006
+ policy = database.relationship("VacationPolicy", back_populates="accounts")
2007
+
2008
+ # Current balance (calculated from ledger)
2009
+ current_balance = database.Column(database.Numeric(10, 4), nullable=False, default=Decimal("0.0000"))
2010
+
2011
+ # Last accrual date (for automated accrual processing)
2012
+ last_accrual_date = database.Column(database.Date, nullable=True)
2013
+
2014
+ # Status
2015
+ activo = database.Column(database.Boolean(), default=True, nullable=False)
2016
+
2017
+ # Relationships
2018
+ ledger_entries = database.relationship("VacationLedger", back_populates="account", order_by="VacationLedger.fecha")
2019
+
2020
+
2021
+ class VacationLedger(database.Model, BaseTabla):
2022
+ """Immutable ledger of all vacation transactions.
2023
+
2024
+ This is the core of the vacation system - all vacation balance changes are recorded here.
2025
+ The ledger is append-only (immutable) for full audit trail.
2026
+
2027
+ Design principle: Balance = SUM(ledger entries)
2028
+
2029
+ Entry types:
2030
+ - ACCRUAL: Vacation earned
2031
+ - USAGE: Vacation taken
2032
+ - ADJUSTMENT: Manual adjustment
2033
+ - EXPIRATION: Vacation expired
2034
+ - PAYOUT: Vacation paid out (e.g., termination)
2035
+ """
2036
+
2037
+ __tablename__ = "vacation_ledger"
2038
+ __table_args__ = (
2039
+ database.Index("ix_vacation_ledger_account", "account_id"),
2040
+ database.Index("ix_vacation_ledger_fecha", "fecha"),
2041
+ database.Index("ix_vacation_ledger_type", "entry_type"),
2042
+ database.Index("ix_vacation_ledger_empleado", "empleado_id"),
2043
+ )
2044
+
2045
+ # Account reference
2046
+ account_id = database.Column(database.String(26), database.ForeignKey("vacation_account.id"), nullable=False)
2047
+ account = database.relationship("VacationAccount", back_populates="ledger_entries")
2048
+
2049
+ # Employee reference (for easier querying)
2050
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False)
2051
+ empleado = database.relationship("Empleado")
2052
+
2053
+ # Transaction details
2054
+ fecha = database.Column(database.Date, nullable=False, default=date.today)
2055
+ entry_type = database.Column(
2056
+ database.String(20), nullable=False
2057
+ ) # accrual | usage | adjustment | expiration | payout
2058
+
2059
+ # Quantity (positive for additions, negative for deductions)
2060
+ quantity = database.Column(database.Numeric(10, 4), nullable=False)
2061
+
2062
+ # Source of the transaction
2063
+ source = database.Column(database.String(50), nullable=False) # system | novelty | termination | manual
2064
+
2065
+ # Reference to source record (e.g., novelty_id, nomina_id)
2066
+ reference_id = database.Column(database.String(26), nullable=True)
2067
+ reference_type = database.Column(database.String(50), nullable=True) # Type of reference (novelty, nomina, etc.)
2068
+
2069
+ # Notes/description
2070
+ observaciones = database.Column(database.String(500), nullable=True)
2071
+
2072
+ # Balance after this transaction (for convenience, though can be calculated)
2073
+ balance_after = database.Column(database.Numeric(10, 4), nullable=True)
2074
+
2075
+
2076
+ class VacationNovelty(database.Model, BaseTabla):
2077
+ """Vacation leave request/novelty.
2078
+
2079
+ Represents a vacation leave request that affects the vacation balance.
2080
+ When approved, it creates entries in the VacationLedger.
2081
+
2082
+ This integrates the vacation system with the existing novelty workflow.
2083
+ """
2084
+
2085
+ __tablename__ = "vacation_novelty"
2086
+ __table_args__ = (
2087
+ database.Index("ix_vacation_novelty_empleado", "empleado_id"),
2088
+ database.Index("ix_vacation_novelty_account", "account_id"),
2089
+ database.Index("ix_vacation_novelty_estado", "estado"),
2090
+ database.Index("ix_vacation_novelty_dates", "start_date", "end_date"),
2091
+ )
2092
+
2093
+ # Employee and account
2094
+ empleado_id = database.Column(database.String(26), database.ForeignKey("empleado.id"), nullable=False, index=True)
2095
+ empleado = database.relationship("Empleado")
2096
+
2097
+ account_id = database.Column(
2098
+ database.String(26), database.ForeignKey("vacation_account.id"), nullable=False, index=True
2099
+ )
2100
+ account = database.relationship("VacationAccount")
2101
+
2102
+ # Leave dates
2103
+ start_date = database.Column(database.Date, nullable=False, index=True)
2104
+ end_date = database.Column(database.Date, nullable=False, index=True)
2105
+
2106
+ # Units (days or hours, depending on policy)
2107
+ units = database.Column(database.Numeric(10, 4), nullable=False)
2108
+
2109
+ # Status
2110
+ estado = database.Column(
2111
+ database.String(20), nullable=False, default="pendiente"
2112
+ ) # pendiente | aprobado | rechazado | disfrutado
2113
+
2114
+ # Approval tracking
2115
+ fecha_aprobacion = database.Column(database.Date, nullable=True)
2116
+ aprobado_por = database.Column(database.String(150), nullable=True)
2117
+
2118
+ # Link to ledger entry (when processed)
2119
+ ledger_entry_id = database.Column(database.String(26), database.ForeignKey("vacation_ledger.id"), nullable=True)
2120
+ ledger_entry = database.relationship("VacationLedger")
2121
+
2122
+ # Link to payroll novelty (NominaNovedad) when integrated with payroll
2123
+ nomina_novedades = database.relationship(
2124
+ "NominaNovedad", back_populates="vacation_novelty", foreign_keys="NominaNovedad.vacation_novelty_id"
2125
+ )
2126
+
2127
+ # Notes
2128
+ observaciones = database.Column(database.String(500), nullable=True)
2129
+ motivo_rechazo = database.Column(database.String(500), nullable=True)
2130
+
2131
+
2132
+ # ============================================================================
2133
+ # Reports Module
2134
+ # ============================================================================
2135
+
2136
+
2137
+ class Report(database.Model, BaseTabla):
2138
+ """Report definition and configuration.
2139
+
2140
+ Represents both System and Custom reports. System reports are pre-defined
2141
+ in the application code with optimized queries. Custom reports are defined
2142
+ by users through the UI using a declarative JSON-based configuration.
2143
+ """
2144
+
2145
+ __tablename__ = "report"
2146
+ __table_args__ = (database.UniqueConstraint("name", name="uq_report_name"),)
2147
+
2148
+ # Basic information
2149
+ name = database.Column(database.String(150), nullable=False, unique=True, index=True)
2150
+ description = database.Column(database.String(500), nullable=True)
2151
+
2152
+ # Report type: SYSTEM or CUSTOM
2153
+ type = database.Column(database.String(20), nullable=False, default="custom") # system | custom
2154
+
2155
+ # Administrative status
2156
+ status = database.Column(database.String(20), nullable=False, default="enabled") # enabled | disabled
2157
+
2158
+ # Base entity for the report (e.g., Employee, Nomina, Vacation)
2159
+ base_entity = database.Column(database.String(100), nullable=False)
2160
+
2161
+ # Report definition (JSON, nullable for System reports as they're coded)
2162
+ # For Custom reports: contains columns, filters, sorting, expressions
2163
+ definition = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2164
+
2165
+ # System report identifier (for system reports only)
2166
+ # Used to identify the report implementation in code
2167
+ system_report_id = database.Column(database.String(100), nullable=True, unique=True, index=True)
2168
+
2169
+ # Category for organization (e.g., "payroll", "employee", "vacation")
2170
+ category = database.Column(database.String(50), nullable=True, index=True)
2171
+
2172
+ # Relationships
2173
+ permissions = database.relationship("ReportRole", back_populates="report", cascade="all,delete-orphan")
2174
+ executions = database.relationship("ReportExecution", back_populates="report", cascade="all,delete-orphan")
2175
+ audit_entries = database.relationship("ReportAudit", back_populates="report", cascade="all,delete-orphan")
2176
+
2177
+
2178
+ class ReportRole(database.Model, BaseTabla):
2179
+ """Report permissions by user role.
2180
+
2181
+ Defines which user types (admin, hhrr, audit) can view, execute, and
2182
+ export a specific report.
2183
+ """
2184
+
2185
+ __tablename__ = "report_role"
2186
+ __table_args__ = (database.UniqueConstraint("report_id", "role", name="uq_report_role"),)
2187
+
2188
+ # Foreign key to report
2189
+ report_id = database.Column(database.String(26), database.ForeignKey("report.id"), nullable=False)
2190
+ report = database.relationship("Report", back_populates="permissions")
2191
+
2192
+ # User role (admin, hhrr, audit)
2193
+ role = database.Column(database.String(20), nullable=False, index=True)
2194
+
2195
+ # Permissions
2196
+ can_view = database.Column(database.Boolean(), nullable=False, default=True)
2197
+ can_execute = database.Column(database.Boolean(), nullable=False, default=True)
2198
+ can_export = database.Column(database.Boolean(), nullable=False, default=False)
2199
+
2200
+
2201
+ class ReportExecution(database.Model, BaseTabla):
2202
+ """Report execution history and status.
2203
+
2204
+ Tracks report executions including status, parameters, results,
2205
+ and performance metrics. Used for auditing and async execution.
2206
+ """
2207
+
2208
+ __tablename__ = "report_execution"
2209
+
2210
+ # Foreign key to report
2211
+ report_id = database.Column(database.String(26), database.ForeignKey("report.id"), nullable=False)
2212
+ report = database.relationship("Report", back_populates="executions")
2213
+
2214
+ # Execution status
2215
+ status = database.Column(
2216
+ database.String(20), nullable=False, default="queued"
2217
+ ) # queued | running | completed | failed | cancelled
2218
+
2219
+ # Execution parameters (filters applied by user)
2220
+ parameters = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2221
+
2222
+ # User who requested the execution
2223
+ executed_by = database.Column(database.String(150), nullable=False, index=True)
2224
+
2225
+ # Execution timestamps
2226
+ started_at = database.Column(database.DateTime, nullable=True)
2227
+ completed_at = database.Column(database.DateTime, nullable=True)
2228
+
2229
+ # Results
2230
+ row_count = database.Column(database.Integer, nullable=True)
2231
+ execution_time_ms = database.Column(database.Integer, nullable=True) # in milliseconds
2232
+
2233
+ # Error information (if failed)
2234
+ error_message = database.Column(database.String(1000), nullable=True)
2235
+
2236
+ # Export file path (if exported)
2237
+ export_file_path = database.Column(database.String(500), nullable=True)
2238
+ export_format = database.Column(database.String(20), nullable=True) # excel, csv, pdf
2239
+
2240
+
2241
+ class ReportAudit(database.Model, BaseTabla):
2242
+ """Audit trail for report configuration changes.
2243
+
2244
+ Records all changes to report definitions, status, and permissions
2245
+ for compliance and debugging.
2246
+ """
2247
+
2248
+ __tablename__ = "report_audit"
2249
+
2250
+ # Foreign key to report
2251
+ report_id = database.Column(database.String(26), database.ForeignKey("report.id"), nullable=False)
2252
+ report = database.relationship("Report", back_populates="audit_entries")
2253
+
2254
+ # Action performed
2255
+ action = database.Column(
2256
+ database.String(50), nullable=False, index=True
2257
+ ) # created | updated | status_changed | etc
2258
+
2259
+ # User who performed the action
2260
+ performed_by = database.Column(database.String(150), nullable=False, index=True)
2261
+
2262
+ # Changes (JSON storing before/after values)
2263
+ changes = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2264
+
2265
+ # Timestamp is inherited from BaseTabla
2266
+
2267
+
2268
+ class ConceptoAuditLog(database.Model, BaseTabla):
2269
+ """Audit trail for payroll concept changes (percepciones, deducciones, prestaciones).
2270
+
2271
+ Records all changes to payroll concepts including creation, modification, and approval.
2272
+ Tracks who made changes, when, and what was changed for governance and compliance.
2273
+ """
2274
+
2275
+ __tablename__ = "concepto_audit_log"
2276
+
2277
+ # Foreign keys to the concepts (only one will be set)
2278
+ percepcion_id = database.Column(database.String(26), database.ForeignKey("percepcion.id"), nullable=True)
2279
+ deduccion_id = database.Column(database.String(26), database.ForeignKey("deduccion.id"), nullable=True)
2280
+ prestacion_id = database.Column(database.String(26), database.ForeignKey("prestacion.id"), nullable=True)
2281
+
2282
+ # Type of concept (for easier filtering)
2283
+ tipo_concepto = database.Column(
2284
+ database.String(20), nullable=False, index=True
2285
+ ) # percepcion | deduccion | prestacion
2286
+
2287
+ # Action performed
2288
+ accion = database.Column(
2289
+ database.String(50), nullable=False, index=True
2290
+ ) # created | updated | approved | rejected | status_changed
2291
+
2292
+ # User who performed the action
2293
+ usuario = database.Column(database.String(150), nullable=False, index=True)
2294
+
2295
+ # Description of the change (human-readable)
2296
+ descripcion = database.Column(database.String(1000), nullable=True)
2297
+
2298
+ # Detailed changes (JSON storing field-level before/after values)
2299
+ cambios = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2300
+
2301
+ # Previous and new approval status (if applicable)
2302
+ estado_anterior = database.Column(database.String(20), nullable=True)
2303
+ estado_nuevo = database.Column(database.String(20), nullable=True)
2304
+
2305
+ # Relationships
2306
+ percepcion = database.relationship("Percepcion", back_populates="audit_logs")
2307
+ deduccion = database.relationship("Deduccion", back_populates="audit_logs")
2308
+ prestacion = database.relationship("Prestacion", back_populates="audit_logs")
2309
+
2310
+
2311
+ class PlanillaAuditLog(database.Model, BaseTabla):
2312
+ """Audit trail for Planilla changes.
2313
+
2314
+ Records all changes to planillas including creation, modification, approval,
2315
+ and configuration changes (adding/removing employees, concepts, etc.).
2316
+ """
2317
+
2318
+ __tablename__ = "planilla_audit_log"
2319
+
2320
+ # Foreign key to planilla
2321
+ planilla_id = database.Column(database.String(26), database.ForeignKey("planilla.id"), nullable=False)
2322
+
2323
+ # Action performed
2324
+ accion = database.Column(
2325
+ database.String(50), nullable=False, index=True
2326
+ ) # created | updated | approved | rejected | employee_added | employee_removed | concept_added | concept_removed
2327
+
2328
+ # User who performed the action
2329
+ usuario = database.Column(database.String(150), nullable=False, index=True)
2330
+
2331
+ # Description of the change (human-readable)
2332
+ descripcion = database.Column(database.String(1000), nullable=True)
2333
+
2334
+ # Detailed changes (JSON storing field-level before/after values)
2335
+ cambios = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2336
+
2337
+ # Previous and new approval status (if applicable)
2338
+ estado_anterior = database.Column(database.String(20), nullable=True)
2339
+ estado_nuevo = database.Column(database.String(20), nullable=True)
2340
+
2341
+ # Relationship
2342
+ planilla = database.relationship("Planilla", back_populates="audit_logs")
2343
+
2344
+
2345
+ class NominaAuditLog(database.Model, BaseTabla):
2346
+ """Audit trail for Nomina state changes.
2347
+
2348
+ Records all state transitions of nominas: generation, approval, application,
2349
+ cancellation, and any modifications.
2350
+ """
2351
+
2352
+ __tablename__ = "nomina_audit_log"
2353
+
2354
+ # Foreign key to nomina
2355
+ nomina_id = database.Column(database.String(26), database.ForeignKey("nomina.id"), nullable=False)
2356
+
2357
+ # Action performed
2358
+ accion = database.Column(
2359
+ database.String(50), nullable=False, index=True
2360
+ ) # generated | approved | applied | cancelled | recalculated | modified
2361
+
2362
+ # User who performed the action
2363
+ usuario = database.Column(database.String(150), nullable=False, index=True)
2364
+
2365
+ # Description of the change (human-readable)
2366
+ descripcion = database.Column(database.String(1000), nullable=True)
2367
+
2368
+ # Detailed changes (JSON storing field-level before/after values)
2369
+ cambios = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2370
+
2371
+ # Previous and new state (for state transitions)
2372
+ estado_anterior = database.Column(database.String(30), nullable=True)
2373
+ estado_nuevo = database.Column(database.String(30), nullable=True)
2374
+
2375
+ # Relationship
2376
+ nomina = database.relationship("Nomina", back_populates="audit_logs")
2377
+
2378
+
2379
+ class ReglaCalculoAuditLog(database.Model, BaseTabla):
2380
+ """Audit trail for ReglaCalculo changes.
2381
+
2382
+ Records all changes to calculation rules including creation, modification,
2383
+ approval, schema changes, and versioning for SOX/COSO compliance.
2384
+ """
2385
+
2386
+ __tablename__ = "regla_calculo_audit_log"
2387
+
2388
+ # Foreign key to regla_calculo
2389
+ regla_calculo_id = database.Column(database.String(26), database.ForeignKey("regla_calculo.id"), nullable=False)
2390
+
2391
+ # Action performed
2392
+ accion = database.Column(
2393
+ database.String(50), nullable=False, index=True
2394
+ ) # created | updated | approved | rejected | schema_changed | version_changed | status_changed
2395
+
2396
+ # User who performed the action
2397
+ usuario = database.Column(database.String(150), nullable=False, index=True)
2398
+
2399
+ # Description of the change (human-readable)
2400
+ descripcion = database.Column(database.String(1000), nullable=True)
2401
+
2402
+ # Detailed changes (JSON storing field-level before/after values)
2403
+ cambios = database.Column(MutableDict.as_mutable(OrjsonType), nullable=True, default=dict)
2404
+
2405
+ # Previous and new approval status (if applicable)
2406
+ estado_anterior = database.Column(database.String(20), nullable=True)
2407
+ estado_nuevo = database.Column(database.String(20), nullable=True)
2408
+
2409
+ # Relationship
2410
+ regla_calculo = database.relationship("ReglaCalculo", back_populates="audit_logs")