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.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- 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
|