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,1159 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% from 'macros.html' import render_messages %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
<div class="container-fluid">
|
|
6
|
+
{{ render_messages() }}
|
|
7
|
+
|
|
8
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
9
|
+
<div>
|
|
10
|
+
<h2>
|
|
11
|
+
<i class="bi bi-code-square me-2"></i>
|
|
12
|
+
{{ _('Editor de Esquema') }}: {{ rule.nombre }}
|
|
13
|
+
</h2>
|
|
14
|
+
<small class="text-muted">
|
|
15
|
+
{{ rule.codigo }} v{{ rule.version }} | {{ rule.jurisdiccion or _('Sin jurisdicción') }}
|
|
16
|
+
</small>
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<a href="{{ url_for('calculation_rule.edit', id=rule.id) }}" class="btn btn-secondary">
|
|
20
|
+
<i class="bi bi-pencil me-1"></i>{{ _('Editar Metadatos') }}
|
|
21
|
+
</a>
|
|
22
|
+
<a href="{{ url_for('calculation_rule.index') }}" class="btn btn-outline-secondary">
|
|
23
|
+
<i class="bi bi-arrow-left me-1"></i>{{ _('Volver') }}
|
|
24
|
+
</a>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="row">
|
|
29
|
+
<!-- Left Panel: Visual Editor -->
|
|
30
|
+
<div class="col-lg-7">
|
|
31
|
+
<div class="card mb-4">
|
|
32
|
+
<div class="card-header bg-primary text-white">
|
|
33
|
+
<i class="bi bi-ui-checks me-2"></i>{{ _('Editor Visual') }}
|
|
34
|
+
</div>
|
|
35
|
+
<div class="card-body">
|
|
36
|
+
<!-- Meta Section -->
|
|
37
|
+
<div class="schema-section mb-4">
|
|
38
|
+
<h5 class="section-title">
|
|
39
|
+
<i class="bi bi-info-circle me-2"></i>{{ _('Configuración') }}
|
|
40
|
+
</h5>
|
|
41
|
+
<div class="row">
|
|
42
|
+
<div class="col-md-6">
|
|
43
|
+
<div class="mb-3">
|
|
44
|
+
<label class="form-label">{{ _('Nombre de la regla') }}</label>
|
|
45
|
+
<input type="text" class="form-control" id="meta-name"
|
|
46
|
+
placeholder="{{ _('Ej: Income Tax Rule') }}">
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="col-md-6">
|
|
50
|
+
<div class="mb-3">
|
|
51
|
+
<label class="form-label">{{ _('Moneda de Referencia') }}</label>
|
|
52
|
+
<input type="text" class="form-control" id="meta-reference-currency"
|
|
53
|
+
placeholder="{{ _('Ej: NIO, USD') }}">
|
|
54
|
+
<div class="form-text">
|
|
55
|
+
{{ _('Moneda base para los cálculos. La moneda de la planilla se define en el Tipo de Planilla.') }}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="mb-3">
|
|
61
|
+
<label class="form-label">{{ _('Descripción') }}</label>
|
|
62
|
+
<textarea class="form-control" id="meta-description" rows="2"
|
|
63
|
+
placeholder="{{ _('Descripción de lo que calcula esta regla') }}"></textarea>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Inputs Section -->
|
|
68
|
+
<div class="schema-section mb-4">
|
|
69
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
70
|
+
<h5 class="section-title mb-0">
|
|
71
|
+
<i class="bi bi-box-arrow-in-right me-2"></i>{{ _('Variables de Entrada') }}
|
|
72
|
+
</h5>
|
|
73
|
+
<button type="button" class="btn btn-sm btn-primary" onclick="addInput()">
|
|
74
|
+
<i class="bi bi-plus-lg me-1"></i>{{ _('Agregar') }}
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="inputs-container">
|
|
78
|
+
<!-- Dynamic inputs will be added here -->
|
|
79
|
+
</div>
|
|
80
|
+
<div class="text-muted small mt-2">
|
|
81
|
+
<i class="bi bi-info-circle me-1"></i>
|
|
82
|
+
{{ _('Las variables de entrada pueden venir de la base de datos (empleado.salario_base) o ser valores estáticos.') }}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Steps Section -->
|
|
87
|
+
<div class="schema-section mb-4">
|
|
88
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
89
|
+
<h5 class="section-title mb-0">
|
|
90
|
+
<i class="bi bi-list-ol me-2"></i>{{ _('Pasos de Cálculo') }}
|
|
91
|
+
</h5>
|
|
92
|
+
<div class="btn-group btn-group-sm">
|
|
93
|
+
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown">
|
|
94
|
+
<i class="bi bi-plus-lg me-1"></i>{{ _('Agregar Paso') }}
|
|
95
|
+
</button>
|
|
96
|
+
<ul class="dropdown-menu">
|
|
97
|
+
<li><a class="dropdown-item" href="#" onclick="addStep('calculation')">
|
|
98
|
+
<i class="bi bi-calculator me-2"></i>{{ _('Cálculo') }}
|
|
99
|
+
</a></li>
|
|
100
|
+
<li><a class="dropdown-item" href="#" onclick="addStep('conditional')">
|
|
101
|
+
<i class="bi bi-signpost-split me-2"></i>{{ _('Condicional') }}
|
|
102
|
+
</a></li>
|
|
103
|
+
<li><a class="dropdown-item" href="#" onclick="addStep('tax_lookup')">
|
|
104
|
+
<i class="bi bi-table me-2"></i>{{ _('Búsqueda en Tabla') }}
|
|
105
|
+
</a></li>
|
|
106
|
+
<li><a class="dropdown-item" href="#" onclick="addStep('assignment')">
|
|
107
|
+
<i class="bi bi-arrow-right-circle me-2"></i>{{ _('Asignación') }}
|
|
108
|
+
</a></li>
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div id="steps-container">
|
|
113
|
+
<!-- Dynamic steps will be added here -->
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Tax Tables Section -->
|
|
118
|
+
<div class="schema-section mb-4">
|
|
119
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
120
|
+
<h5 class="section-title mb-0">
|
|
121
|
+
<i class="bi bi-table me-2"></i>{{ _('Tablas de Impuestos') }}
|
|
122
|
+
</h5>
|
|
123
|
+
<button type="button" class="btn btn-sm btn-primary" onclick="addTaxTable()">
|
|
124
|
+
<i class="bi bi-plus-lg me-1"></i>{{ _('Agregar Tabla') }}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="tax-tables-container">
|
|
128
|
+
<!-- Dynamic tax tables will be added here -->
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Output Section -->
|
|
133
|
+
<div class="schema-section">
|
|
134
|
+
<h5 class="section-title">
|
|
135
|
+
<i class="bi bi-box-arrow-right me-2"></i>{{ _('Resultado Final') }}
|
|
136
|
+
</h5>
|
|
137
|
+
<div class="mb-3">
|
|
138
|
+
<label class="form-label">{{ _('Variable de salida') }}</label>
|
|
139
|
+
<select class="form-select" id="output-variable">
|
|
140
|
+
<option value="">{{ _('Seleccionar variable de resultado...') }}</option>
|
|
141
|
+
</select>
|
|
142
|
+
<div class="form-text">
|
|
143
|
+
{{ _('Seleccione la variable que contiene el resultado final del cálculo.') }}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Right Panel: JSON Preview & Testing -->
|
|
152
|
+
<div class="col-lg-5">
|
|
153
|
+
<!-- JSON Preview -->
|
|
154
|
+
<div class="card mb-4">
|
|
155
|
+
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
|
156
|
+
<span><i class="bi bi-code-slash me-2"></i>{{ _('Vista Previa JSON') }}</span>
|
|
157
|
+
<div class="btn-group btn-group-sm">
|
|
158
|
+
<button type="button" class="btn btn-outline-light" onclick="copyJson()">
|
|
159
|
+
<i class="bi bi-clipboard"></i>
|
|
160
|
+
</button>
|
|
161
|
+
<button type="button" class="btn btn-outline-light" onclick="formatJson()">
|
|
162
|
+
<i class="bi bi-magic"></i>
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="card-body p-0">
|
|
167
|
+
<textarea id="json-preview" class="form-control font-monospace" rows="15"
|
|
168
|
+
style="border: none; border-radius: 0; resize: vertical;"></textarea>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Test Panel -->
|
|
173
|
+
<div class="card mb-4">
|
|
174
|
+
<div class="card-header bg-info text-white">
|
|
175
|
+
<i class="bi bi-play-circle me-2"></i>{{ _('Probar Cálculo') }}
|
|
176
|
+
</div>
|
|
177
|
+
<div class="card-body">
|
|
178
|
+
<div id="test-inputs-container">
|
|
179
|
+
<!-- Dynamic test inputs will be generated -->
|
|
180
|
+
</div>
|
|
181
|
+
<button type="button" class="btn btn-info w-100 mt-3" onclick="testCalculation()">
|
|
182
|
+
<i class="bi bi-play-fill me-1"></i>{{ _('Ejecutar Prueba') }}
|
|
183
|
+
</button>
|
|
184
|
+
<div id="test-result" class="mt-3" style="display: none;">
|
|
185
|
+
<h6>{{ _('Resultado:') }}</h6>
|
|
186
|
+
<pre class="bg-light p-3 rounded" id="test-result-content"></pre>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Actions -->
|
|
192
|
+
<div class="card">
|
|
193
|
+
<div class="card-body">
|
|
194
|
+
<div class="d-grid gap-2">
|
|
195
|
+
<button type="button" class="btn btn-success btn-lg" onclick="saveSchema()">
|
|
196
|
+
<i class="bi bi-check-lg me-2"></i>{{ _('Guardar Esquema') }}
|
|
197
|
+
</button>
|
|
198
|
+
<button type="button" class="btn btn-outline-primary" onclick="document.getElementById('json-file-input').click()">
|
|
199
|
+
<i class="bi bi-upload me-2"></i>{{ _('Cargar desde Archivo JSON') }}
|
|
200
|
+
</button>
|
|
201
|
+
<input type="file" id="json-file-input" accept=".json" style="display: none;" onchange="loadJsonFile(event)">
|
|
202
|
+
<button type="button" class="btn btn-outline-secondary" onclick="loadExample()">
|
|
203
|
+
<i class="bi bi-file-earmark-code me-2"></i>{{ _('Cargar Ejemplo de Impuesto Progresivo') }}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<style>
|
|
213
|
+
.schema-section {
|
|
214
|
+
background: #f8f9fa;
|
|
215
|
+
border-radius: 8px;
|
|
216
|
+
padding: 1.25rem;
|
|
217
|
+
border: 1px solid #e9ecef;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.section-title {
|
|
221
|
+
color: #495057;
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
margin-bottom: 1rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.input-item, .step-item, .tax-table-item {
|
|
227
|
+
background: white;
|
|
228
|
+
border: 1px solid #dee2e6;
|
|
229
|
+
border-radius: 6px;
|
|
230
|
+
padding: 1rem;
|
|
231
|
+
margin-bottom: 0.75rem;
|
|
232
|
+
position: relative;
|
|
233
|
+
cursor: move;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.input-item .drag-handle, .step-item .drag-handle {
|
|
237
|
+
position: absolute;
|
|
238
|
+
left: 0.5rem;
|
|
239
|
+
top: 50%;
|
|
240
|
+
transform: translateY(-50%);
|
|
241
|
+
color: #6c757d;
|
|
242
|
+
cursor: grab;
|
|
243
|
+
font-size: 1.2rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.input-item .drag-handle:active, .step-item .drag-handle:active {
|
|
247
|
+
cursor: grabbing;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.reorder-buttons {
|
|
251
|
+
position: absolute;
|
|
252
|
+
right: 3rem;
|
|
253
|
+
top: 0.5rem;
|
|
254
|
+
display: flex;
|
|
255
|
+
gap: 0.25rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.reorder-buttons .btn {
|
|
259
|
+
padding: 0.125rem 0.375rem;
|
|
260
|
+
font-size: 0.75rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.input-item:hover, .step-item:hover, .tax-table-item:hover {
|
|
264
|
+
border-color: #0d6efd;
|
|
265
|
+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.remove-btn {
|
|
269
|
+
position: absolute;
|
|
270
|
+
top: 0.5rem;
|
|
271
|
+
right: 0.5rem;
|
|
272
|
+
padding: 0.25rem 0.5rem;
|
|
273
|
+
font-size: 0.875rem;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.step-type-badge {
|
|
277
|
+
font-size: 0.75rem;
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
text-transform: uppercase;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.bracket-row {
|
|
283
|
+
background: #f8f9fa;
|
|
284
|
+
border-radius: 4px;
|
|
285
|
+
padding: 0.75rem;
|
|
286
|
+
margin-bottom: 0.5rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#json-preview {
|
|
290
|
+
font-size: 0.85rem;
|
|
291
|
+
background: #1e1e1e;
|
|
292
|
+
color: #d4d4d4;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.sortable-ghost {
|
|
296
|
+
opacity: 0.4;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.sortable-drag {
|
|
300
|
+
opacity: 0.8;
|
|
301
|
+
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
302
|
+
}
|
|
303
|
+
</style>
|
|
304
|
+
|
|
305
|
+
<script>
|
|
306
|
+
// Initialize with existing schema
|
|
307
|
+
let schema = {{ schema_json|safe }} || {
|
|
308
|
+
meta: {},
|
|
309
|
+
inputs: [],
|
|
310
|
+
steps: [],
|
|
311
|
+
tax_tables: {},
|
|
312
|
+
output: ''
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const exampleSchema = {{ example_schema|safe }};
|
|
316
|
+
|
|
317
|
+
// Available data sources from the database
|
|
318
|
+
const availableSources = {{ available_sources|safe }};
|
|
319
|
+
|
|
320
|
+
// Counter for unique IDs
|
|
321
|
+
let inputCounter = 0;
|
|
322
|
+
let stepCounter = 0;
|
|
323
|
+
let tableCounter = 0;
|
|
324
|
+
|
|
325
|
+
// Sortable instances
|
|
326
|
+
let inputsSortable = null;
|
|
327
|
+
let stepsSortable = null;
|
|
328
|
+
|
|
329
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
330
|
+
loadSchemaToEditor();
|
|
331
|
+
updateJsonPreview();
|
|
332
|
+
initializeSortable();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
function initializeSortable() {
|
|
336
|
+
// Initialize sortable for inputs
|
|
337
|
+
const inputsContainer = document.getElementById('inputs-container');
|
|
338
|
+
if (inputsContainer && typeof Sortable !== 'undefined') {
|
|
339
|
+
inputsSortable = Sortable.create(inputsContainer, {
|
|
340
|
+
animation: 150,
|
|
341
|
+
handle: '.drag-handle',
|
|
342
|
+
ghostClass: 'sortable-ghost',
|
|
343
|
+
dragClass: 'sortable-drag',
|
|
344
|
+
onEnd: function() {
|
|
345
|
+
updateJsonPreview();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Initialize sortable for steps
|
|
351
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
352
|
+
if (stepsContainer && typeof Sortable !== 'undefined') {
|
|
353
|
+
stepsSortable = Sortable.create(stepsContainer, {
|
|
354
|
+
animation: 150,
|
|
355
|
+
handle: '.drag-handle',
|
|
356
|
+
ghostClass: 'sortable-ghost',
|
|
357
|
+
dragClass: 'sortable-drag',
|
|
358
|
+
onEnd: function() {
|
|
359
|
+
updateJsonPreview();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function loadSchemaToEditor() {
|
|
366
|
+
// Load meta
|
|
367
|
+
if (schema.meta) {
|
|
368
|
+
document.getElementById('meta-name').value = schema.meta.name || '';
|
|
369
|
+
document.getElementById('meta-reference-currency').value = schema.meta.reference_currency || schema.meta.currency || '';
|
|
370
|
+
document.getElementById('meta-description').value = schema.meta.description || '';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Load inputs
|
|
374
|
+
const inputsContainer = document.getElementById('inputs-container');
|
|
375
|
+
inputsContainer.innerHTML = '';
|
|
376
|
+
if (schema.inputs && schema.inputs.length > 0) {
|
|
377
|
+
schema.inputs.forEach(input => addInput(input));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Load steps
|
|
381
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
382
|
+
stepsContainer.innerHTML = '';
|
|
383
|
+
if (schema.steps && schema.steps.length > 0) {
|
|
384
|
+
schema.steps.forEach(step => addStep(step.type, step));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Load tax tables
|
|
388
|
+
const tablesContainer = document.getElementById('tax-tables-container');
|
|
389
|
+
tablesContainer.innerHTML = '';
|
|
390
|
+
if (schema.tax_tables) {
|
|
391
|
+
Object.entries(schema.tax_tables).forEach(([name, brackets]) => {
|
|
392
|
+
addTaxTable(name, brackets);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Load output
|
|
397
|
+
updateOutputSelect();
|
|
398
|
+
document.getElementById('output-variable').value = schema.output || '';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function addInput(data = null) {
|
|
402
|
+
const container = document.getElementById('inputs-container');
|
|
403
|
+
const id = inputCounter++;
|
|
404
|
+
|
|
405
|
+
// Build source options grouped by category
|
|
406
|
+
let sourceOptionsHtml = '<option value="">{{ _("Seleccionar origen o escribir valor...") }}</option>';
|
|
407
|
+
let currentCategory = '';
|
|
408
|
+
availableSources.forEach(source => {
|
|
409
|
+
if (source.category !== currentCategory) {
|
|
410
|
+
if (currentCategory !== '') {
|
|
411
|
+
sourceOptionsHtml += '</optgroup>';
|
|
412
|
+
}
|
|
413
|
+
sourceOptionsHtml += `<optgroup label="${source.category}">`;
|
|
414
|
+
currentCategory = source.category;
|
|
415
|
+
}
|
|
416
|
+
sourceOptionsHtml += `<option value="${source.value}" title="${source.description}">${source.label}</option>`;
|
|
417
|
+
});
|
|
418
|
+
if (currentCategory !== '') {
|
|
419
|
+
sourceOptionsHtml += '</optgroup>';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const currentSourceValue = data?.source || data?.default || '';
|
|
423
|
+
|
|
424
|
+
const html = `
|
|
425
|
+
<div class="input-item" id="input-${id}">
|
|
426
|
+
<div class="drag-handle">
|
|
427
|
+
<i class="bi bi-grip-vertical"></i>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="reorder-buttons">
|
|
430
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputUp(${id})" title="{{ _('Subir') }}">
|
|
431
|
+
<i class="bi bi-arrow-up"></i>
|
|
432
|
+
</button>
|
|
433
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputDown(${id})" title="{{ _('Bajar') }}">
|
|
434
|
+
<i class="bi bi-arrow-down"></i>
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeInput(${id})">
|
|
438
|
+
<i class="bi bi-x"></i>
|
|
439
|
+
</button>
|
|
440
|
+
<div class="row">
|
|
441
|
+
<div class="col-md-4">
|
|
442
|
+
<label class="form-label small">{{ _('Nombre') }}</label>
|
|
443
|
+
<input type="text" class="form-control form-control-sm input-name"
|
|
444
|
+
value="${data?.name || ''}" placeholder="{{ _('Ej: salario_mensual') }}"
|
|
445
|
+
onchange="updateJsonPreview()">
|
|
446
|
+
</div>
|
|
447
|
+
<div class="col-md-3">
|
|
448
|
+
<label class="form-label small">{{ _('Tipo') }}</label>
|
|
449
|
+
<select class="form-select form-select-sm input-type" onchange="updateJsonPreview()">
|
|
450
|
+
<option value="decimal" ${data?.type === 'decimal' ? 'selected' : ''}>{{ _('Decimal') }}</option>
|
|
451
|
+
<option value="integer" ${data?.type === 'integer' ? 'selected' : ''}>{{ _('Entero') }}</option>
|
|
452
|
+
<option value="string" ${data?.type === 'string' ? 'selected' : ''}>{{ _('Texto') }}</option>
|
|
453
|
+
<option value="boolean" ${data?.type === 'boolean' ? 'selected' : ''}>{{ _('Booleano') }}</option>
|
|
454
|
+
<option value="date" ${data?.type === 'date' ? 'selected' : ''}>{{ _('Fecha') }}</option>
|
|
455
|
+
</select>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="col-md-5">
|
|
458
|
+
<label class="form-label small">{{ _('Origen de Datos') }}</label>
|
|
459
|
+
<select class="form-select form-select-sm input-source-select"
|
|
460
|
+
onchange="handleSourceChange(this, ${id}); updateJsonPreview()">
|
|
461
|
+
${sourceOptionsHtml}
|
|
462
|
+
<option value="__custom__">{{ _('Valor personalizado...') }}</option>
|
|
463
|
+
</select>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="row mt-2">
|
|
467
|
+
<div class="col-md-6">
|
|
468
|
+
<input type="text" class="form-control form-control-sm input-source-custom"
|
|
469
|
+
value="${currentSourceValue}"
|
|
470
|
+
placeholder="{{ _('Origen (empleado.campo) o valor default') }}"
|
|
471
|
+
onchange="updateJsonPreview()"
|
|
472
|
+
style="${currentSourceValue && !availableSources.find(s => s.value === currentSourceValue) ? '' : 'display:none'}">
|
|
473
|
+
</div>
|
|
474
|
+
<div class="col-md-6">
|
|
475
|
+
<input type="text" class="form-control form-control-sm input-description"
|
|
476
|
+
value="${data?.description || ''}"
|
|
477
|
+
placeholder="{{ _('Descripción (opcional)') }}"
|
|
478
|
+
onchange="updateJsonPreview()">
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
`;
|
|
483
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
484
|
+
|
|
485
|
+
// Set the select value if it matches an available source
|
|
486
|
+
const selectEl = document.querySelector(`#input-${id} .input-source-select`);
|
|
487
|
+
if (currentSourceValue) {
|
|
488
|
+
const matchingSource = availableSources.find(s => s.value === currentSourceValue);
|
|
489
|
+
if (matchingSource) {
|
|
490
|
+
selectEl.value = currentSourceValue;
|
|
491
|
+
} else {
|
|
492
|
+
selectEl.value = '__custom__';
|
|
493
|
+
document.querySelector(`#input-${id} .input-source-custom`).style.display = '';
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
updateJsonPreview();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function handleSourceChange(selectEl, inputId) {
|
|
501
|
+
const customInput = document.querySelector(`#input-${inputId} .input-source-custom`);
|
|
502
|
+
if (selectEl.value === '__custom__') {
|
|
503
|
+
customInput.style.display = '';
|
|
504
|
+
customInput.focus();
|
|
505
|
+
} else if (selectEl.value === '') {
|
|
506
|
+
customInput.style.display = 'none';
|
|
507
|
+
customInput.value = '';
|
|
508
|
+
} else {
|
|
509
|
+
customInput.style.display = 'none';
|
|
510
|
+
customInput.value = selectEl.value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function removeInput(id) {
|
|
515
|
+
document.getElementById(`input-${id}`).remove();
|
|
516
|
+
updateJsonPreview();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function moveInputUp(id) {
|
|
520
|
+
const element = document.getElementById(`input-${id}`);
|
|
521
|
+
const prev = element.previousElementSibling;
|
|
522
|
+
if (prev) {
|
|
523
|
+
element.parentNode.insertBefore(element, prev);
|
|
524
|
+
updateJsonPreview();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function moveInputDown(id) {
|
|
529
|
+
const element = document.getElementById(`input-${id}`);
|
|
530
|
+
const next = element.nextElementSibling;
|
|
531
|
+
if (next) {
|
|
532
|
+
element.parentNode.insertBefore(next, element);
|
|
533
|
+
updateJsonPreview();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function addStep(type, data = null) {
|
|
538
|
+
const container = document.getElementById('steps-container');
|
|
539
|
+
const id = stepCounter++;
|
|
540
|
+
|
|
541
|
+
let typeHtml = '';
|
|
542
|
+
let typeBadgeClass = '';
|
|
543
|
+
|
|
544
|
+
switch(type) {
|
|
545
|
+
case 'calculation':
|
|
546
|
+
typeBadgeClass = 'bg-primary';
|
|
547
|
+
typeHtml = `
|
|
548
|
+
<div class="mb-2">
|
|
549
|
+
<label class="form-label small">{{ _('Fórmula') }}</label>
|
|
550
|
+
<input type="text" class="form-control form-control-sm step-formula"
|
|
551
|
+
value="${data?.formula || ''}"
|
|
552
|
+
placeholder="{{ _('Ej: salario_mensual * 12') }}"
|
|
553
|
+
onchange="updateJsonPreview()">
|
|
554
|
+
</div>
|
|
555
|
+
`;
|
|
556
|
+
break;
|
|
557
|
+
case 'conditional':
|
|
558
|
+
typeBadgeClass = 'bg-warning text-dark';
|
|
559
|
+
typeHtml = `
|
|
560
|
+
<div class="mb-2">
|
|
561
|
+
<label class="form-label small">{{ _('Condición') }}</label>
|
|
562
|
+
<div class="row g-2">
|
|
563
|
+
<div class="col-4">
|
|
564
|
+
<input type="text" class="form-control form-control-sm step-cond-left"
|
|
565
|
+
value="${data?.condition?.left || ''}" placeholder="{{ _('Variable') }}"
|
|
566
|
+
onchange="updateJsonPreview()">
|
|
567
|
+
</div>
|
|
568
|
+
<div class="col-2">
|
|
569
|
+
<select class="form-select form-select-sm step-cond-op" onchange="updateJsonPreview()">
|
|
570
|
+
<option value=">" ${ data?.condition?.operator === '>' ? 'selected' : '' }>></option>
|
|
571
|
+
<option value=">=" ${ data?.condition?.operator === '>=' ? 'selected' : '' }>>=</option>
|
|
572
|
+
<option value="<" ${ data?.condition?.operator === '<' ? 'selected' : '' }><</option>
|
|
573
|
+
<option value="<=" ${ data?.condition?.operator === '<=' ? 'selected' : '' }><=</option>
|
|
574
|
+
<option value="==" ${ data?.condition?.operator === '==' ? 'selected' : '' }>==</option>
|
|
575
|
+
<option value="!=" ${ data?.condition?.operator === '!=' ? 'selected' : '' }>!=</option>
|
|
576
|
+
</select>
|
|
577
|
+
</div>
|
|
578
|
+
<div class="col-4">
|
|
579
|
+
<input type="text" class="form-control form-control-sm step-cond-right"
|
|
580
|
+
value="${data?.condition?.right || ''}" placeholder="{{ _('Valor') }}"
|
|
581
|
+
onchange="updateJsonPreview()">
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="row g-2">
|
|
586
|
+
<div class="col-6">
|
|
587
|
+
<label class="form-label small">{{ _('Si verdadero') }}</label>
|
|
588
|
+
<input type="text" class="form-control form-control-sm step-if-true"
|
|
589
|
+
value="${data?.if_true || ''}" placeholder="{{ _('Fórmula si cumple') }}"
|
|
590
|
+
onchange="updateJsonPreview()">
|
|
591
|
+
</div>
|
|
592
|
+
<div class="col-6">
|
|
593
|
+
<label class="form-label small">{{ _('Si falso') }}</label>
|
|
594
|
+
<input type="text" class="form-control form-control-sm step-if-false"
|
|
595
|
+
value="${data?.if_false || ''}" placeholder="{{ _('Fórmula si no cumple') }}"
|
|
596
|
+
onchange="updateJsonPreview()">
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
`;
|
|
600
|
+
break;
|
|
601
|
+
case 'tax_lookup':
|
|
602
|
+
typeBadgeClass = 'bg-danger';
|
|
603
|
+
typeHtml = `
|
|
604
|
+
<div class="row g-2">
|
|
605
|
+
<div class="col-6">
|
|
606
|
+
<label class="form-label small">{{ _('Tabla de impuestos') }}</label>
|
|
607
|
+
<input type="text" class="form-control form-control-sm step-table"
|
|
608
|
+
value="${data?.table || ''}" placeholder="{{ _('Nombre de la tabla') }}"
|
|
609
|
+
onchange="updateJsonPreview()">
|
|
610
|
+
</div>
|
|
611
|
+
<div class="col-6">
|
|
612
|
+
<label class="form-label small">{{ _('Variable de entrada') }}</label>
|
|
613
|
+
<input type="text" class="form-control form-control-sm step-input"
|
|
614
|
+
value="${data?.input || ''}" placeholder="{{ _('Variable a buscar') }}"
|
|
615
|
+
onchange="updateJsonPreview()">
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
`;
|
|
619
|
+
break;
|
|
620
|
+
case 'assignment':
|
|
621
|
+
typeBadgeClass = 'bg-info';
|
|
622
|
+
typeHtml = `
|
|
623
|
+
<div class="mb-2">
|
|
624
|
+
<label class="form-label small">{{ _('Valor') }}</label>
|
|
625
|
+
<input type="text" class="form-control form-control-sm step-value"
|
|
626
|
+
value="${data?.value || ''}" placeholder="{{ _('Variable o valor a asignar') }}"
|
|
627
|
+
onchange="updateJsonPreview()">
|
|
628
|
+
</div>
|
|
629
|
+
`;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const html = `
|
|
634
|
+
<div class="step-item" id="step-${id}" data-type="${type}">
|
|
635
|
+
<div class="drag-handle">
|
|
636
|
+
<i class="bi bi-grip-vertical"></i>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="reorder-buttons">
|
|
639
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepUp(${id})" title="{{ _('Subir') }}">
|
|
640
|
+
<i class="bi bi-arrow-up"></i>
|
|
641
|
+
</button>
|
|
642
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepDown(${id})" title="{{ _('Bajar') }}">
|
|
643
|
+
<i class="bi bi-arrow-down"></i>
|
|
644
|
+
</button>
|
|
645
|
+
</div>
|
|
646
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeStep(${id})">
|
|
647
|
+
<i class="bi bi-x"></i>
|
|
648
|
+
</button>
|
|
649
|
+
<div class="d-flex align-items-center mb-2">
|
|
650
|
+
<span class="badge ${typeBadgeClass} step-type-badge me-2">${type}</span>
|
|
651
|
+
<input type="text" class="form-control form-control-sm step-name"
|
|
652
|
+
value="${data?.name || ''}" placeholder="{{ _('Nombre del paso') }}"
|
|
653
|
+
style="max-width: 200px;" onchange="updateJsonPreview()">
|
|
654
|
+
</div>
|
|
655
|
+
${typeHtml}
|
|
656
|
+
<div class="mt-2">
|
|
657
|
+
<input type="text" class="form-control form-control-sm step-description"
|
|
658
|
+
value="${data?.description || ''}" placeholder="{{ _('Descripción (opcional)') }}"
|
|
659
|
+
onchange="updateJsonPreview()">
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
`;
|
|
663
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
664
|
+
updateJsonPreview();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function removeStep(id) {
|
|
668
|
+
document.getElementById(`step-${id}`).remove();
|
|
669
|
+
updateJsonPreview();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function moveStepUp(id) {
|
|
673
|
+
const element = document.getElementById(`step-${id}`);
|
|
674
|
+
const prev = element.previousElementSibling;
|
|
675
|
+
if (prev) {
|
|
676
|
+
element.parentNode.insertBefore(element, prev);
|
|
677
|
+
updateJsonPreview();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function moveStepDown(id) {
|
|
682
|
+
const element = document.getElementById(`step-${id}`);
|
|
683
|
+
const next = element.nextElementSibling;
|
|
684
|
+
if (next) {
|
|
685
|
+
element.parentNode.insertBefore(next, element);
|
|
686
|
+
updateJsonPreview();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function addTaxTable(name = null, brackets = null) {
|
|
691
|
+
const container = document.getElementById('tax-tables-container');
|
|
692
|
+
const id = tableCounter++;
|
|
693
|
+
|
|
694
|
+
const html = `
|
|
695
|
+
<div class="tax-table-item" id="table-${id}">
|
|
696
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeTaxTable(${id})">
|
|
697
|
+
<i class="bi bi-x"></i>
|
|
698
|
+
</button>
|
|
699
|
+
<div class="mb-3">
|
|
700
|
+
<label class="form-label small">{{ _('Nombre de la tabla') }}</label>
|
|
701
|
+
<input type="text" class="form-control form-control-sm table-name"
|
|
702
|
+
value="${name || ''}" placeholder="{{ _('Ej: income_tax_brackets') }}"
|
|
703
|
+
onchange="updateJsonPreview()">
|
|
704
|
+
</div>
|
|
705
|
+
<div class="brackets-container" id="brackets-${id}">
|
|
706
|
+
<!-- Brackets will be added here -->
|
|
707
|
+
</div>
|
|
708
|
+
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addBracket(${id})">
|
|
709
|
+
<i class="bi bi-plus-lg me-1"></i>{{ _('Agregar Tramo') }}
|
|
710
|
+
</button>
|
|
711
|
+
</div>
|
|
712
|
+
`;
|
|
713
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
714
|
+
|
|
715
|
+
// Add existing brackets
|
|
716
|
+
if (brackets && brackets.length > 0) {
|
|
717
|
+
brackets.forEach(bracket => addBracket(id, bracket));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
updateJsonPreview();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function removeTaxTable(id) {
|
|
724
|
+
document.getElementById(`table-${id}`).remove();
|
|
725
|
+
updateJsonPreview();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function addBracket(tableId, data = null) {
|
|
729
|
+
const container = document.getElementById(`brackets-${tableId}`);
|
|
730
|
+
const html = `
|
|
731
|
+
<div class="bracket-row">
|
|
732
|
+
<div class="row g-2 align-items-center">
|
|
733
|
+
<div class="col">
|
|
734
|
+
<input type="number" class="form-control form-control-sm bracket-min"
|
|
735
|
+
value="${data?.min ?? ''}" placeholder="{{ _('Mín') }}"
|
|
736
|
+
onchange="updateJsonPreview()">
|
|
737
|
+
</div>
|
|
738
|
+
<div class="col">
|
|
739
|
+
<input type="number" class="form-control form-control-sm bracket-max"
|
|
740
|
+
value="${data?.max ?? ''}" placeholder="{{ _('Máx') }}"
|
|
741
|
+
onchange="updateJsonPreview()">
|
|
742
|
+
</div>
|
|
743
|
+
<div class="col">
|
|
744
|
+
<input type="number" class="form-control form-control-sm bracket-rate"
|
|
745
|
+
value="${data?.rate ?? ''}" placeholder="{{ _('Tasa') }}" step="0.01"
|
|
746
|
+
onchange="updateJsonPreview()">
|
|
747
|
+
</div>
|
|
748
|
+
<div class="col">
|
|
749
|
+
<input type="number" class="form-control form-control-sm bracket-fixed"
|
|
750
|
+
value="${data?.fixed ?? ''}" placeholder="{{ _('Fijo') }}"
|
|
751
|
+
onchange="updateJsonPreview()">
|
|
752
|
+
</div>
|
|
753
|
+
<div class="col">
|
|
754
|
+
<input type="number" class="form-control form-control-sm bracket-over"
|
|
755
|
+
value="${data?.over ?? ''}" placeholder="{{ _('Sobre') }}"
|
|
756
|
+
onchange="updateJsonPreview()">
|
|
757
|
+
</div>
|
|
758
|
+
<div class="col-auto">
|
|
759
|
+
<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.bracket-row').remove(); updateJsonPreview();">
|
|
760
|
+
<i class="bi bi-x"></i>
|
|
761
|
+
</button>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
`;
|
|
766
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
767
|
+
updateJsonPreview();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function updateOutputSelect() {
|
|
771
|
+
const select = document.getElementById('output-variable');
|
|
772
|
+
const currentValue = select.value;
|
|
773
|
+
select.innerHTML = '<option value="">{{ _("Seleccionar variable de resultado...") }}</option>';
|
|
774
|
+
|
|
775
|
+
// Add all input names
|
|
776
|
+
document.querySelectorAll('.input-name').forEach(input => {
|
|
777
|
+
if (input.value) {
|
|
778
|
+
select.innerHTML += `<option value="${input.value}">${input.value} (input)</option>`;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Add all step names
|
|
783
|
+
document.querySelectorAll('.step-name').forEach(input => {
|
|
784
|
+
if (input.value) {
|
|
785
|
+
select.innerHTML += `<option value="${input.value}">${input.value} (step)</option>`;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
select.value = currentValue;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function collectSchemaFromEditor() {
|
|
793
|
+
const newSchema = {
|
|
794
|
+
meta: {
|
|
795
|
+
name: document.getElementById('meta-name').value,
|
|
796
|
+
reference_currency: document.getElementById('meta-reference-currency').value,
|
|
797
|
+
description: document.getElementById('meta-description').value
|
|
798
|
+
},
|
|
799
|
+
inputs: [],
|
|
800
|
+
steps: [],
|
|
801
|
+
tax_tables: {},
|
|
802
|
+
output: document.getElementById('output-variable').value
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// Collect inputs
|
|
806
|
+
document.querySelectorAll('.input-item').forEach(item => {
|
|
807
|
+
const input = {
|
|
808
|
+
name: item.querySelector('.input-name').value,
|
|
809
|
+
type: item.querySelector('.input-type').value
|
|
810
|
+
};
|
|
811
|
+
// Get source from either the select dropdown or the custom input field
|
|
812
|
+
const selectEl = item.querySelector('.input-source-select');
|
|
813
|
+
const customEl = item.querySelector('.input-source-custom');
|
|
814
|
+
let source = '';
|
|
815
|
+
if (selectEl && selectEl.value && selectEl.value !== '__custom__' && selectEl.value !== '') {
|
|
816
|
+
source = selectEl.value;
|
|
817
|
+
} else if (customEl) {
|
|
818
|
+
source = customEl.value;
|
|
819
|
+
}
|
|
820
|
+
if (source) {
|
|
821
|
+
if (source.includes('.')) {
|
|
822
|
+
input.source = source;
|
|
823
|
+
} else if (!isNaN(source)) {
|
|
824
|
+
input.default = parseFloat(source);
|
|
825
|
+
} else {
|
|
826
|
+
input.default = source;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const desc = item.querySelector('.input-description').value;
|
|
830
|
+
if (desc) input.description = desc;
|
|
831
|
+
|
|
832
|
+
if (input.name) newSchema.inputs.push(input);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Collect steps
|
|
836
|
+
document.querySelectorAll('.step-item').forEach(item => {
|
|
837
|
+
const type = item.dataset.type;
|
|
838
|
+
const step = {
|
|
839
|
+
name: item.querySelector('.step-name').value,
|
|
840
|
+
type: type
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
switch(type) {
|
|
844
|
+
case 'calculation':
|
|
845
|
+
step.formula = item.querySelector('.step-formula').value;
|
|
846
|
+
break;
|
|
847
|
+
case 'conditional':
|
|
848
|
+
step.condition = {
|
|
849
|
+
left: item.querySelector('.step-cond-left').value,
|
|
850
|
+
operator: item.querySelector('.step-cond-op').value,
|
|
851
|
+
right: item.querySelector('.step-cond-right').value
|
|
852
|
+
};
|
|
853
|
+
// Try to parse right value as number
|
|
854
|
+
if (!isNaN(step.condition.right)) {
|
|
855
|
+
step.condition.right = parseFloat(step.condition.right);
|
|
856
|
+
}
|
|
857
|
+
step.if_true = item.querySelector('.step-if-true').value;
|
|
858
|
+
step.if_false = item.querySelector('.step-if-false').value;
|
|
859
|
+
break;
|
|
860
|
+
case 'tax_lookup':
|
|
861
|
+
step.table = item.querySelector('.step-table').value;
|
|
862
|
+
step.input = item.querySelector('.step-input').value;
|
|
863
|
+
break;
|
|
864
|
+
case 'assignment':
|
|
865
|
+
step.value = item.querySelector('.step-value').value;
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const desc = item.querySelector('.step-description').value;
|
|
870
|
+
if (desc) step.description = desc;
|
|
871
|
+
|
|
872
|
+
if (step.name) newSchema.steps.push(step);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// Collect tax tables
|
|
876
|
+
document.querySelectorAll('.tax-table-item').forEach(item => {
|
|
877
|
+
const tableName = item.querySelector('.table-name').value;
|
|
878
|
+
if (!tableName) return;
|
|
879
|
+
|
|
880
|
+
const brackets = [];
|
|
881
|
+
item.querySelectorAll('.bracket-row').forEach(row => {
|
|
882
|
+
const bracket = {};
|
|
883
|
+
const min = row.querySelector('.bracket-min').value;
|
|
884
|
+
const max = row.querySelector('.bracket-max').value;
|
|
885
|
+
const rate = row.querySelector('.bracket-rate').value;
|
|
886
|
+
const fixed = row.querySelector('.bracket-fixed').value;
|
|
887
|
+
const over = row.querySelector('.bracket-over').value;
|
|
888
|
+
|
|
889
|
+
if (min !== '') bracket.min = parseFloat(min);
|
|
890
|
+
if (max !== '') bracket.max = parseFloat(max);
|
|
891
|
+
else bracket.max = null;
|
|
892
|
+
if (rate !== '') bracket.rate = parseFloat(rate);
|
|
893
|
+
if (fixed !== '') bracket.fixed = parseFloat(fixed);
|
|
894
|
+
if (over !== '') bracket.over = parseFloat(over);
|
|
895
|
+
|
|
896
|
+
if (Object.keys(bracket).length > 0) brackets.push(bracket);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
if (brackets.length > 0) {
|
|
900
|
+
newSchema.tax_tables[tableName] = brackets;
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
return newSchema;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function updateJsonPreview() {
|
|
908
|
+
schema = collectSchemaFromEditor();
|
|
909
|
+
document.getElementById('json-preview').value = JSON.stringify(schema, null, 2);
|
|
910
|
+
updateOutputSelect();
|
|
911
|
+
generateTestInputs();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function generateTestInputs() {
|
|
915
|
+
const container = document.getElementById('test-inputs-container');
|
|
916
|
+
container.innerHTML = '';
|
|
917
|
+
|
|
918
|
+
schema.inputs.forEach(input => {
|
|
919
|
+
const html = `
|
|
920
|
+
<div class="mb-2">
|
|
921
|
+
<label class="form-label small">${input.name}</label>
|
|
922
|
+
<input type="number" class="form-control form-control-sm test-input"
|
|
923
|
+
data-name="${input.name}"
|
|
924
|
+
value="${input.default || 0}" step="0.01">
|
|
925
|
+
</div>
|
|
926
|
+
`;
|
|
927
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function copyJson() {
|
|
932
|
+
const textarea = document.getElementById('json-preview');
|
|
933
|
+
textarea.select();
|
|
934
|
+
document.execCommand('copy');
|
|
935
|
+
alert('{{ _("JSON copiado al portapapeles") }}');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function formatJson() {
|
|
939
|
+
const textarea = document.getElementById('json-preview');
|
|
940
|
+
try {
|
|
941
|
+
const json = JSON.parse(textarea.value);
|
|
942
|
+
textarea.value = JSON.stringify(json, null, 2);
|
|
943
|
+
} catch (e) {
|
|
944
|
+
alert('{{ _("JSON inválido") }}');
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function loadExample() {
|
|
949
|
+
if (confirm('{{ _("¿Cargar el ejemplo de impuesto progresivo? Esto reemplazará el esquema actual.") }}')) {
|
|
950
|
+
schema = JSON.parse(JSON.stringify(exampleSchema));
|
|
951
|
+
loadSchemaToEditor();
|
|
952
|
+
updateJsonPreview();
|
|
953
|
+
alert('{{ _("Ejemplo cargado exitosamente") }}');
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function saveSchema() {
|
|
958
|
+
try {
|
|
959
|
+
const schemaToSave = collectSchemaFromEditor();
|
|
960
|
+
|
|
961
|
+
const response = await fetch('{{ url_for("calculation_rule.save_schema", id=rule.id) }}', {
|
|
962
|
+
method: 'POST',
|
|
963
|
+
headers: {
|
|
964
|
+
'Content-Type': 'application/json',
|
|
965
|
+
},
|
|
966
|
+
body: JSON.stringify({ schema: schemaToSave })
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const result = await response.json();
|
|
970
|
+
|
|
971
|
+
if (result.success) {
|
|
972
|
+
alert('{{ _("Esquema guardado exitosamente") }}');
|
|
973
|
+
} else {
|
|
974
|
+
alert('{{ _("Error: ") }}' + result.error);
|
|
975
|
+
}
|
|
976
|
+
} catch (e) {
|
|
977
|
+
alert('{{ _("Error al guardar: ") }}' + e.message);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function testCalculation() {
|
|
982
|
+
try {
|
|
983
|
+
const testInputs = {};
|
|
984
|
+
document.querySelectorAll('.test-input').forEach(input => {
|
|
985
|
+
testInputs[input.dataset.name] = parseFloat(input.value) || 0;
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
const response = await fetch('{{ url_for("calculation_rule.test_schema", id=rule.id) }}', {
|
|
989
|
+
method: 'POST',
|
|
990
|
+
headers: {
|
|
991
|
+
'Content-Type': 'application/json',
|
|
992
|
+
},
|
|
993
|
+
body: JSON.stringify({
|
|
994
|
+
schema: collectSchemaFromEditor(),
|
|
995
|
+
inputs: testInputs
|
|
996
|
+
})
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
const result = await response.json();
|
|
1000
|
+
|
|
1001
|
+
const resultDiv = document.getElementById('test-result');
|
|
1002
|
+
const resultContent = document.getElementById('test-result-content');
|
|
1003
|
+
|
|
1004
|
+
resultDiv.style.display = 'block';
|
|
1005
|
+
|
|
1006
|
+
if (result.success) {
|
|
1007
|
+
// Format result with 2 decimal places
|
|
1008
|
+
const formattedResult = formatResultWithDecimals(result.result);
|
|
1009
|
+
resultContent.innerHTML = JSON.stringify(formattedResult, null, 2);
|
|
1010
|
+
resultContent.classList.remove('text-danger');
|
|
1011
|
+
resultContent.classList.add('text-success');
|
|
1012
|
+
} else {
|
|
1013
|
+
resultContent.innerHTML = '{{ _("Error: ") }}' + result.error;
|
|
1014
|
+
resultContent.classList.remove('text-success');
|
|
1015
|
+
resultContent.classList.add('text-danger');
|
|
1016
|
+
}
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
alert('{{ _("Error al probar: ") }}' + e.message);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function formatResultWithDecimals(obj) {
|
|
1023
|
+
if (typeof obj === 'number') {
|
|
1024
|
+
return parseFloat(obj.toFixed(2));
|
|
1025
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
1026
|
+
if (Array.isArray(obj)) {
|
|
1027
|
+
return obj.map(item => formatResultWithDecimals(item));
|
|
1028
|
+
} else {
|
|
1029
|
+
const formatted = {};
|
|
1030
|
+
for (const key in obj) {
|
|
1031
|
+
formatted[key] = formatResultWithDecimals(obj[key]);
|
|
1032
|
+
}
|
|
1033
|
+
return formatted;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return obj;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function loadJsonFile(event) {
|
|
1040
|
+
const file = event.target.files[0];
|
|
1041
|
+
if (!file) return;
|
|
1042
|
+
|
|
1043
|
+
// Check file type
|
|
1044
|
+
if (!file.name.endsWith('.json')) {
|
|
1045
|
+
alert('{{ _("Por favor seleccione un archivo JSON válido") }}');
|
|
1046
|
+
event.target.value = '';
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
const text = await file.text();
|
|
1052
|
+
let jsonData;
|
|
1053
|
+
|
|
1054
|
+
// Parse JSON
|
|
1055
|
+
try {
|
|
1056
|
+
jsonData = JSON.parse(text);
|
|
1057
|
+
} catch (e) {
|
|
1058
|
+
alert('{{ _("Error: El archivo no contiene JSON válido") }}\n' + e.message);
|
|
1059
|
+
event.target.value = '';
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Validate schema structure before loading
|
|
1064
|
+
const validationResult = await validateJsonSchema(jsonData);
|
|
1065
|
+
|
|
1066
|
+
if (!validationResult.valid) {
|
|
1067
|
+
alert('{{ _("Error de validación") }}:\n\n' + validationResult.error);
|
|
1068
|
+
event.target.value = '';
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Show confirmation dialog
|
|
1073
|
+
if (confirm('{{ _("¿Cargar este esquema? Esto reemplazará el esquema actual.") }}')) {
|
|
1074
|
+
schema = jsonData;
|
|
1075
|
+
loadSchemaToEditor();
|
|
1076
|
+
updateJsonPreview();
|
|
1077
|
+
alert('{{ _("Esquema cargado exitosamente") }}');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
alert('{{ _("Error al leer el archivo") }}: ' + e.message);
|
|
1082
|
+
} finally {
|
|
1083
|
+
event.target.value = '';
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function validateJsonSchema(jsonData) {
|
|
1088
|
+
// Basic structure validation
|
|
1089
|
+
if (!jsonData || typeof jsonData !== 'object') {
|
|
1090
|
+
return { valid: false, error: '{{ _("El esquema debe ser un objeto JSON") }}' };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Check for required sections
|
|
1094
|
+
if (!jsonData.steps || !Array.isArray(jsonData.steps)) {
|
|
1095
|
+
return { valid: false, error: '{{ _("El esquema debe contener una sección \'steps\' con un array de pasos") }}' };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Validate steps structure
|
|
1099
|
+
for (let i = 0; i < jsonData.steps.length; i++) {
|
|
1100
|
+
const step = jsonData.steps[i];
|
|
1101
|
+
if (!step.name) {
|
|
1102
|
+
return { valid: false, error: `{{ _("El paso") }} ${i + 1} {{ _("debe tener un campo \'name\'") }}` };
|
|
1103
|
+
}
|
|
1104
|
+
if (!step.type) {
|
|
1105
|
+
return { valid: false, error: `{{ _("El paso") }} ${i + 1} {{ _("debe tener un campo \'type\'") }}` };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Validate step type
|
|
1109
|
+
const validTypes = ['calculation', 'conditional', 'tax_lookup', 'assignment'];
|
|
1110
|
+
if (!validTypes.includes(step.type)) {
|
|
1111
|
+
return {
|
|
1112
|
+
valid: false,
|
|
1113
|
+
error: `{{ _("El paso") }} ${i + 1} {{ _("tiene un tipo inválido") }}: '${step.type}'. {{ _("Tipos permitidos") }}: ${validTypes.join(', ')}`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Validate step-specific fields
|
|
1118
|
+
if (step.type === 'calculation' && !step.formula) {
|
|
1119
|
+
return { valid: false, error: `{{ _("El paso de cálculo") }} '${step.name}' {{ _("debe tener un campo \'formula\'") }}` };
|
|
1120
|
+
}
|
|
1121
|
+
if (step.type === 'conditional' && !step.condition) {
|
|
1122
|
+
return { valid: false, error: `{{ _("El paso condicional") }} '${step.name}' {{ _("debe tener un campo \'condition\'") }}` };
|
|
1123
|
+
}
|
|
1124
|
+
if (step.type === 'tax_lookup' && (!step.table || !step.input)) {
|
|
1125
|
+
return { valid: false, error: `{{ _("El paso de búsqueda en tabla") }} '${step.name}' {{ _("debe tener campos \'table\' e \'input\'") }}` };
|
|
1126
|
+
}
|
|
1127
|
+
if (step.type === 'assignment' && step.value === undefined) {
|
|
1128
|
+
return { valid: false, error: `{{ _("El paso de asignación") }} '${step.name}' {{ _("debe tener un campo \'value\'") }}` };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Validate with backend FormulaEngine
|
|
1133
|
+
try {
|
|
1134
|
+
const response = await fetch('{{ url_for("calculation_rule.validate_schema_api", id=rule.id) }}', {
|
|
1135
|
+
method: 'POST',
|
|
1136
|
+
headers: {
|
|
1137
|
+
'Content-Type': 'application/json',
|
|
1138
|
+
},
|
|
1139
|
+
body: JSON.stringify({ schema: jsonData })
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
const result = await response.json();
|
|
1143
|
+
|
|
1144
|
+
if (!result.success) {
|
|
1145
|
+
return { valid: false, error: result.error };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return { valid: true };
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
// If backend validation fails, still allow if basic validation passed
|
|
1151
|
+
console.warn('Backend validation failed:', e);
|
|
1152
|
+
return { valid: true };
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
</script>
|
|
1156
|
+
|
|
1157
|
+
<!-- Include SortableJS library -->
|
|
1158
|
+
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
1159
|
+
{% endblock %}
|