coati-payroll 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coati-payroll might be problematic. Click here for more details.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,415 @@
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
+ """
15
+ Coati Payroll
16
+ =============
17
+
18
+ Simple payroll management system.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ # <-------------------------------------------------------------------------> #
24
+ # Standard library
25
+ # <-------------------------------------------------------------------------> #
26
+ from os import environ
27
+ from datetime import datetime
28
+
29
+ # <-------------------------------------------------------------------------> #
30
+ # Third party packages
31
+ # <-------------------------------------------------------------------------> #
32
+ from flask import Flask, flash, redirect, url_for
33
+ from flask_babel import Babel
34
+ from flask_login import LoginManager
35
+ from flask_session import Session
36
+ from flask_wtf.csrf import CSRFProtect
37
+ import flask_session.sqlalchemy.sqlalchemy as fs_sqlalchemy
38
+ from sqlalchemy import Column, DateTime, Integer, LargeBinary, Sequence, String
39
+
40
+ # <-------------------------------------------------------------------------> #
41
+ # Local modules
42
+ # <-------------------------------------------------------------------------> #
43
+ from coati_payroll.app import app as app_blueprint
44
+ from coati_payroll.auth import auth
45
+ from coati_payroll.config import DIRECTORIO_ARCHIVOS_BASE, DIRECTORIO_PLANTILLAS_BASE
46
+ from coati_payroll.i18n import _
47
+ from coati_payroll.model import Usuario, db
48
+ from coati_payroll.log import log
49
+ from coati_payroll.plugin_manager import get_active_plugins_menu_entries, register_active_plugins, sync_plugin_registry
50
+
51
+
52
+ # Patch Flask-Session to use extend_existing=True for the sessions table
53
+ # This prevents the "Table 'sessions' is already defined" error when the app
54
+ # is initialized multiple times (e.g., by the CLI).
55
+ #
56
+ # NOTE: This monkey-patch is necessary because Flask-Session v0.8.0 does not
57
+ # provide a configuration option to set extend_existing=True on the sessions
58
+ # table. Without this, the CLI fails when it initializes the app multiple times.
59
+ # This is a minimal, surgical fix that only modifies the table_args to add
60
+ # extend_existing=True. If Flask-Session adds native support for this in a
61
+ # future version, this patch can be removed.
62
+ def _patched_create_session_model(db, table_name, schema=None, bind_key=None, sequence=None):
63
+ """Patched version of Flask-Session's create_session_model that includes extend_existing=True."""
64
+
65
+ class Session(db.Model):
66
+ __tablename__ = table_name
67
+ # Include extend_existing=True to allow table redefinition
68
+ __table_args__ = {"schema": schema, "extend_existing": True} if schema else {"extend_existing": True}
69
+ __bind_key__ = bind_key
70
+
71
+ id = Column(Integer, Sequence(sequence), primary_key=True) if sequence else Column(Integer, primary_key=True)
72
+ session_id = Column(String(255), unique=True)
73
+ data = Column(LargeBinary)
74
+ expiry = Column(DateTime)
75
+
76
+ def __init__(self, session_id: str, data, expiry: datetime):
77
+ self.session_id = session_id
78
+ self.data = data
79
+ self.expiry = expiry
80
+
81
+ def __repr__(self):
82
+ return f"<Session data {self.data}>"
83
+
84
+ return Session
85
+
86
+
87
+ fs_sqlalchemy.create_session_model = _patched_create_session_model
88
+
89
+ # Third party libraries
90
+ session_manager = Session()
91
+ login_manager = LoginManager()
92
+ babel = Babel()
93
+ csrf = CSRFProtect()
94
+ limiter = None # Will be initialized in create_app()
95
+
96
+
97
+ # ---------------------------------------------------------------------------------------
98
+ # Control de acceso a la aplicación con la extensión flask_login.
99
+ # ---------------------------------------------------------------------------------------
100
+ @login_manager.user_loader
101
+ def cargar_sesion(identidad):
102
+ """Devuelve la entrada correspondiente al usuario que inicio sesión desde la base de datos."""
103
+ if identidad is not None:
104
+ return db.session.get(Usuario, identidad)
105
+ return None
106
+
107
+
108
+ @login_manager.unauthorized_handler
109
+ def no_autorizado():
110
+ """Redirecciona al inicio de sesión usuarios no autorizados."""
111
+ flash(_("Favor iniciar sesión para acceder al sistema."), "warning")
112
+ return redirect(url_for("auth.login"))
113
+
114
+
115
+ # ---------------------------------------------------------------------------------------
116
+ # Locale selector for Flask-Babel
117
+ # ---------------------------------------------------------------------------------------
118
+ def get_locale():
119
+ """Determine the locale for the current request.
120
+
121
+ Returns the language configured in the database (with caching).
122
+ Falls back to English if database is not available.
123
+ """
124
+ try:
125
+ from coati_payroll.locale_config import get_language_from_db
126
+
127
+ return get_language_from_db()
128
+ except Exception:
129
+ # Fallback to default if database not available (e.g., during initialization)
130
+ return "en"
131
+
132
+
133
+ # ---------------------------------------------------------------------------------------
134
+ # app factory.
135
+ # ---------------------------------------------------------------------------------------
136
+ def create_app(config) -> Flask:
137
+ """App factory."""
138
+
139
+ app = Flask(
140
+ __name__,
141
+ static_folder=DIRECTORIO_ARCHIVOS_BASE,
142
+ template_folder=DIRECTORIO_PLANTILLAS_BASE,
143
+ )
144
+
145
+ if config:
146
+ app.config.from_mapping(config)
147
+ else:
148
+ from coati_payroll.config import configuration
149
+
150
+ app.config.from_object(configuration)
151
+
152
+ # Warn if using default SECRET_KEY in production
153
+ from coati_payroll.config import DESARROLLO
154
+
155
+ if not DESARROLLO and app.config.get("SECRET_KEY") == "dev":
156
+ log.warning("Using default SECRET_KEY in production! This can cause issues.")
157
+
158
+ log.trace("create_app: initializing app")
159
+ db.init_app(app)
160
+
161
+ # Mostrar la URI de la base de datos para diagnóstico
162
+ try:
163
+ db_uri = app.config.get("SQLALCHEMY_DATABASE_URI")
164
+ log.trace(f"create_app: SQLALCHEMY_DATABASE_URI = {db_uri}")
165
+ except Exception:
166
+ log.trace("create_app: could not read SQLALCHEMY_DATABASE_URI from app.config")
167
+
168
+ # Asegurar la creación de las tablas básicas al iniciar la app.
169
+ try:
170
+ log.trace("create_app: calling ensure_database_initialized")
171
+ ensure_database_initialized(app)
172
+ log.trace("create_app: ensure_database_initialized completed")
173
+ except Exception as exc:
174
+ log.trace(f"create_app: ensure_database_initialized raised: {exc}")
175
+ try:
176
+ log.exception("create_app: ensure_database_initialized exception")
177
+ except Exception:
178
+ pass
179
+ # No interrumpir el arranque si la inicialización automática falla.
180
+ pass
181
+
182
+ try:
183
+ with app.app_context():
184
+ sync_plugin_registry()
185
+ except Exception as exc:
186
+ log.trace(f"create_app: sync_plugin_registry raised: {exc}")
187
+
188
+ # Configure session storage
189
+ # In testing mode, respect the SESSION_TYPE from config (e.g., filesystem)
190
+ # to avoid conflicts with parallel test execution
191
+ if not app.config.get("SESSION_TYPE"):
192
+ if session_redis_url := environ.get("SESSION_REDIS_URL", None):
193
+ from redis import Redis
194
+
195
+ app.config["SESSION_TYPE"] = "redis"
196
+ app.config["SESSION_REDIS"] = Redis.from_url(session_redis_url)
197
+
198
+ else:
199
+ app.config["SESSION_TYPE"] = "sqlalchemy"
200
+ app.config["SESSION_SQLALCHEMY"] = db
201
+ app.config["SESSION_SQLALCHEMY_TABLE"] = "sessions"
202
+ app.config["SESSION_PERMANENT"] = False
203
+ app.config["SESSION_USE_SIGNER"] = True
204
+
205
+ # Configure secure session cookies
206
+ # These settings protect against session hijacking and cookie theft
207
+ from datetime import timedelta
208
+
209
+ app.config["SESSION_COOKIE_HTTPONLY"] = True # Prevent JavaScript access to cookies
210
+ app.config["SESSION_COOKIE_SECURE"] = not DESARROLLO # Only send over HTTPS in production
211
+ app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # CSRF protection
212
+ app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=24) # Session timeout
213
+
214
+ # Configure Flask-Babel
215
+ app.config["BABEL_DEFAULT_LOCALE"] = "en"
216
+ app.config["BABEL_TRANSLATION_DIRECTORIES"] = "translations"
217
+ babel.init_app(app, locale_selector=get_locale)
218
+
219
+ session_manager.init_app(app)
220
+ login_manager.init_app(app)
221
+
222
+ # Initialize CSRF protection globally
223
+ # This protects all forms from Cross-Site Request Forgery attacks
224
+ # CSRF is automatically disabled in testing mode (WTF_CSRF_ENABLED = False in test config)
225
+ csrf.init_app(app)
226
+
227
+ # Initialize rate limiting
228
+ # Protects against brute force attacks and abuse
229
+ global limiter
230
+ from coati_payroll.rate_limiting import configure_rate_limiting
231
+
232
+ limiter = configure_rate_limiting(app)
233
+
234
+ # Load initial data and demo data after Babel is initialized
235
+ # This allows translations to work properly
236
+ # Skip loading in test environments to keep test databases clean
237
+ if not app.config.get("TESTING"):
238
+ with app.app_context():
239
+ # Load initial data (currencies, income concepts, deduction concepts)
240
+ # Strings are translated automatically based on the configured language
241
+ try:
242
+ from coati_payroll.initial_data import load_initial_data
243
+
244
+ load_initial_data()
245
+ except Exception as exc:
246
+ log.trace(f"Could not load initial data: {exc}")
247
+
248
+ # Load demo data if COATI_LOAD_DEMO_DATA environment variable is set
249
+ # This provides comprehensive sample data for manual testing
250
+ if environ.get("COATI_LOAD_DEMO_DATA"):
251
+ try:
252
+ from coati_payroll.demo_data import load_demo_data
253
+
254
+ load_demo_data()
255
+ except Exception as exc:
256
+ log.trace(f"Could not load demo data: {exc}")
257
+
258
+ app.register_blueprint(auth, url_prefix="/auth")
259
+ app.register_blueprint(app_blueprint, url_prefix="/")
260
+
261
+ try:
262
+ with app.app_context():
263
+ register_active_plugins(app)
264
+ except Exception as exc:
265
+ log.trace(f"create_app: register_active_plugins raised: {exc}")
266
+
267
+ # Register CRUD blueprints
268
+ from coati_payroll.vistas import (
269
+ user_bp,
270
+ currency_bp,
271
+ exchange_rate_bp,
272
+ employee_bp,
273
+ custom_field_bp,
274
+ calculation_rule_bp,
275
+ percepcion_bp,
276
+ deduccion_bp,
277
+ prestacion_bp,
278
+ planilla_bp,
279
+ tipo_planilla_bp,
280
+ prestamo_bp,
281
+ empresa_bp,
282
+ configuracion_bp,
283
+ carga_inicial_prestacion_bp,
284
+ vacation_bp,
285
+ prestacion_management_bp,
286
+ report_bp,
287
+ settings_bp,
288
+ plugins_bp,
289
+ config_calculos_bp,
290
+ liquidacion_bp,
291
+ )
292
+
293
+ app.register_blueprint(user_bp)
294
+ app.register_blueprint(currency_bp)
295
+ app.register_blueprint(exchange_rate_bp)
296
+ app.register_blueprint(employee_bp)
297
+ app.register_blueprint(custom_field_bp)
298
+ app.register_blueprint(calculation_rule_bp)
299
+ app.register_blueprint(percepcion_bp)
300
+ app.register_blueprint(deduccion_bp)
301
+ app.register_blueprint(prestacion_bp)
302
+ app.register_blueprint(planilla_bp)
303
+ app.register_blueprint(tipo_planilla_bp)
304
+ app.register_blueprint(prestamo_bp)
305
+ app.register_blueprint(empresa_bp)
306
+ app.register_blueprint(configuracion_bp)
307
+ app.register_blueprint(carga_inicial_prestacion_bp)
308
+ app.register_blueprint(vacation_bp)
309
+ app.register_blueprint(prestacion_management_bp)
310
+ app.register_blueprint(report_bp)
311
+ app.register_blueprint(settings_bp)
312
+ app.register_blueprint(plugins_bp)
313
+ app.register_blueprint(config_calculos_bp)
314
+ app.register_blueprint(liquidacion_bp)
315
+
316
+ @app.context_processor
317
+ def inject_plugins_menu():
318
+ try:
319
+ plugin_actives = get_active_plugins_menu_entries()
320
+ except Exception:
321
+ plugin_actives = []
322
+ return {"plugin_actives": plugin_actives}
323
+
324
+ # Register CLI commands
325
+ from coati_payroll.cli import register_cli_commands
326
+
327
+ register_cli_commands(app)
328
+
329
+ # Configure security headers
330
+ # This adds HTTP security headers to all responses to protect against
331
+ # common web vulnerabilities (XSS, clickjacking, MIME sniffing, etc.)
332
+ from coati_payroll.security import configure_security_headers
333
+
334
+ configure_security_headers(app)
335
+
336
+ return app
337
+
338
+
339
+ def ensure_database_initialized(app: Flask | None = None) -> None:
340
+ """Verifica que la base de datos haya sido inicializada.
341
+
342
+ - Si la tabla de `Usuario` no existe, ejecuta `create_all()`.
343
+ - Si no existe al menos un usuario con `tipo='admin'`, crea un usuario
344
+ administrador usando las variables de entorno `ADMIN_USER` y
345
+ `ADMIN_PASSWORD` (con valores por defecto si no están presentes).
346
+
347
+ Esta función puede llamarse con la `app` o desde un `app.app_context()` ya activo.
348
+ """
349
+
350
+ from os import environ as _environ
351
+ from coati_payroll.model import Usuario, db as _db
352
+ from coati_payroll.auth import proteger_passwd as _proteger_passwd
353
+
354
+ # Determinar si debemos usar el contexto de la app pasada o el actual.
355
+ ctx = app
356
+ if ctx is None:
357
+ from flask import current_app
358
+
359
+ ctx = current_app
360
+
361
+ with ctx.app_context():
362
+ # Crear todas las tablas definidas (idempotente). Esto garantiza que
363
+ # el archivo sqlite se cree cuando se use una URI sqlite.
364
+ try:
365
+ # Logear información útil para diagnóstico
366
+ try:
367
+ log.trace(f"ensure_database_initialized: engine.url = {_db.engine.url}")
368
+ except Exception:
369
+ log.trace("ensure_database_initialized: could not read _db.engine.url")
370
+
371
+ try:
372
+ db_uri = ctx.config.get("SQLALCHEMY_DATABASE_URI")
373
+ log.trace(f"ensure_database_initialized: Flask SQLALCHEMY_DATABASE_URI = {db_uri}")
374
+ except Exception:
375
+ log.trace("ensure_database_initialized: could not read SQLALCHEMY_DATABASE_URI from ctx.config")
376
+
377
+ log.trace("ensure_database_initialized: calling create_all()")
378
+ _db.create_all()
379
+ log.trace("ensure_database_initialized: create_all() completed")
380
+ except Exception as exc:
381
+ # Registrar excepción completa para diagnóstico
382
+ log.trace(f"ensure_database_initialized: create_all() raised: {exc}")
383
+ try:
384
+ log.exception("ensure_database_initialized: create_all() exception")
385
+ except Exception:
386
+ pass
387
+ # Re-raise? No — dejar que el llamador decida; aquí se registra la traza.
388
+
389
+ # Comprobar existencia de al menos un admin.
390
+ registro_admin = _db.session.execute(_db.select(Usuario).filter_by(tipo="admin")).scalar_one_or_none()
391
+
392
+ if registro_admin is None:
393
+ # Leer credenciales de entorno o usar valores por defecto.
394
+ admin_user = _environ.get("ADMIN_USER", "coati-admin")
395
+ admin_pass = _environ.get("ADMIN_PASSWORD", "coati-admin")
396
+
397
+ nuevo = Usuario()
398
+ nuevo.usuario = admin_user
399
+ nuevo.acceso = _proteger_passwd(admin_pass)
400
+ nuevo.nombre = "Administrador"
401
+ nuevo.apellido = ""
402
+ nuevo.correo_electronico = None
403
+ nuevo.tipo = "admin"
404
+ nuevo.activo = True
405
+
406
+ _db.session.add(nuevo)
407
+ _db.session.commit()
408
+
409
+ # Initialize language from environment variable if provided
410
+ try:
411
+ from coati_payroll.locale_config import initialize_language_from_env
412
+
413
+ initialize_language_from_env()
414
+ except Exception as exc:
415
+ log.trace(f"Could not initialize language from environment: {exc}")
coati_payroll/app.py ADDED
@@ -0,0 +1,95 @@
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
+ """App module."""
15
+
16
+ from __future__ import annotations
17
+
18
+ # <-------------------------------------------------------------------------> #
19
+ # Standard library
20
+ # <-------------------------------------------------------------------------> #
21
+
22
+ # <-------------------------------------------------------------------------> #
23
+ # Third party libraries
24
+ # <-------------------------------------------------------------------------> #
25
+ from flask import Blueprint, render_template
26
+ from flask_login import login_required
27
+ from sqlalchemy import func
28
+ from sqlalchemy.exc import SQLAlchemyError
29
+
30
+ # <-------------------------------------------------------------------------> #
31
+ # Local modules
32
+ # <-------------------------------------------------------------------------> #
33
+ from coati_payroll.model import db, Empleado, Empresa, Planilla, Nomina
34
+
35
+ app = Blueprint("app", __name__)
36
+
37
+
38
+ @app.route("/")
39
+ @login_required
40
+ def index():
41
+ # Get statistics for dashboard
42
+ total_empleados = (
43
+ db.session.execute(db.select(func.count(Empleado.id)).filter(Empleado.activo.is_(True))).scalar() or 0
44
+ )
45
+ total_empresas = (
46
+ db.session.execute(db.select(func.count(Empresa.id)).filter(Empresa.activo.is_(True))).scalar() or 0
47
+ )
48
+ total_planillas = (
49
+ db.session.execute(db.select(func.count(Planilla.id)).filter(Planilla.activo.is_(True))).scalar() or 0
50
+ )
51
+ total_nominas = db.session.execute(db.select(func.count(Nomina.id))).scalar() or 0
52
+
53
+ # Get recent payrolls (last 5)
54
+ recent_nominas = (
55
+ db.session.execute(db.select(Nomina).order_by(Nomina.fecha_generacion.desc()).limit(5)).scalars().all()
56
+ )
57
+
58
+ return render_template(
59
+ "index.html",
60
+ total_empleados=total_empleados,
61
+ total_empresas=total_empresas,
62
+ total_planillas=total_planillas,
63
+ total_nominas=total_nominas,
64
+ recent_nominas=recent_nominas,
65
+ )
66
+
67
+
68
+ @app.route("/health")
69
+ def health():
70
+ """Health check endpoint for container orchestration.
71
+
72
+ Returns a simple OK response to indicate the application is running.
73
+ This endpoint does not require authentication and does not check database connectivity.
74
+ """
75
+ return {"status": "ok"}, 200
76
+
77
+
78
+ @app.route("/ready")
79
+ def ready():
80
+ """Readiness check endpoint for container orchestration.
81
+
82
+ Returns OK if the application is ready to serve traffic (database is accessible).
83
+ This endpoint does not require authentication.
84
+ """
85
+ try:
86
+ # Test database connectivity using a fresh connection
87
+ # This avoids issues with session state from other parts of the application
88
+ with db.engine.connect() as connection:
89
+ connection.execute(db.text("SELECT 1")).scalar()
90
+ return {"status": "ok"}, 200
91
+ except SQLAlchemyError:
92
+ return {"status": "unavailable"}, 503
93
+ except Exception:
94
+ # Catch any other exception that might occur
95
+ return {"status": "unavailable"}, 503