coati-payroll 0.0.12__py3-none-any.whl → 0.0.13__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 +7 -17
- coati_payroll/app.py +2 -13
- coati_payroll/audit_helpers.py +2 -13
- coati_payroll/auth.py +2 -13
- coati_payroll/cli.py +17 -14
- coati_payroll/config.py +2 -13
- coati_payroll/demo_data.py +2 -13
- coati_payroll/enums.py +2 -13
- coati_payroll/forms.py +7 -13
- coati_payroll/formula_engine/__init__.py +14 -13
- coati_payroll/formula_engine/ast/__init__.py +16 -1
- coati_payroll/formula_engine/ast/ast_visitor.py +15 -17
- coati_payroll/formula_engine/ast/expression_evaluator.py +12 -14
- coati_payroll/formula_engine/ast/safe_operators.py +16 -15
- coati_payroll/formula_engine/ast/type_converter.py +17 -17
- coati_payroll/formula_engine/data_sources.py +3 -13
- coati_payroll/formula_engine/engine.py +12 -13
- coati_payroll/formula_engine/exceptions.py +13 -13
- coati_payroll/formula_engine/execution/__init__.py +15 -13
- coati_payroll/formula_engine/execution/execution_context.py +12 -13
- coati_payroll/formula_engine/execution/step_executor.py +12 -14
- coati_payroll/formula_engine/execution/variable_store.py +13 -13
- coati_payroll/formula_engine/novelty_codes.py +3 -13
- coati_payroll/formula_engine/results/__init__.py +14 -13
- coati_payroll/formula_engine/results/execution_result.py +47 -35
- coati_payroll/formula_engine/steps/__init__.py +14 -14
- coati_payroll/formula_engine/steps/assignment_step.py +12 -13
- coati_payroll/formula_engine/steps/base_step.py +13 -13
- coati_payroll/formula_engine/steps/calculation_step.py +12 -13
- coati_payroll/formula_engine/steps/conditional_step.py +12 -13
- coati_payroll/formula_engine/steps/step_factory.py +11 -13
- coati_payroll/formula_engine/steps/tax_lookup_step.py +12 -13
- coati_payroll/formula_engine/tables/__init__.py +14 -13
- coati_payroll/formula_engine/tables/bracket_calculator.py +12 -13
- coati_payroll/formula_engine/tables/table_lookup.py +12 -14
- coati_payroll/formula_engine/tables/tax_table.py +13 -13
- coati_payroll/formula_engine/validation/__init__.py +14 -13
- coati_payroll/formula_engine/validation/schema_validator.py +12 -14
- coati_payroll/formula_engine/validation/security_validator.py +12 -13
- coati_payroll/formula_engine/validation/tax_table_validator.py +12 -13
- coati_payroll/formula_engine_examples.py +2 -13
- coati_payroll/i18n.py +2 -13
- coati_payroll/initial_data.py +2 -13
- coati_payroll/interes_engine.py +2 -13
- coati_payroll/liquidacion_engine/__init__.py +2 -13
- coati_payroll/liquidacion_engine/engine.py +2 -13
- coati_payroll/locale_config.py +2 -13
- coati_payroll/migrations/20260125_032900_initial_migration.py +2 -14
- coati_payroll/migrations/__init__.py +2 -13
- coati_payroll/model.py +83 -82
- coati_payroll/nomina_engine/__init__.py +2 -13
- coati_payroll/nomina_engine/calculators/__init__.py +2 -13
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +2 -13
- coati_payroll/nomina_engine/calculators/concept_calculator.py +2 -1
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +2 -13
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +2 -13
- coati_payroll/nomina_engine/calculators/perception_calculator.py +2 -13
- coati_payroll/nomina_engine/calculators/salary_calculator.py +2 -13
- coati_payroll/nomina_engine/domain/__init__.py +2 -13
- coati_payroll/nomina_engine/domain/calculation_items.py +2 -13
- coati_payroll/nomina_engine/domain/employee_calculation.py +2 -13
- coati_payroll/nomina_engine/domain/payroll_context.py +2 -13
- coati_payroll/nomina_engine/engine.py +2 -13
- coati_payroll/nomina_engine/processors/__init__.py +2 -13
- coati_payroll/nomina_engine/processors/accounting_processor.py +2 -13
- coati_payroll/nomina_engine/processors/accumulation_processor.py +2 -13
- coati_payroll/nomina_engine/processors/loan_processor.py +2 -13
- coati_payroll/nomina_engine/processors/novelty_processor.py +2 -13
- coati_payroll/nomina_engine/processors/vacation_processor.py +2 -13
- coati_payroll/nomina_engine/repositories/__init__.py +2 -13
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/base_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/config_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/employee_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/novelty_repository.py +2 -13
- coati_payroll/nomina_engine/repositories/planilla_repository.py +2 -13
- coati_payroll/nomina_engine/results/__init__.py +2 -13
- coati_payroll/nomina_engine/results/error_result.py +2 -13
- coati_payroll/nomina_engine/results/payroll_result.py +2 -13
- coati_payroll/nomina_engine/results/validation_result.py +2 -13
- coati_payroll/nomina_engine/services/__init__.py +2 -13
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +2 -13
- coati_payroll/nomina_engine/services/employee_processing_service.py +2 -13
- coati_payroll/nomina_engine/services/payroll_execution_service.py +2 -13
- coati_payroll/nomina_engine/validators/__init__.py +2 -13
- coati_payroll/nomina_engine/validators/base_validator.py +2 -13
- coati_payroll/nomina_engine/validators/currency_validator.py +2 -13
- coati_payroll/nomina_engine/validators/employee_validator.py +2 -13
- coati_payroll/nomina_engine/validators/period_validator.py +2 -13
- coati_payroll/nomina_engine/validators/planilla_validator.py +2 -13
- coati_payroll/queue/__init__.py +2 -13
- coati_payroll/queue/driver.py +2 -13
- coati_payroll/queue/drivers/__init__.py +2 -13
- coati_payroll/queue/drivers/dramatiq_driver.py +9 -16
- coati_payroll/queue/drivers/huey_driver.py +2 -13
- coati_payroll/queue/drivers/noop_driver.py +2 -13
- coati_payroll/queue/selector.py +2 -13
- coati_payroll/queue/tasks.py +13 -20
- coati_payroll/rate_limiting.py +2 -13
- coati_payroll/rbac.py +12 -19
- coati_payroll/report_engine.py +2 -13
- coati_payroll/report_export.py +2 -13
- coati_payroll/schema_validator.py +2 -13
- coati_payroll/security.py +2 -13
- coati_payroll/static/schema_editor.js +862 -0
- coati_payroll/static/styles.css +5 -0
- coati_payroll/system_reports.py +2 -13
- coati_payroll/templates/auth/login.html +5 -0
- coati_payroll/templates/base.html +5 -1
- coati_payroll/templates/index.html +5 -0
- coati_payroll/templates/macros.html +5 -0
- coati_payroll/templates/modules/calculation_rule/form.html +5 -0
- coati_payroll/templates/modules/calculation_rule/index.html +5 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +292 -974
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +5 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +5 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +5 -0
- coati_payroll/templates/modules/config_calculos/index.html +5 -0
- coati_payroll/templates/modules/configuracion/index.html +24 -27
- coati_payroll/templates/modules/currency/form.html +5 -0
- coati_payroll/templates/modules/currency/index.html +5 -0
- coati_payroll/templates/modules/custom_field/form.html +5 -0
- coati_payroll/templates/modules/custom_field/index.html +5 -0
- coati_payroll/templates/modules/deduccion/form.html +5 -0
- coati_payroll/templates/modules/deduccion/index.html +5 -0
- coati_payroll/templates/modules/employee/form.html +5 -0
- coati_payroll/templates/modules/employee/index.html +5 -0
- coati_payroll/templates/modules/empresa/form.html +5 -0
- coati_payroll/templates/modules/empresa/index.html +5 -0
- coati_payroll/templates/modules/exchange_rate/form.html +5 -0
- coati_payroll/templates/modules/exchange_rate/import.html +5 -0
- coati_payroll/templates/modules/exchange_rate/index.html +5 -0
- coati_payroll/templates/modules/liquidacion/index.html +5 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +5 -0
- coati_payroll/templates/modules/liquidacion/ver.html +5 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +5 -0
- coati_payroll/templates/modules/percepcion/form.html +5 -0
- coati_payroll/templates/modules/percepcion/index.html +5 -0
- coati_payroll/templates/modules/planilla/config.html +5 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +5 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +5 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +5 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +5 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +5 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +5 -0
- coati_payroll/templates/modules/planilla/form.html +5 -0
- coati_payroll/templates/modules/planilla/index.html +5 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +5 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +5 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +5 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +5 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +5 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +5 -0
- coati_payroll/templates/modules/plugins/index.html +5 -0
- coati_payroll/templates/modules/prestacion/form.html +5 -0
- coati_payroll/templates/modules/prestacion/index.html +5 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +5 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +5 -0
- coati_payroll/templates/modules/prestamo/approve.html +5 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +5 -0
- coati_payroll/templates/modules/prestamo/detail.html +5 -0
- coati_payroll/templates/modules/prestamo/form.html +5 -0
- coati_payroll/templates/modules/prestamo/index.html +5 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +5 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +5 -0
- coati_payroll/templates/modules/report/admin_index.html +5 -0
- coati_payroll/templates/modules/report/detail.html +5 -0
- coati_payroll/templates/modules/report/execute.html +5 -0
- coati_payroll/templates/modules/report/index.html +5 -0
- coati_payroll/templates/modules/report/permissions.html +5 -0
- coati_payroll/templates/modules/settings/index.html +5 -0
- coati_payroll/templates/modules/shared/concept_form.html +19 -12
- coati_payroll/templates/modules/shared/concept_index.html +39 -27
- coati_payroll/templates/modules/tipo_planilla/form.html +5 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +5 -0
- coati_payroll/templates/modules/user/form.html +5 -0
- coati_payroll/templates/modules/user/index.html +5 -0
- coati_payroll/templates/modules/user/profile.html +5 -0
- coati_payroll/templates/modules/vacation/account_detail.html +5 -0
- coati_payroll/templates/modules/vacation/account_form.html +5 -0
- coati_payroll/templates/modules/vacation/account_index.html +5 -0
- coati_payroll/templates/modules/vacation/dashboard.html +5 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +5 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +5 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +5 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +5 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +5 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +5 -0
- coati_payroll/templates/modules/vacation/policy_form.html +5 -0
- coati_payroll/templates/modules/vacation/policy_index.html +5 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +5 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +2963 -3561
- coati_payroll/vacation_service.py +2 -13
- coati_payroll/version.py +3 -14
- coati_payroll/vistas/__init__.py +2 -13
- coati_payroll/vistas/calculation_rule.py +14 -24
- coati_payroll/vistas/carga_inicial_prestacion.py +2 -13
- coati_payroll/vistas/config_calculos.py +2 -13
- coati_payroll/vistas/configuracion.py +29 -39
- coati_payroll/vistas/constants.py +2 -13
- coati_payroll/vistas/currency.py +2 -13
- coati_payroll/vistas/custom_field.py +2 -13
- coati_payroll/vistas/employee.py +2 -13
- coati_payroll/vistas/empresa.py +2 -13
- coati_payroll/vistas/exchange_rate.py +2 -13
- coati_payroll/vistas/liquidacion.py +2 -13
- coati_payroll/vistas/payroll_concepts.py +2 -13
- coati_payroll/vistas/planilla/__init__.py +2 -13
- coati_payroll/vistas/planilla/association_routes.py +8 -17
- coati_payroll/vistas/planilla/config_routes.py +2 -13
- coati_payroll/vistas/planilla/export_routes.py +28 -35
- coati_payroll/vistas/planilla/helpers/__init__.py +2 -13
- coati_payroll/vistas/planilla/helpers/association_helpers.py +2 -13
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +2 -13
- coati_payroll/vistas/planilla/helpers/form_helpers.py +2 -13
- coati_payroll/vistas/planilla/nomina_routes.py +2 -13
- coati_payroll/vistas/planilla/novedad_routes.py +2 -13
- coati_payroll/vistas/planilla/routes.py +2 -13
- coati_payroll/vistas/planilla/services/__init__.py +2 -13
- coati_payroll/vistas/planilla/services/export_service.py +4 -15
- coati_payroll/vistas/planilla/services/nomina_service.py +2 -13
- coati_payroll/vistas/planilla/services/novedad_service.py +3 -16
- coati_payroll/vistas/planilla/services/planilla_service.py +2 -13
- coati_payroll/vistas/planilla/validators/__init__.py +2 -13
- coati_payroll/vistas/planilla/validators/planilla_validators.py +2 -13
- coati_payroll/vistas/prestacion.py +2 -13
- coati_payroll/vistas/prestamo.py +2 -13
- coati_payroll/vistas/report.py +2 -13
- coati_payroll/vistas/settings.py +2 -13
- coati_payroll/vistas/tipo_planilla.py +2 -13
- coati_payroll/vistas/user.py +2 -13
- coati_payroll/vistas/vacation.py +15 -13
- coati_payroll/wsgi_server.py +37 -0
- {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/METADATA +11 -4
- coati_payroll-0.0.13.dist-info/RECORD +248 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +0 -7374
- coati_payroll-0.0.12.dist-info/RECORD +0 -248
- {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/LICENSE +0 -0
- {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/WHEEL +0 -0
- {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/entry_points.txt +0 -0
- {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 - 2026 BMO Soluciones, S.A.
|
|
3
|
+
|
|
4
|
+
// Note: The following variables are initialized in the HTML template with Jinja2:
|
|
5
|
+
// - schema: initialized from {{ schema_data|tojson }}
|
|
6
|
+
// - exampleSchema: initialized from {{ example_schema_data|tojson }}
|
|
7
|
+
// - availableSources: initialized from {{ available_sources_data|tojson }}
|
|
8
|
+
|
|
9
|
+
// Counter for unique IDs
|
|
10
|
+
let inputCounter = 0;
|
|
11
|
+
let stepCounter = 0;
|
|
12
|
+
let tableCounter = 0;
|
|
13
|
+
|
|
14
|
+
// Sortable instances
|
|
15
|
+
let inputsSortable = null;
|
|
16
|
+
let stepsSortable = null;
|
|
17
|
+
|
|
18
|
+
function initializeSortable() {
|
|
19
|
+
// Initialize sortable for inputs
|
|
20
|
+
const inputsContainer = document.getElementById('inputs-container');
|
|
21
|
+
if (inputsContainer && typeof Sortable !== 'undefined') {
|
|
22
|
+
inputsSortable = Sortable.create(inputsContainer, {
|
|
23
|
+
animation: 150,
|
|
24
|
+
handle: '.drag-handle',
|
|
25
|
+
ghostClass: 'sortable-ghost',
|
|
26
|
+
dragClass: 'sortable-drag',
|
|
27
|
+
onEnd: function() {
|
|
28
|
+
updateJsonPreview();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Initialize sortable for steps
|
|
34
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
35
|
+
if (stepsContainer && typeof Sortable !== 'undefined') {
|
|
36
|
+
stepsSortable = Sortable.create(stepsContainer, {
|
|
37
|
+
animation: 150,
|
|
38
|
+
handle: '.drag-handle',
|
|
39
|
+
ghostClass: 'sortable-ghost',
|
|
40
|
+
dragClass: 'sortable-drag',
|
|
41
|
+
onEnd: function() {
|
|
42
|
+
updateJsonPreview();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadSchemaToEditor() {
|
|
49
|
+
// Load meta
|
|
50
|
+
if (schema.meta) {
|
|
51
|
+
document.getElementById('meta-name').value = schema.meta.name || '';
|
|
52
|
+
document.getElementById('meta-reference-currency').value = schema.meta.reference_currency || schema.meta.currency || '';
|
|
53
|
+
document.getElementById('meta-description').value = schema.meta.description || '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load inputs
|
|
57
|
+
const inputsContainer = document.getElementById('inputs-container');
|
|
58
|
+
inputsContainer.innerHTML = '';
|
|
59
|
+
if (schema.inputs && schema.inputs.length > 0) {
|
|
60
|
+
schema.inputs.forEach(input => addInput(input));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load steps
|
|
64
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
65
|
+
stepsContainer.innerHTML = '';
|
|
66
|
+
if (schema.steps && schema.steps.length > 0) {
|
|
67
|
+
schema.steps.forEach(step => addStep(step.type, step));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Load tax tables
|
|
71
|
+
const tablesContainer = document.getElementById('tax-tables-container');
|
|
72
|
+
tablesContainer.innerHTML = '';
|
|
73
|
+
if (schema.tax_tables) {
|
|
74
|
+
Object.entries(schema.tax_tables).forEach(([name, brackets]) => {
|
|
75
|
+
addTaxTable(name, brackets);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Load output
|
|
80
|
+
updateOutputSelect();
|
|
81
|
+
document.getElementById('output-variable').value = schema.output || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addInput(data = null) {
|
|
85
|
+
const container = document.getElementById('inputs-container');
|
|
86
|
+
const id = inputCounter++;
|
|
87
|
+
|
|
88
|
+
// Build source options grouped by category
|
|
89
|
+
let sourceOptionsHtml = '<option value="">Select source or enter value...</option>';
|
|
90
|
+
let currentCategory = '';
|
|
91
|
+
availableSources.forEach(source => {
|
|
92
|
+
if (source.category !== currentCategory) {
|
|
93
|
+
if (currentCategory !== '') {
|
|
94
|
+
sourceOptionsHtml += '</optgroup>';
|
|
95
|
+
}
|
|
96
|
+
sourceOptionsHtml += `<optgroup label="${source.category}">`;
|
|
97
|
+
currentCategory = source.category;
|
|
98
|
+
}
|
|
99
|
+
sourceOptionsHtml += `<option value="${source.value}" title="${source.description}">${source.label}</option>`;
|
|
100
|
+
});
|
|
101
|
+
if (currentCategory !== '') {
|
|
102
|
+
sourceOptionsHtml += '</optgroup>';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const currentSourceValue = data?.source || data?.default || '';
|
|
106
|
+
|
|
107
|
+
const html = `
|
|
108
|
+
<div class="input-item" id="input-${id}">
|
|
109
|
+
<div class="drag-handle">
|
|
110
|
+
<i class="bi bi-grip-vertical"></i>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="reorder-buttons">
|
|
113
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputUp(${id})" title="Move up">
|
|
114
|
+
<i class="bi bi-arrow-up"></i>
|
|
115
|
+
</button>
|
|
116
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputDown(${id})" title="Move down">
|
|
117
|
+
<i class="bi bi-arrow-down"></i>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeInput(${id})">
|
|
121
|
+
<i class="bi bi-x"></i>
|
|
122
|
+
</button>
|
|
123
|
+
<div class="row">
|
|
124
|
+
<div class="col-md-4">
|
|
125
|
+
<label class="form-label small">Name</label>
|
|
126
|
+
<input type="text" class="form-control form-control-sm input-name"
|
|
127
|
+
value="${data?.name || ''}" placeholder="Ex: salary_monthly"
|
|
128
|
+
onchange="updateJsonPreview()">
|
|
129
|
+
</div>
|
|
130
|
+
<div class="col-md-3">
|
|
131
|
+
<label class="form-label small">Type</label>
|
|
132
|
+
<select class="form-select form-select-sm input-type" onchange="updateJsonPreview()">
|
|
133
|
+
<option value="decimal" ${data?.type === 'decimal' ? 'selected' : ''}>Decimal</option>
|
|
134
|
+
<option value="integer" ${data?.type === 'integer' ? 'selected' : ''}>Integer</option>
|
|
135
|
+
<option value="string" ${data?.type === 'string' ? 'selected' : ''}>Text</option>
|
|
136
|
+
<option value="boolean" ${data?.type === 'boolean' ? 'selected' : ''}>Boolean</option>
|
|
137
|
+
<option value="date" ${data?.type === 'date' ? 'selected' : ''}>Date</option>
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="col-md-5">
|
|
141
|
+
<label class="form-label small">Data Source</label>
|
|
142
|
+
<select class="form-select form-select-sm input-source-select"
|
|
143
|
+
onchange="handleSourceChange(this, ${id}); updateJsonPreview()">
|
|
144
|
+
${sourceOptionsHtml}
|
|
145
|
+
<option value="__custom__">Custom value...</option>
|
|
146
|
+
</select>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="row mt-2">
|
|
150
|
+
<div class="col-md-6">
|
|
151
|
+
<input type="text" class="form-control form-control-sm input-source-custom"
|
|
152
|
+
value="${currentSourceValue}"
|
|
153
|
+
placeholder="Source (employee.field) or default value"
|
|
154
|
+
onchange="updateJsonPreview()"
|
|
155
|
+
style="${currentSourceValue && !availableSources.find(s => s.value === currentSourceValue) ? '' : 'display:none'}">
|
|
156
|
+
</div>
|
|
157
|
+
<div class="col-md-6">
|
|
158
|
+
<input type="text" class="form-control form-control-sm input-description"
|
|
159
|
+
value="${data?.description || ''}"
|
|
160
|
+
placeholder="Description (optional)"
|
|
161
|
+
onchange="updateJsonPreview()">
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
167
|
+
|
|
168
|
+
// Set the select value if it matches an available source
|
|
169
|
+
const selectEl = document.querySelector(`#input-${id} .input-source-select`);
|
|
170
|
+
if (currentSourceValue) {
|
|
171
|
+
const matchingSource = availableSources.find(s => s.value === currentSourceValue);
|
|
172
|
+
if (matchingSource) {
|
|
173
|
+
selectEl.value = currentSourceValue;
|
|
174
|
+
} else {
|
|
175
|
+
selectEl.value = '__custom__';
|
|
176
|
+
document.querySelector(`#input-${id} .input-source-custom`).style.display = '';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
updateJsonPreview();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleSourceChange(selectEl, inputId) {
|
|
184
|
+
const customInput = document.querySelector(`#input-${inputId} .input-source-custom`);
|
|
185
|
+
if (selectEl.value === '__custom__') {
|
|
186
|
+
customInput.style.display = '';
|
|
187
|
+
customInput.focus();
|
|
188
|
+
} else if (selectEl.value === '') {
|
|
189
|
+
customInput.style.display = 'none';
|
|
190
|
+
customInput.value = '';
|
|
191
|
+
} else {
|
|
192
|
+
customInput.style.display = 'none';
|
|
193
|
+
customInput.value = selectEl.value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function removeInput(id) {
|
|
198
|
+
document.getElementById(`input-${id}`).remove();
|
|
199
|
+
updateJsonPreview();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function moveInputUp(id) {
|
|
203
|
+
const element = document.getElementById(`input-${id}`);
|
|
204
|
+
const prev = element.previousElementSibling;
|
|
205
|
+
if (prev) {
|
|
206
|
+
element.parentNode.insertBefore(element, prev);
|
|
207
|
+
updateJsonPreview();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function moveInputDown(id) {
|
|
212
|
+
const element = document.getElementById(`input-${id}`);
|
|
213
|
+
const next = element.nextElementSibling;
|
|
214
|
+
if (next) {
|
|
215
|
+
element.parentNode.insertBefore(next, element);
|
|
216
|
+
updateJsonPreview();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function addStep(type, data = null) {
|
|
221
|
+
const container = document.getElementById('steps-container');
|
|
222
|
+
const id = stepCounter++;
|
|
223
|
+
|
|
224
|
+
let typeHtml = '';
|
|
225
|
+
let typeBadgeClass = '';
|
|
226
|
+
|
|
227
|
+
switch(type) {
|
|
228
|
+
case 'calculation':
|
|
229
|
+
typeBadgeClass = 'bg-primary';
|
|
230
|
+
typeHtml = `
|
|
231
|
+
<div class="mb-2">
|
|
232
|
+
<label class="form-label small">Fórmula</label>
|
|
233
|
+
<input type="text" class="form-control form-control-sm step-formula"
|
|
234
|
+
value="${data?.formula || ''}"
|
|
235
|
+
placeholder="Ej: salario_mensual * 12"
|
|
236
|
+
onchange="updateJsonPreview()">
|
|
237
|
+
</div>
|
|
238
|
+
`;
|
|
239
|
+
break;
|
|
240
|
+
case 'conditional':
|
|
241
|
+
typeBadgeClass = 'bg-warning text-dark';
|
|
242
|
+
typeHtml = `
|
|
243
|
+
<div class="mb-2">
|
|
244
|
+
<label class="form-label small">Condición</label>
|
|
245
|
+
<div class="row g-2">
|
|
246
|
+
<div class="col-4">
|
|
247
|
+
<input type="text" class="form-control form-control-sm step-cond-left"
|
|
248
|
+
value="${data?.condition?.left || ''}" placeholder="Variable"
|
|
249
|
+
onchange="updateJsonPreview()">
|
|
250
|
+
</div>
|
|
251
|
+
<div class="col-2">
|
|
252
|
+
<select class="form-select form-select-sm step-cond-op" onchange="updateJsonPreview()">
|
|
253
|
+
<option value=">" ${ data?.condition?.operator === '>' ? 'selected' : '' }>></option>
|
|
254
|
+
<option value=">=" ${ data?.condition?.operator === '>=' ? 'selected' : '' }>>=</option>
|
|
255
|
+
<option value="<" ${ data?.condition?.operator === '<' ? 'selected' : '' }><</option>
|
|
256
|
+
<option value="<=" ${ data?.condition?.operator === '<=' ? 'selected' : '' }><=</option>
|
|
257
|
+
<option value="==" ${ data?.condition?.operator === '==' ? 'selected' : '' }>==</option>
|
|
258
|
+
<option value="!=" ${ data?.condition?.operator === '!=' ? 'selected' : '' }>!=</option>
|
|
259
|
+
</select>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="col-4">
|
|
262
|
+
<input type="text" class="form-control form-control-sm step-cond-right"
|
|
263
|
+
value="${data?.condition?.right || ''}" placeholder="Valor"
|
|
264
|
+
onchange="updateJsonPreview()">
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="row g-2">
|
|
269
|
+
<div class="col-6">
|
|
270
|
+
<label class="form-label small">Si verdadero</label>
|
|
271
|
+
<input type="text" class="form-control form-control-sm step-if-true"
|
|
272
|
+
value="${data?.if_true || ''}" placeholder="Fórmula si cumple"
|
|
273
|
+
onchange="updateJsonPreview()">
|
|
274
|
+
</div>
|
|
275
|
+
<div class="col-6">
|
|
276
|
+
<label class="form-label small">Si falso</label>
|
|
277
|
+
<input type="text" class="form-control form-control-sm step-if-false"
|
|
278
|
+
value="${data?.if_false || ''}" placeholder="Fórmula si no cumple"
|
|
279
|
+
onchange="updateJsonPreview()">
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
break;
|
|
284
|
+
case 'tax_lookup':
|
|
285
|
+
typeBadgeClass = 'bg-danger';
|
|
286
|
+
typeHtml = `
|
|
287
|
+
<div class="row g-2">
|
|
288
|
+
<div class="col-6">
|
|
289
|
+
<label class="form-label small">Tabla de impuestos</label>
|
|
290
|
+
<input type="text" class="form-control form-control-sm step-table"
|
|
291
|
+
value="${data?.table || ''}" placeholder="Nombre de la tabla"
|
|
292
|
+
onchange="updateJsonPreview()">
|
|
293
|
+
</div>
|
|
294
|
+
<div class="col-6">
|
|
295
|
+
<label class="form-label small">Variable de entrada</label>
|
|
296
|
+
<input type="text" class="form-control form-control-sm step-input"
|
|
297
|
+
value="${data?.input || ''}" placeholder="Variable a buscar"
|
|
298
|
+
onchange="updateJsonPreview()">
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
break;
|
|
303
|
+
case 'assignment':
|
|
304
|
+
typeBadgeClass = 'bg-info';
|
|
305
|
+
typeHtml = `
|
|
306
|
+
<div class="mb-2">
|
|
307
|
+
<label class="form-label small">Valor</label>
|
|
308
|
+
<input type="text" class="form-control form-control-sm step-value"
|
|
309
|
+
value="${data?.value || ''}" placeholder="Variable o valor a asignar"
|
|
310
|
+
onchange="updateJsonPreview()">
|
|
311
|
+
</div>
|
|
312
|
+
`;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const html = `
|
|
317
|
+
<div class="step-item" id="step-${id}" data-type="${type}">
|
|
318
|
+
<div class="drag-handle">
|
|
319
|
+
<i class="bi bi-grip-vertical"></i>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="reorder-buttons">
|
|
322
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepUp(${id})" title="Subir">
|
|
323
|
+
<i class="bi bi-arrow-up"></i>
|
|
324
|
+
</button>
|
|
325
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepDown(${id})" title="Bajar">
|
|
326
|
+
<i class="bi bi-arrow-down"></i>
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeStep(${id})">
|
|
330
|
+
<i class="bi bi-x"></i>
|
|
331
|
+
</button>
|
|
332
|
+
<div class="d-flex align-items-center mb-2">
|
|
333
|
+
<span class="badge ${typeBadgeClass} step-type-badge me-2">${type}</span>
|
|
334
|
+
<input type="text" class="form-control form-control-sm step-name"
|
|
335
|
+
value="${data?.name || ''}" placeholder="Nombre del paso"
|
|
336
|
+
style="max-width: 200px;" onchange="updateJsonPreview()">
|
|
337
|
+
</div>
|
|
338
|
+
${typeHtml}
|
|
339
|
+
<div class="mt-2">
|
|
340
|
+
<input type="text" class="form-control form-control-sm step-description"
|
|
341
|
+
value="${data?.description || ''}" placeholder="Descripción (opcional)"
|
|
342
|
+
onchange="updateJsonPreview()">
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
`;
|
|
346
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
347
|
+
updateJsonPreview();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function removeStep(id) {
|
|
351
|
+
document.getElementById(`step-${id}`).remove();
|
|
352
|
+
updateJsonPreview();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function moveStepUp(id) {
|
|
356
|
+
const element = document.getElementById(`step-${id}`);
|
|
357
|
+
const prev = element.previousElementSibling;
|
|
358
|
+
if (prev) {
|
|
359
|
+
element.parentNode.insertBefore(element, prev);
|
|
360
|
+
updateJsonPreview();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function moveStepDown(id) {
|
|
365
|
+
const element = document.getElementById(`step-${id}`);
|
|
366
|
+
const next = element.nextElementSibling;
|
|
367
|
+
if (next) {
|
|
368
|
+
element.parentNode.insertBefore(next, element);
|
|
369
|
+
updateJsonPreview();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function addTaxTable(name = null, brackets = null) {
|
|
374
|
+
const container = document.getElementById('tax-tables-container');
|
|
375
|
+
const id = tableCounter++;
|
|
376
|
+
|
|
377
|
+
const html = `
|
|
378
|
+
<div class="tax-table-item" id="table-${id}">
|
|
379
|
+
<button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeTaxTable(${id})">
|
|
380
|
+
<i class="bi bi-x"></i>
|
|
381
|
+
</button>
|
|
382
|
+
<div class="mb-3">
|
|
383
|
+
<label class="form-label small">Nombre de la tabla</label>
|
|
384
|
+
<input type="text" class="form-control form-control-sm table-name"
|
|
385
|
+
value="${name || ''}" placeholder="Ej: income_tax_brackets"
|
|
386
|
+
onchange="updateJsonPreview()">
|
|
387
|
+
</div>
|
|
388
|
+
<div class="brackets-container" id="brackets-${id}">
|
|
389
|
+
<!-- Brackets will be added here -->
|
|
390
|
+
</div>
|
|
391
|
+
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addBracket(${id})">
|
|
392
|
+
<i class="bi bi-plus-lg me-1"></i>Agregar Tramo
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
`;
|
|
396
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
397
|
+
|
|
398
|
+
// Add existing brackets
|
|
399
|
+
if (brackets && brackets.length > 0) {
|
|
400
|
+
brackets.forEach(bracket => addBracket(id, bracket));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
updateJsonPreview();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function removeTaxTable(id) {
|
|
407
|
+
document.getElementById(`table-${id}`).remove();
|
|
408
|
+
updateJsonPreview();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function addBracket(tableId, data = null) {
|
|
412
|
+
const container = document.getElementById(`brackets-${tableId}`);
|
|
413
|
+
const html = `
|
|
414
|
+
<div class="bracket-row">
|
|
415
|
+
<div class="row g-2 align-items-center">
|
|
416
|
+
<div class="col">
|
|
417
|
+
<input type="number" class="form-control form-control-sm bracket-min"
|
|
418
|
+
value="${data?.min ?? ''}" placeholder="Mín"
|
|
419
|
+
onchange="updateJsonPreview()">
|
|
420
|
+
</div>
|
|
421
|
+
<div class="col">
|
|
422
|
+
<input type="number" class="form-control form-control-sm bracket-max"
|
|
423
|
+
value="${data?.max ?? ''}" placeholder="Máx"
|
|
424
|
+
onchange="updateJsonPreview()">
|
|
425
|
+
</div>
|
|
426
|
+
<div class="col">
|
|
427
|
+
<input type="number" class="form-control form-control-sm bracket-rate"
|
|
428
|
+
value="${data?.rate ?? ''}" placeholder="Tasa" step="0.01"
|
|
429
|
+
onchange="updateJsonPreview()">
|
|
430
|
+
</div>
|
|
431
|
+
<div class="col">
|
|
432
|
+
<input type="number" class="form-control form-control-sm bracket-fixed"
|
|
433
|
+
value="${data?.fixed ?? ''}" placeholder="Fijo"
|
|
434
|
+
onchange="updateJsonPreview()">
|
|
435
|
+
</div>
|
|
436
|
+
<div class="col">
|
|
437
|
+
<input type="number" class="form-control form-control-sm bracket-over"
|
|
438
|
+
value="${data?.over ?? ''}" placeholder="Sobre"
|
|
439
|
+
onchange="updateJsonPreview()">
|
|
440
|
+
</div>
|
|
441
|
+
<div class="col-auto">
|
|
442
|
+
<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.bracket-row').remove(); updateJsonPreview();">
|
|
443
|
+
<i class="bi bi-x"></i>
|
|
444
|
+
</button>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
`;
|
|
449
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
450
|
+
updateJsonPreview();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function updateOutputSelect() {
|
|
454
|
+
const select = document.getElementById('output-variable');
|
|
455
|
+
const currentValue = select.value;
|
|
456
|
+
select.innerHTML = '<option value="">Seleccionar variable de resultado...</option>';
|
|
457
|
+
|
|
458
|
+
// Add all input names
|
|
459
|
+
document.querySelectorAll('.input-name').forEach(input => {
|
|
460
|
+
if (input.value) {
|
|
461
|
+
select.innerHTML += `<option value="${input.value}">${input.value} (input)</option>`;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Add all step names
|
|
466
|
+
document.querySelectorAll('.step-name').forEach(input => {
|
|
467
|
+
if (input.value) {
|
|
468
|
+
select.innerHTML += `<option value="${input.value}">${input.value} (step)</option>`;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
select.value = currentValue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function collectSchemaFromEditor() {
|
|
476
|
+
const newSchema = {
|
|
477
|
+
meta: {
|
|
478
|
+
name: document.getElementById('meta-name').value,
|
|
479
|
+
reference_currency: document.getElementById('meta-reference-currency').value,
|
|
480
|
+
description: document.getElementById('meta-description').value
|
|
481
|
+
},
|
|
482
|
+
inputs: [],
|
|
483
|
+
steps: [],
|
|
484
|
+
tax_tables: {},
|
|
485
|
+
output: document.getElementById('output-variable').value
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Collect inputs
|
|
489
|
+
document.querySelectorAll('.input-item').forEach(item => {
|
|
490
|
+
const input = {
|
|
491
|
+
name: item.querySelector('.input-name').value,
|
|
492
|
+
type: item.querySelector('.input-type').value
|
|
493
|
+
};
|
|
494
|
+
// Get source from either the select dropdown or the custom input field
|
|
495
|
+
const selectEl = item.querySelector('.input-source-select');
|
|
496
|
+
const customEl = item.querySelector('.input-source-custom');
|
|
497
|
+
let source = '';
|
|
498
|
+
if (selectEl && selectEl.value && selectEl.value !== '__custom__' && selectEl.value !== '') {
|
|
499
|
+
source = selectEl.value;
|
|
500
|
+
} else if (customEl) {
|
|
501
|
+
source = customEl.value;
|
|
502
|
+
}
|
|
503
|
+
if (source) {
|
|
504
|
+
if (source.includes('.')) {
|
|
505
|
+
input.source = source;
|
|
506
|
+
} else if (!isNaN(source)) {
|
|
507
|
+
input.default = parseFloat(source);
|
|
508
|
+
} else {
|
|
509
|
+
input.default = source;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const desc = item.querySelector('.input-description').value;
|
|
513
|
+
if (desc) input.description = desc;
|
|
514
|
+
|
|
515
|
+
if (input.name) newSchema.inputs.push(input);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Collect steps
|
|
519
|
+
document.querySelectorAll('.step-item').forEach(item => {
|
|
520
|
+
const type = item.dataset.type;
|
|
521
|
+
const step = {
|
|
522
|
+
name: item.querySelector('.step-name').value,
|
|
523
|
+
type: type
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
switch(type) {
|
|
527
|
+
case 'calculation':
|
|
528
|
+
step.formula = item.querySelector('.step-formula').value;
|
|
529
|
+
break;
|
|
530
|
+
case 'conditional':
|
|
531
|
+
step.condition = {
|
|
532
|
+
left: item.querySelector('.step-cond-left').value,
|
|
533
|
+
operator: item.querySelector('.step-cond-op').value,
|
|
534
|
+
right: item.querySelector('.step-cond-right').value
|
|
535
|
+
};
|
|
536
|
+
// Try to parse right value as number
|
|
537
|
+
if (!isNaN(step.condition.right)) {
|
|
538
|
+
step.condition.right = parseFloat(step.condition.right);
|
|
539
|
+
}
|
|
540
|
+
step.if_true = item.querySelector('.step-if-true').value;
|
|
541
|
+
step.if_false = item.querySelector('.step-if-false').value;
|
|
542
|
+
break;
|
|
543
|
+
case 'tax_lookup':
|
|
544
|
+
step.table = item.querySelector('.step-table').value;
|
|
545
|
+
step.input = item.querySelector('.step-input').value;
|
|
546
|
+
break;
|
|
547
|
+
case 'assignment':
|
|
548
|
+
step.value = item.querySelector('.step-value').value;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const desc = item.querySelector('.step-description').value;
|
|
553
|
+
if (desc) step.description = desc;
|
|
554
|
+
|
|
555
|
+
if (step.name) newSchema.steps.push(step);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Collect tax tables
|
|
559
|
+
document.querySelectorAll('.tax-table-item').forEach(item => {
|
|
560
|
+
const tableName = item.querySelector('.table-name').value;
|
|
561
|
+
if (!tableName) return;
|
|
562
|
+
|
|
563
|
+
const brackets = [];
|
|
564
|
+
item.querySelectorAll('.bracket-row').forEach(row => {
|
|
565
|
+
const bracket = {};
|
|
566
|
+
const min = row.querySelector('.bracket-min').value;
|
|
567
|
+
const max = row.querySelector('.bracket-max').value;
|
|
568
|
+
const rate = row.querySelector('.bracket-rate').value;
|
|
569
|
+
const fixed = row.querySelector('.bracket-fixed').value;
|
|
570
|
+
const over = row.querySelector('.bracket-over').value;
|
|
571
|
+
|
|
572
|
+
if (min !== '') bracket.min = parseFloat(min);
|
|
573
|
+
if (max !== '') bracket.max = parseFloat(max);
|
|
574
|
+
else bracket.max = null;
|
|
575
|
+
if (rate !== '') bracket.rate = parseFloat(rate);
|
|
576
|
+
if (fixed !== '') bracket.fixed = parseFloat(fixed);
|
|
577
|
+
if (over !== '') bracket.over = parseFloat(over);
|
|
578
|
+
|
|
579
|
+
if (Object.keys(bracket).length > 0) brackets.push(bracket);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
if (brackets.length > 0) {
|
|
583
|
+
newSchema.tax_tables[tableName] = brackets;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return newSchema;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function updateJsonPreview() {
|
|
591
|
+
schema = collectSchemaFromEditor();
|
|
592
|
+
document.getElementById('json-preview').value = JSON.stringify(schema, null, 2);
|
|
593
|
+
updateOutputSelect();
|
|
594
|
+
generateTestInputs();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function generateTestInputs() {
|
|
598
|
+
const container = document.getElementById('test-inputs-container');
|
|
599
|
+
container.innerHTML = '';
|
|
600
|
+
|
|
601
|
+
schema.inputs.forEach(input => {
|
|
602
|
+
const html = `
|
|
603
|
+
<div class="mb-2">
|
|
604
|
+
<label class="form-label small">${input.name}</label>
|
|
605
|
+
<input type="number" class="form-control form-control-sm test-input"
|
|
606
|
+
data-name="${input.name}"
|
|
607
|
+
value="${input.default || 0}" step="0.01">
|
|
608
|
+
</div>
|
|
609
|
+
`;
|
|
610
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function copyJson() {
|
|
615
|
+
const textarea = document.getElementById('json-preview');
|
|
616
|
+
textarea.select();
|
|
617
|
+
document.execCommand('copy');
|
|
618
|
+
alert('JSON copied to clipboard');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function formatJson() {
|
|
622
|
+
const textarea = document.getElementById('json-preview');
|
|
623
|
+
try {
|
|
624
|
+
const json = JSON.parse(textarea.value);
|
|
625
|
+
textarea.value = JSON.stringify(json, null, 2);
|
|
626
|
+
} catch (e) {
|
|
627
|
+
alert('Invalid JSON');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function loadExample() {
|
|
632
|
+
if (confirm('Load progressive tax example? This will replace the current schema.')) {
|
|
633
|
+
schema = JSON.parse(JSON.stringify(exampleSchema));
|
|
634
|
+
loadSchemaToEditor();
|
|
635
|
+
updateJsonPreview();
|
|
636
|
+
alert('Example loaded successfully');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function saveSchema() {
|
|
641
|
+
try {
|
|
642
|
+
const schemaToSave = collectSchemaFromEditor();
|
|
643
|
+
const ruleId = document.body.dataset.ruleId;
|
|
644
|
+
|
|
645
|
+
const response = await fetch(`/calculation_rule/${ruleId}/save_schema`, {
|
|
646
|
+
method: 'POST',
|
|
647
|
+
headers: {
|
|
648
|
+
'Content-Type': 'application/json',
|
|
649
|
+
},
|
|
650
|
+
body: JSON.stringify({ schema: schemaToSave })
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const result = await response.json();
|
|
654
|
+
|
|
655
|
+
if (result.success) {
|
|
656
|
+
alert('Esquema guardado exitosamente');
|
|
657
|
+
} else {
|
|
658
|
+
alert('Error: ' + result.error);
|
|
659
|
+
}
|
|
660
|
+
} catch (e) {
|
|
661
|
+
alert('Error al guardar: ' + e.message);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function testCalculation() {
|
|
666
|
+
try {
|
|
667
|
+
const testInputs = {};
|
|
668
|
+
document.querySelectorAll('.test-input').forEach(input => {
|
|
669
|
+
testInputs[input.dataset.name] = parseFloat(input.value) || 0;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const ruleId = document.body.dataset.ruleId;
|
|
673
|
+
|
|
674
|
+
const response = await fetch(`/calculation_rule/${ruleId}/test_schema`, {
|
|
675
|
+
method: 'POST',
|
|
676
|
+
headers: {
|
|
677
|
+
'Content-Type': 'application/json',
|
|
678
|
+
},
|
|
679
|
+
body: JSON.stringify({
|
|
680
|
+
schema: collectSchemaFromEditor(),
|
|
681
|
+
inputs: testInputs
|
|
682
|
+
})
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const result = await response.json();
|
|
686
|
+
|
|
687
|
+
const resultDiv = document.getElementById('test-result');
|
|
688
|
+
const resultContent = document.getElementById('test-result-content');
|
|
689
|
+
|
|
690
|
+
resultDiv.style.display = 'block';
|
|
691
|
+
|
|
692
|
+
if (result.success) {
|
|
693
|
+
// Format result with 2 decimal places
|
|
694
|
+
const formattedResult = formatResultWithDecimals(result.result);
|
|
695
|
+
resultContent.innerHTML = JSON.stringify(formattedResult, null, 2);
|
|
696
|
+
resultContent.classList.remove('text-danger');
|
|
697
|
+
resultContent.classList.add('text-success');
|
|
698
|
+
} else {
|
|
699
|
+
resultContent.innerHTML = 'Error: ' + result.error;
|
|
700
|
+
resultContent.classList.remove('text-success');
|
|
701
|
+
resultContent.classList.add('text-danger');
|
|
702
|
+
}
|
|
703
|
+
} catch (e) {
|
|
704
|
+
alert('Error al probar: ' + e.message);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function formatResultWithDecimals(obj) {
|
|
709
|
+
if (typeof obj === 'number') {
|
|
710
|
+
return parseFloat(obj.toFixed(2));
|
|
711
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
712
|
+
if (Array.isArray(obj)) {
|
|
713
|
+
return obj.map(item => formatResultWithDecimals(item));
|
|
714
|
+
} else {
|
|
715
|
+
const formatted = {};
|
|
716
|
+
for (const key in obj) {
|
|
717
|
+
formatted[key] = formatResultWithDecimals(obj[key]);
|
|
718
|
+
}
|
|
719
|
+
return formatted;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return obj;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function loadJsonFile(event) {
|
|
726
|
+
const file = event.target.files[0];
|
|
727
|
+
if (!file) return;
|
|
728
|
+
|
|
729
|
+
// Check file type
|
|
730
|
+
if (!file.name.endsWith('.json')) {
|
|
731
|
+
alert('Please select a valid JSON file');
|
|
732
|
+
event.target.value = '';
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
const text = await file.text();
|
|
738
|
+
let jsonData;
|
|
739
|
+
|
|
740
|
+
// Parse JSON
|
|
741
|
+
try {
|
|
742
|
+
jsonData = JSON.parse(text);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
alert('Error: The file does not contain valid JSON\n' + e.message);
|
|
745
|
+
event.target.value = '';
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Validate schema structure before loading
|
|
750
|
+
const validationResult = await validateJsonSchema(jsonData);
|
|
751
|
+
|
|
752
|
+
if (!validationResult.valid) {
|
|
753
|
+
alert('Validation error:\n\n' + validationResult.error);
|
|
754
|
+
event.target.value = '';
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Show confirmation dialog
|
|
759
|
+
if (confirm('Load this schema? This will replace the current schema.')) {
|
|
760
|
+
schema = jsonData;
|
|
761
|
+
loadSchemaToEditor();
|
|
762
|
+
updateJsonPreview();
|
|
763
|
+
alert('Schema loaded successfully');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
} catch (e) {
|
|
767
|
+
alert('Error reading file: ' + e.message);
|
|
768
|
+
} finally {
|
|
769
|
+
event.target.value = '';
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function validateJsonSchema(jsonData) {
|
|
774
|
+
// Basic structure validation
|
|
775
|
+
if (!jsonData || typeof jsonData !== 'object') {
|
|
776
|
+
return { valid: false, error: 'The schema must be a JSON object' };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check for required sections
|
|
780
|
+
if (!jsonData.steps || !Array.isArray(jsonData.steps)) {
|
|
781
|
+
return { valid: false, error: 'The schema must contain a \'steps\' section with an array of steps' };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Validate steps structure
|
|
785
|
+
for (let i = 0; i < jsonData.steps.length; i++) {
|
|
786
|
+
const step = jsonData.steps[i];
|
|
787
|
+
if (!step.name) {
|
|
788
|
+
return { valid: false, error: `Step ${i + 1} must have a 'name' field` };
|
|
789
|
+
}
|
|
790
|
+
if (!step.type) {
|
|
791
|
+
return { valid: false, error: `Step ${i + 1} must have a 'type' field` };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Validate step type
|
|
795
|
+
const validTypes = ['calculation', 'conditional', 'tax_lookup', 'assignment'];
|
|
796
|
+
if (!validTypes.includes(step.type)) {
|
|
797
|
+
return {
|
|
798
|
+
valid: false,
|
|
799
|
+
error: `Step ${i + 1} has an invalid type: '${step.type}'. Allowed types: ${validTypes.join(', ')}`
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Validate step-specific fields
|
|
804
|
+
if (step.type === 'calculation' && !step.formula) {
|
|
805
|
+
return { valid: false, error: `Calculation step '${step.name}' must have a 'formula' field` };
|
|
806
|
+
}
|
|
807
|
+
if (step.type === 'conditional' && !step.condition) {
|
|
808
|
+
return { valid: false, error: `Conditional step '${step.name}' must have a 'condition' field` };
|
|
809
|
+
}
|
|
810
|
+
if (step.type === 'tax_lookup' && (!step.table || !step.input)) {
|
|
811
|
+
return { valid: false, error: `Tax lookup step '${step.name}' must have 'table' and 'input' fields` };
|
|
812
|
+
}
|
|
813
|
+
if (step.type === 'assignment' && step.value === undefined) {
|
|
814
|
+
return { valid: false, error: `Assignment step '${step.name}' must have a 'value' field` };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Validate with backend FormulaEngine
|
|
819
|
+
try {
|
|
820
|
+
const ruleId = document.body.dataset.ruleId;
|
|
821
|
+
const response = await fetch(`/calculation_rule/${ruleId}/validate_schema_api`, {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: {
|
|
824
|
+
'Content-Type': 'application/json',
|
|
825
|
+
},
|
|
826
|
+
body: JSON.stringify({ schema: jsonData })
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const result = await response.json();
|
|
830
|
+
|
|
831
|
+
if (!result.success) {
|
|
832
|
+
return { valid: false, error: result.error };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return { valid: true };
|
|
836
|
+
} catch (e) {
|
|
837
|
+
// If backend validation fails, still allow if basic validation passed
|
|
838
|
+
console.warn('Backend validation failed:', e);
|
|
839
|
+
return { valid: true };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Make functions globally accessible
|
|
844
|
+
window.copyJson = copyJson;
|
|
845
|
+
window.formatJson = formatJson;
|
|
846
|
+
window.moveInputUp = moveInputUp;
|
|
847
|
+
window.moveInputDown = moveInputDown;
|
|
848
|
+
window.removeInput = removeInput;
|
|
849
|
+
window.moveStepUp = moveStepUp;
|
|
850
|
+
window.moveStepDown = moveStepDown;
|
|
851
|
+
window.removeStep = removeStep;
|
|
852
|
+
window.removeTaxTable = removeTaxTable;
|
|
853
|
+
window.addBracket = addBracket;
|
|
854
|
+
window.loadExample = loadExample;
|
|
855
|
+
window.saveSchema = saveSchema;
|
|
856
|
+
window.testCalculation = testCalculation;
|
|
857
|
+
window.loadJsonFile = loadJsonFile;
|
|
858
|
+
window.addInput = addInput;
|
|
859
|
+
window.addStep = addStep;
|
|
860
|
+
window.addTaxTable = addTaxTable;
|
|
861
|
+
window.handleSourceChange = handleSourceChange;
|
|
862
|
+
window.updateJsonPreview = updateJsonPreview;
|