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,432 @@
|
|
|
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
|
+
"""Report management routes."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify, send_file
|
|
21
|
+
from flask_login import current_user
|
|
22
|
+
|
|
23
|
+
from coati_payroll.enums import ReportType, ReportStatus, TipoUsuario
|
|
24
|
+
from coati_payroll.i18n import _
|
|
25
|
+
from coati_payroll.model import db, Report, ReportRole, ReportExecution, ReportAudit
|
|
26
|
+
from coati_payroll.rbac import require_read_access, require_role
|
|
27
|
+
from coati_payroll.report_engine import (
|
|
28
|
+
ReportExecutionManager,
|
|
29
|
+
can_view_report,
|
|
30
|
+
can_execute_report,
|
|
31
|
+
can_export_report,
|
|
32
|
+
)
|
|
33
|
+
from coati_payroll.report_export import export_report_to_excel, export_report_to_csv
|
|
34
|
+
from coati_payroll.system_reports import get_system_report_metadata
|
|
35
|
+
from coati_payroll.log import log
|
|
36
|
+
from coati_payroll.vistas.constants import PER_PAGE
|
|
37
|
+
|
|
38
|
+
report_bp = Blueprint("report", __name__, url_prefix="/report")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Report List and Administration
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@report_bp.route("/")
|
|
47
|
+
@require_read_access()
|
|
48
|
+
def index():
|
|
49
|
+
"""List all available reports.
|
|
50
|
+
|
|
51
|
+
Shows reports based on user permissions.
|
|
52
|
+
"""
|
|
53
|
+
page = request.args.get("page", 1, type=int)
|
|
54
|
+
category = request.args.get("category", "", type=str)
|
|
55
|
+
report_type = request.args.get("type", "", type=str)
|
|
56
|
+
status = request.args.get("status", "", type=str)
|
|
57
|
+
|
|
58
|
+
# Base query
|
|
59
|
+
stmt = db.select(Report)
|
|
60
|
+
|
|
61
|
+
# Apply filters
|
|
62
|
+
if category:
|
|
63
|
+
stmt = stmt.filter(Report.category == category)
|
|
64
|
+
if report_type:
|
|
65
|
+
stmt = stmt.filter(Report.type == report_type)
|
|
66
|
+
if status:
|
|
67
|
+
stmt = stmt.filter(Report.status == status)
|
|
68
|
+
|
|
69
|
+
# Only show enabled reports to non-admin users
|
|
70
|
+
if current_user.tipo != TipoUsuario.ADMIN:
|
|
71
|
+
stmt = stmt.filter(Report.status == ReportStatus.ENABLED)
|
|
72
|
+
|
|
73
|
+
# Filter by user permissions
|
|
74
|
+
if current_user.tipo != TipoUsuario.ADMIN:
|
|
75
|
+
# Filter reports where user has view permission
|
|
76
|
+
stmt = stmt.join(ReportRole).filter(
|
|
77
|
+
ReportRole.role == current_user.tipo, ReportRole.can_view == True # noqa: E712
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
stmt = stmt.order_by(Report.category, Report.name)
|
|
81
|
+
|
|
82
|
+
# Paginate using Flask-SQLAlchemy's paginate method
|
|
83
|
+
pagination = db.paginate(stmt, page=page, per_page=PER_PAGE, error_out=False)
|
|
84
|
+
|
|
85
|
+
# Get unique categories for filter
|
|
86
|
+
categories = (
|
|
87
|
+
db.session.execute(
|
|
88
|
+
db.select(Report.category).distinct().filter(Report.category.isnot(None)).order_by(Report.category)
|
|
89
|
+
)
|
|
90
|
+
.scalars()
|
|
91
|
+
.all()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return render_template(
|
|
95
|
+
"modules/report/index.html",
|
|
96
|
+
reports=pagination.items,
|
|
97
|
+
pagination=pagination,
|
|
98
|
+
categories=categories,
|
|
99
|
+
current_category=category,
|
|
100
|
+
current_type=report_type,
|
|
101
|
+
current_status=status,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@report_bp.route("/admin")
|
|
106
|
+
@require_role(TipoUsuario.ADMIN)
|
|
107
|
+
def admin_index():
|
|
108
|
+
"""Administrative report list.
|
|
109
|
+
|
|
110
|
+
Only accessible to administrators.
|
|
111
|
+
Shows all reports with management options.
|
|
112
|
+
"""
|
|
113
|
+
page = request.args.get("page", 1, type=int)
|
|
114
|
+
category = request.args.get("category", "", type=str)
|
|
115
|
+
report_type = request.args.get("type", "", type=str)
|
|
116
|
+
status = request.args.get("status", "", type=str)
|
|
117
|
+
|
|
118
|
+
# Base query - show all reports for admin
|
|
119
|
+
stmt = db.select(Report)
|
|
120
|
+
|
|
121
|
+
# Apply filters
|
|
122
|
+
if category:
|
|
123
|
+
stmt = stmt.filter(Report.category == category)
|
|
124
|
+
if report_type:
|
|
125
|
+
stmt = stmt.filter(Report.type == report_type)
|
|
126
|
+
if status:
|
|
127
|
+
stmt = stmt.filter(Report.status == status)
|
|
128
|
+
|
|
129
|
+
stmt = stmt.order_by(Report.category, Report.name)
|
|
130
|
+
|
|
131
|
+
# Paginate using Flask-SQLAlchemy's paginate method
|
|
132
|
+
pagination = db.paginate(stmt, page=page, per_page=PER_PAGE, error_out=False)
|
|
133
|
+
|
|
134
|
+
# Get unique categories
|
|
135
|
+
categories = (
|
|
136
|
+
db.session.execute(
|
|
137
|
+
db.select(Report.category).distinct().filter(Report.category.isnot(None)).order_by(Report.category)
|
|
138
|
+
)
|
|
139
|
+
.scalars()
|
|
140
|
+
.all()
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return render_template(
|
|
144
|
+
"modules/report/admin_index.html",
|
|
145
|
+
reports=pagination.items,
|
|
146
|
+
pagination=pagination,
|
|
147
|
+
categories=categories,
|
|
148
|
+
current_category=category,
|
|
149
|
+
current_type=report_type,
|
|
150
|
+
current_status=status,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ============================================================================
|
|
155
|
+
# Report Execution
|
|
156
|
+
# ============================================================================
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@report_bp.route("/<report_id>/execute")
|
|
160
|
+
@require_read_access()
|
|
161
|
+
def execute_form(report_id: str):
|
|
162
|
+
"""Show report execution form.
|
|
163
|
+
|
|
164
|
+
Displays form to input parameters and execute report.
|
|
165
|
+
"""
|
|
166
|
+
report = db.session.get(Report, report_id)
|
|
167
|
+
if not report:
|
|
168
|
+
flash(_("Reporte no encontrado."), "danger")
|
|
169
|
+
return redirect(url_for("report.index"))
|
|
170
|
+
|
|
171
|
+
# Check if report is enabled
|
|
172
|
+
if report.status != ReportStatus.ENABLED and current_user.tipo != TipoUsuario.ADMIN:
|
|
173
|
+
flash(_("Este reporte está deshabilitado."), "warning")
|
|
174
|
+
return redirect(url_for("report.index"))
|
|
175
|
+
|
|
176
|
+
# Check view permission
|
|
177
|
+
if not can_view_report(report, current_user.tipo):
|
|
178
|
+
flash(_("No tiene permisos para ver este reporte."), "danger")
|
|
179
|
+
return redirect(url_for("report.index"))
|
|
180
|
+
|
|
181
|
+
# Check execute permission
|
|
182
|
+
if not can_execute_report(report, current_user.tipo):
|
|
183
|
+
flash(_("No tiene permisos para ejecutar este reporte."), "danger")
|
|
184
|
+
return redirect(url_for("report.index"))
|
|
185
|
+
|
|
186
|
+
# Get metadata for system reports
|
|
187
|
+
metadata = None
|
|
188
|
+
if report.type == ReportType.SYSTEM:
|
|
189
|
+
metadata = get_system_report_metadata(report.system_report_id)
|
|
190
|
+
|
|
191
|
+
return render_template("modules/report/execute.html", report=report, metadata=metadata)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@report_bp.route("/<report_id>/run", methods=["POST"])
|
|
195
|
+
@require_read_access()
|
|
196
|
+
def run_report(report_id: str):
|
|
197
|
+
"""Execute report and show results.
|
|
198
|
+
|
|
199
|
+
Processes parameters and executes report.
|
|
200
|
+
"""
|
|
201
|
+
report = db.session.get(Report, report_id)
|
|
202
|
+
if not report:
|
|
203
|
+
return jsonify({"error": "Report not found"}), 404
|
|
204
|
+
|
|
205
|
+
# Check permissions
|
|
206
|
+
if report.status != ReportStatus.ENABLED and current_user.tipo != TipoUsuario.ADMIN:
|
|
207
|
+
return jsonify({"error": "Report is disabled"}), 403
|
|
208
|
+
|
|
209
|
+
if not can_execute_report(report, current_user.tipo):
|
|
210
|
+
return jsonify({"error": "Permission denied"}), 403
|
|
211
|
+
|
|
212
|
+
# Get parameters from request
|
|
213
|
+
parameters = request.get_json() or {}
|
|
214
|
+
|
|
215
|
+
# Get pagination parameters
|
|
216
|
+
page = parameters.pop("page", 1)
|
|
217
|
+
per_page = parameters.pop("per_page", 100)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
# Execute report
|
|
221
|
+
manager = ReportExecutionManager(report, current_user.usuario)
|
|
222
|
+
results, total_count, execution = manager.execute(parameters, page, per_page)
|
|
223
|
+
|
|
224
|
+
return jsonify(
|
|
225
|
+
{
|
|
226
|
+
"success": True,
|
|
227
|
+
"results": results,
|
|
228
|
+
"total_count": total_count,
|
|
229
|
+
"execution_id": execution.id,
|
|
230
|
+
"execution_time_ms": execution.execution_time_ms,
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
log.error(f"Error executing report: {e}")
|
|
236
|
+
return jsonify({"error": str(e)}), 500
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================================
|
|
240
|
+
# Report Export
|
|
241
|
+
# ============================================================================
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@report_bp.route("/<report_id>/export/<format>", methods=["POST"])
|
|
245
|
+
@require_read_access()
|
|
246
|
+
def export_report(report_id: str, format: str):
|
|
247
|
+
"""Export report to specified format.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
report_id: Report ID
|
|
251
|
+
format: Export format (excel, csv)
|
|
252
|
+
"""
|
|
253
|
+
report = db.session.get(Report, report_id)
|
|
254
|
+
if not report:
|
|
255
|
+
return jsonify({"error": "Report not found"}), 404
|
|
256
|
+
|
|
257
|
+
# Check export permission
|
|
258
|
+
if not can_export_report(report, current_user.tipo):
|
|
259
|
+
flash(_("No tiene permisos para exportar este reporte."), "danger")
|
|
260
|
+
return redirect(url_for("report.execute_form", report_id=report_id))
|
|
261
|
+
|
|
262
|
+
# Get parameters from request
|
|
263
|
+
parameters = request.get_json() or {}
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Execute report (get all results, no pagination for export)
|
|
267
|
+
manager = ReportExecutionManager(report, current_user.usuario)
|
|
268
|
+
results, total_count, execution = manager.execute(parameters, page=1, per_page=50000)
|
|
269
|
+
|
|
270
|
+
# Export based on format
|
|
271
|
+
if format == "excel":
|
|
272
|
+
file_path = export_report_to_excel(report.name, results)
|
|
273
|
+
elif format == "csv":
|
|
274
|
+
file_path = export_report_to_csv(report.name, results)
|
|
275
|
+
else:
|
|
276
|
+
return jsonify({"error": "Invalid format"}), 400
|
|
277
|
+
|
|
278
|
+
# Update execution record with export info
|
|
279
|
+
execution.export_file_path = file_path
|
|
280
|
+
execution.export_format = format
|
|
281
|
+
db.session.commit()
|
|
282
|
+
|
|
283
|
+
# Send file
|
|
284
|
+
return send_file(file_path, as_attachment=True)
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
log.error(f"Error exporting report: {e}")
|
|
288
|
+
return jsonify({"error": str(e)}), 500
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ============================================================================
|
|
292
|
+
# Report Administration (Admin Only)
|
|
293
|
+
# ============================================================================
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@report_bp.route("/<report_id>/toggle-status", methods=["POST"])
|
|
297
|
+
@require_role(TipoUsuario.ADMIN)
|
|
298
|
+
def toggle_status(report_id: str):
|
|
299
|
+
"""Toggle report enabled/disabled status.
|
|
300
|
+
|
|
301
|
+
Only accessible to administrators.
|
|
302
|
+
"""
|
|
303
|
+
report = db.session.get(Report, report_id)
|
|
304
|
+
if not report:
|
|
305
|
+
flash(_("Reporte no encontrado."), "danger")
|
|
306
|
+
return redirect(url_for("report.admin_index"))
|
|
307
|
+
|
|
308
|
+
# Toggle status
|
|
309
|
+
old_status = report.status
|
|
310
|
+
report.status = ReportStatus.DISABLED if report.status == ReportStatus.ENABLED else ReportStatus.ENABLED
|
|
311
|
+
|
|
312
|
+
# Create audit entry
|
|
313
|
+
audit = ReportAudit(
|
|
314
|
+
report_id=report.id,
|
|
315
|
+
action="status_changed",
|
|
316
|
+
performed_by=current_user.usuario,
|
|
317
|
+
changes={"old_status": old_status, "new_status": report.status},
|
|
318
|
+
)
|
|
319
|
+
db.session.add(audit)
|
|
320
|
+
db.session.commit()
|
|
321
|
+
|
|
322
|
+
flash(_("Estado del reporte actualizado."), "success")
|
|
323
|
+
return redirect(url_for("report.admin_index"))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@report_bp.route("/<report_id>/permissions")
|
|
327
|
+
@require_role(TipoUsuario.ADMIN)
|
|
328
|
+
def permissions_form(report_id: str):
|
|
329
|
+
"""Show report permissions form.
|
|
330
|
+
|
|
331
|
+
Allows administrators to configure role-based permissions.
|
|
332
|
+
"""
|
|
333
|
+
report = db.session.get(Report, report_id)
|
|
334
|
+
if not report:
|
|
335
|
+
flash(_("Reporte no encontrado."), "danger")
|
|
336
|
+
return redirect(url_for("report.admin_index"))
|
|
337
|
+
|
|
338
|
+
# Get existing permissions
|
|
339
|
+
existing_permissions = {perm.role: perm for perm in report.permissions}
|
|
340
|
+
|
|
341
|
+
return render_template(
|
|
342
|
+
"modules/report/permissions.html", report=report, existing_permissions=existing_permissions, roles=TipoUsuario
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@report_bp.route("/<report_id>/permissions", methods=["POST"])
|
|
347
|
+
@require_role(TipoUsuario.ADMIN)
|
|
348
|
+
def update_permissions(report_id: str):
|
|
349
|
+
"""Update report permissions.
|
|
350
|
+
|
|
351
|
+
Processes form and updates role-based permissions.
|
|
352
|
+
"""
|
|
353
|
+
report = db.session.get(Report, report_id)
|
|
354
|
+
if not report:
|
|
355
|
+
flash(_("Reporte no encontrado."), "danger")
|
|
356
|
+
return redirect(url_for("report.admin_index"))
|
|
357
|
+
|
|
358
|
+
# Process permissions for each role
|
|
359
|
+
for role in [TipoUsuario.ADMIN, TipoUsuario.HHRR, TipoUsuario.AUDIT]:
|
|
360
|
+
can_view = request.form.get(f"{role}_can_view") == "on"
|
|
361
|
+
can_execute = request.form.get(f"{role}_can_execute") == "on"
|
|
362
|
+
can_export = request.form.get(f"{role}_can_export") == "on"
|
|
363
|
+
|
|
364
|
+
# Get or create permission record
|
|
365
|
+
perm = db.session.execute(db.select(ReportRole).filter_by(report_id=report.id, role=role)).scalar_one_or_none()
|
|
366
|
+
|
|
367
|
+
if perm:
|
|
368
|
+
# Update existing
|
|
369
|
+
perm.can_view = can_view
|
|
370
|
+
perm.can_execute = can_execute
|
|
371
|
+
perm.can_export = can_export
|
|
372
|
+
else:
|
|
373
|
+
# Create new
|
|
374
|
+
perm = ReportRole(
|
|
375
|
+
report_id=report.id, role=role, can_view=can_view, can_execute=can_execute, can_export=can_export
|
|
376
|
+
)
|
|
377
|
+
db.session.add(perm)
|
|
378
|
+
|
|
379
|
+
# Create audit entry
|
|
380
|
+
audit = ReportAudit(
|
|
381
|
+
report_id=report.id,
|
|
382
|
+
action="permissions_updated",
|
|
383
|
+
performed_by=current_user.usuario,
|
|
384
|
+
changes={"timestamp": datetime.now().isoformat()},
|
|
385
|
+
)
|
|
386
|
+
db.session.add(audit)
|
|
387
|
+
db.session.commit()
|
|
388
|
+
|
|
389
|
+
flash(_("Permisos actualizados correctamente."), "success")
|
|
390
|
+
return redirect(url_for("report.admin_index"))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ============================================================================
|
|
394
|
+
# Report Detail
|
|
395
|
+
# ============================================================================
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@report_bp.route("/<report_id>")
|
|
399
|
+
@require_read_access()
|
|
400
|
+
def detail(report_id: str):
|
|
401
|
+
"""Show report details.
|
|
402
|
+
|
|
403
|
+
Displays report information, definition, and execution history.
|
|
404
|
+
"""
|
|
405
|
+
report = db.session.get(Report, report_id)
|
|
406
|
+
if not report:
|
|
407
|
+
flash(_("Reporte no encontrado."), "danger")
|
|
408
|
+
return redirect(url_for("report.index"))
|
|
409
|
+
|
|
410
|
+
# Check view permission
|
|
411
|
+
if not can_view_report(report, current_user.tipo):
|
|
412
|
+
flash(_("No tiene permisos para ver este reporte."), "danger")
|
|
413
|
+
return redirect(url_for("report.index"))
|
|
414
|
+
|
|
415
|
+
# Get recent executions
|
|
416
|
+
executions = (
|
|
417
|
+
db.session.execute(
|
|
418
|
+
db.select(ReportExecution)
|
|
419
|
+
.filter_by(report_id=report.id)
|
|
420
|
+
.order_by(ReportExecution.timestamp.desc())
|
|
421
|
+
.limit(10)
|
|
422
|
+
)
|
|
423
|
+
.scalars()
|
|
424
|
+
.all()
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Get metadata for system reports
|
|
428
|
+
metadata = None
|
|
429
|
+
if report.type == ReportType.SYSTEM:
|
|
430
|
+
metadata = get_system_report_metadata(report.system_report_id)
|
|
431
|
+
|
|
432
|
+
return render_template("modules/report/detail.html", report=report, executions=executions, metadata=metadata)
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
"""Settings page to consolidate administrative options."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from flask import Blueprint, render_template
|
|
19
|
+
|
|
20
|
+
from coati_payroll.rbac import require_write_access
|
|
21
|
+
|
|
22
|
+
settings_bp = Blueprint("settings", __name__, url_prefix="/settings")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@settings_bp.route("/")
|
|
26
|
+
@require_write_access()
|
|
27
|
+
def index():
|
|
28
|
+
"""Display settings page with links to all configuration options."""
|
|
29
|
+
return render_template("modules/settings/index.html")
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
"""Payroll Type (TipoPlanilla) CRUD routes."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|
19
|
+
from flask_login import current_user
|
|
20
|
+
|
|
21
|
+
from coati_payroll.forms import TipoPlanillaForm
|
|
22
|
+
from coati_payroll.i18n import _
|
|
23
|
+
from coati_payroll.rbac import require_read_access, require_write_access
|
|
24
|
+
from coati_payroll.model import TipoPlanilla, db
|
|
25
|
+
from coati_payroll.vistas.constants import PER_PAGE
|
|
26
|
+
|
|
27
|
+
tipo_planilla_bp = Blueprint("tipo_planilla", __name__, url_prefix="/tipo-planilla")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tipo_planilla_bp.route("/")
|
|
31
|
+
@require_read_access()
|
|
32
|
+
def index():
|
|
33
|
+
"""List all payroll types with pagination."""
|
|
34
|
+
page = request.args.get("page", 1, type=int)
|
|
35
|
+
pagination = db.paginate(
|
|
36
|
+
db.select(TipoPlanilla).order_by(TipoPlanilla.codigo),
|
|
37
|
+
page=page,
|
|
38
|
+
per_page=PER_PAGE,
|
|
39
|
+
error_out=False,
|
|
40
|
+
)
|
|
41
|
+
return render_template(
|
|
42
|
+
"modules/tipo_planilla/index.html",
|
|
43
|
+
tipos_planilla=pagination.items,
|
|
44
|
+
pagination=pagination,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@tipo_planilla_bp.route("/new", methods=["GET", "POST"])
|
|
49
|
+
@require_write_access()
|
|
50
|
+
def new():
|
|
51
|
+
"""Create a new payroll type."""
|
|
52
|
+
form = TipoPlanillaForm()
|
|
53
|
+
|
|
54
|
+
if form.validate_on_submit():
|
|
55
|
+
tipo_planilla = TipoPlanilla()
|
|
56
|
+
tipo_planilla.codigo = form.codigo.data
|
|
57
|
+
tipo_planilla.descripcion = form.descripcion.data
|
|
58
|
+
tipo_planilla.periodicidad = form.periodicidad.data
|
|
59
|
+
tipo_planilla.dias = form.dias.data
|
|
60
|
+
tipo_planilla.mes_inicio_fiscal = form.mes_inicio_fiscal.data
|
|
61
|
+
tipo_planilla.dia_inicio_fiscal = form.dia_inicio_fiscal.data
|
|
62
|
+
tipo_planilla.acumula_anual = form.acumula_anual.data
|
|
63
|
+
tipo_planilla.periodos_por_anio = form.periodos_por_anio.data
|
|
64
|
+
tipo_planilla.activo = form.activo.data
|
|
65
|
+
tipo_planilla.creado_por = current_user.usuario
|
|
66
|
+
|
|
67
|
+
db.session.add(tipo_planilla)
|
|
68
|
+
db.session.commit()
|
|
69
|
+
flash(_("Tipo de planilla creado exitosamente."), "success")
|
|
70
|
+
return redirect(url_for("tipo_planilla.index"))
|
|
71
|
+
|
|
72
|
+
return render_template(
|
|
73
|
+
"modules/tipo_planilla/form.html",
|
|
74
|
+
form=form,
|
|
75
|
+
title=_("Nuevo Tipo de Planilla"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@tipo_planilla_bp.route("/edit/<string:id>", methods=["GET", "POST"])
|
|
80
|
+
@require_write_access()
|
|
81
|
+
def edit(id: str):
|
|
82
|
+
"""Edit an existing payroll type."""
|
|
83
|
+
tipo_planilla = db.session.get(TipoPlanilla, id)
|
|
84
|
+
if not tipo_planilla:
|
|
85
|
+
flash(_("Tipo de planilla no encontrado."), "error")
|
|
86
|
+
return redirect(url_for("tipo_planilla.index"))
|
|
87
|
+
|
|
88
|
+
form = TipoPlanillaForm(obj=tipo_planilla)
|
|
89
|
+
|
|
90
|
+
if form.validate_on_submit():
|
|
91
|
+
tipo_planilla.codigo = form.codigo.data
|
|
92
|
+
tipo_planilla.descripcion = form.descripcion.data
|
|
93
|
+
tipo_planilla.periodicidad = form.periodicidad.data
|
|
94
|
+
tipo_planilla.dias = form.dias.data
|
|
95
|
+
tipo_planilla.mes_inicio_fiscal = form.mes_inicio_fiscal.data
|
|
96
|
+
tipo_planilla.dia_inicio_fiscal = form.dia_inicio_fiscal.data
|
|
97
|
+
tipo_planilla.acumula_anual = form.acumula_anual.data
|
|
98
|
+
tipo_planilla.periodos_por_anio = form.periodos_por_anio.data
|
|
99
|
+
tipo_planilla.activo = form.activo.data
|
|
100
|
+
tipo_planilla.modificado_por = current_user.usuario
|
|
101
|
+
|
|
102
|
+
db.session.commit()
|
|
103
|
+
flash(_("Tipo de planilla actualizado exitosamente."), "success")
|
|
104
|
+
return redirect(url_for("tipo_planilla.index"))
|
|
105
|
+
|
|
106
|
+
return render_template(
|
|
107
|
+
"modules/tipo_planilla/form.html",
|
|
108
|
+
form=form,
|
|
109
|
+
title=_("Editar Tipo de Planilla"),
|
|
110
|
+
tipo_planilla=tipo_planilla,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@tipo_planilla_bp.route("/delete/<string:id>", methods=["POST"])
|
|
115
|
+
@require_write_access()
|
|
116
|
+
def delete(id: str):
|
|
117
|
+
"""Delete a payroll type."""
|
|
118
|
+
tipo_planilla = db.session.get(TipoPlanilla, id)
|
|
119
|
+
if not tipo_planilla:
|
|
120
|
+
flash(_("Tipo de planilla no encontrado."), "error")
|
|
121
|
+
return redirect(url_for("tipo_planilla.index"))
|
|
122
|
+
|
|
123
|
+
# Check if this type is used by any planilla
|
|
124
|
+
if tipo_planilla.planillas:
|
|
125
|
+
flash(
|
|
126
|
+
_("No se puede eliminar un tipo de planilla que está siendo usado."),
|
|
127
|
+
"error",
|
|
128
|
+
)
|
|
129
|
+
return redirect(url_for("tipo_planilla.index"))
|
|
130
|
+
|
|
131
|
+
db.session.delete(tipo_planilla)
|
|
132
|
+
db.session.commit()
|
|
133
|
+
flash(_("Tipo de planilla eliminado exitosamente."), "success")
|
|
134
|
+
return redirect(url_for("tipo_planilla.index"))
|