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.

Files changed (244) hide show
  1. coati_payroll/__init__.py +7 -17
  2. coati_payroll/app.py +2 -13
  3. coati_payroll/audit_helpers.py +2 -13
  4. coati_payroll/auth.py +2 -13
  5. coati_payroll/cli.py +17 -14
  6. coati_payroll/config.py +2 -13
  7. coati_payroll/demo_data.py +2 -13
  8. coati_payroll/enums.py +2 -13
  9. coati_payroll/forms.py +7 -13
  10. coati_payroll/formula_engine/__init__.py +14 -13
  11. coati_payroll/formula_engine/ast/__init__.py +16 -1
  12. coati_payroll/formula_engine/ast/ast_visitor.py +15 -17
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +12 -14
  14. coati_payroll/formula_engine/ast/safe_operators.py +16 -15
  15. coati_payroll/formula_engine/ast/type_converter.py +17 -17
  16. coati_payroll/formula_engine/data_sources.py +3 -13
  17. coati_payroll/formula_engine/engine.py +12 -13
  18. coati_payroll/formula_engine/exceptions.py +13 -13
  19. coati_payroll/formula_engine/execution/__init__.py +15 -13
  20. coati_payroll/formula_engine/execution/execution_context.py +12 -13
  21. coati_payroll/formula_engine/execution/step_executor.py +12 -14
  22. coati_payroll/formula_engine/execution/variable_store.py +13 -13
  23. coati_payroll/formula_engine/novelty_codes.py +3 -13
  24. coati_payroll/formula_engine/results/__init__.py +14 -13
  25. coati_payroll/formula_engine/results/execution_result.py +47 -35
  26. coati_payroll/formula_engine/steps/__init__.py +14 -14
  27. coati_payroll/formula_engine/steps/assignment_step.py +12 -13
  28. coati_payroll/formula_engine/steps/base_step.py +13 -13
  29. coati_payroll/formula_engine/steps/calculation_step.py +12 -13
  30. coati_payroll/formula_engine/steps/conditional_step.py +12 -13
  31. coati_payroll/formula_engine/steps/step_factory.py +11 -13
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +12 -13
  33. coati_payroll/formula_engine/tables/__init__.py +14 -13
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +12 -13
  35. coati_payroll/formula_engine/tables/table_lookup.py +12 -14
  36. coati_payroll/formula_engine/tables/tax_table.py +13 -13
  37. coati_payroll/formula_engine/validation/__init__.py +14 -13
  38. coati_payroll/formula_engine/validation/schema_validator.py +12 -14
  39. coati_payroll/formula_engine/validation/security_validator.py +12 -13
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +12 -13
  41. coati_payroll/formula_engine_examples.py +2 -13
  42. coati_payroll/i18n.py +2 -13
  43. coati_payroll/initial_data.py +2 -13
  44. coati_payroll/interes_engine.py +2 -13
  45. coati_payroll/liquidacion_engine/__init__.py +2 -13
  46. coati_payroll/liquidacion_engine/engine.py +2 -13
  47. coati_payroll/locale_config.py +2 -13
  48. coati_payroll/migrations/20260125_032900_initial_migration.py +2 -14
  49. coati_payroll/migrations/__init__.py +2 -13
  50. coati_payroll/model.py +83 -82
  51. coati_payroll/nomina_engine/__init__.py +2 -13
  52. coati_payroll/nomina_engine/calculators/__init__.py +2 -13
  53. coati_payroll/nomina_engine/calculators/benefit_calculator.py +2 -13
  54. coati_payroll/nomina_engine/calculators/concept_calculator.py +2 -1
  55. coati_payroll/nomina_engine/calculators/deduction_calculator.py +2 -13
  56. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +2 -13
  57. coati_payroll/nomina_engine/calculators/perception_calculator.py +2 -13
  58. coati_payroll/nomina_engine/calculators/salary_calculator.py +2 -13
  59. coati_payroll/nomina_engine/domain/__init__.py +2 -13
  60. coati_payroll/nomina_engine/domain/calculation_items.py +2 -13
  61. coati_payroll/nomina_engine/domain/employee_calculation.py +2 -13
  62. coati_payroll/nomina_engine/domain/payroll_context.py +2 -13
  63. coati_payroll/nomina_engine/engine.py +2 -13
  64. coati_payroll/nomina_engine/processors/__init__.py +2 -13
  65. coati_payroll/nomina_engine/processors/accounting_processor.py +2 -13
  66. coati_payroll/nomina_engine/processors/accumulation_processor.py +2 -13
  67. coati_payroll/nomina_engine/processors/loan_processor.py +2 -13
  68. coati_payroll/nomina_engine/processors/novelty_processor.py +2 -13
  69. coati_payroll/nomina_engine/processors/vacation_processor.py +2 -13
  70. coati_payroll/nomina_engine/repositories/__init__.py +2 -13
  71. coati_payroll/nomina_engine/repositories/acumulado_repository.py +2 -13
  72. coati_payroll/nomina_engine/repositories/base_repository.py +2 -13
  73. coati_payroll/nomina_engine/repositories/config_repository.py +2 -13
  74. coati_payroll/nomina_engine/repositories/employee_repository.py +2 -13
  75. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +2 -13
  76. coati_payroll/nomina_engine/repositories/novelty_repository.py +2 -13
  77. coati_payroll/nomina_engine/repositories/planilla_repository.py +2 -13
  78. coati_payroll/nomina_engine/results/__init__.py +2 -13
  79. coati_payroll/nomina_engine/results/error_result.py +2 -13
  80. coati_payroll/nomina_engine/results/payroll_result.py +2 -13
  81. coati_payroll/nomina_engine/results/validation_result.py +2 -13
  82. coati_payroll/nomina_engine/services/__init__.py +2 -13
  83. coati_payroll/nomina_engine/services/accounting_voucher_service.py +2 -13
  84. coati_payroll/nomina_engine/services/employee_processing_service.py +2 -13
  85. coati_payroll/nomina_engine/services/payroll_execution_service.py +2 -13
  86. coati_payroll/nomina_engine/validators/__init__.py +2 -13
  87. coati_payroll/nomina_engine/validators/base_validator.py +2 -13
  88. coati_payroll/nomina_engine/validators/currency_validator.py +2 -13
  89. coati_payroll/nomina_engine/validators/employee_validator.py +2 -13
  90. coati_payroll/nomina_engine/validators/period_validator.py +2 -13
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +2 -13
  92. coati_payroll/queue/__init__.py +2 -13
  93. coati_payroll/queue/driver.py +2 -13
  94. coati_payroll/queue/drivers/__init__.py +2 -13
  95. coati_payroll/queue/drivers/dramatiq_driver.py +9 -16
  96. coati_payroll/queue/drivers/huey_driver.py +2 -13
  97. coati_payroll/queue/drivers/noop_driver.py +2 -13
  98. coati_payroll/queue/selector.py +2 -13
  99. coati_payroll/queue/tasks.py +13 -20
  100. coati_payroll/rate_limiting.py +2 -13
  101. coati_payroll/rbac.py +12 -19
  102. coati_payroll/report_engine.py +2 -13
  103. coati_payroll/report_export.py +2 -13
  104. coati_payroll/schema_validator.py +2 -13
  105. coati_payroll/security.py +2 -13
  106. coati_payroll/static/schema_editor.js +862 -0
  107. coati_payroll/static/styles.css +5 -0
  108. coati_payroll/system_reports.py +2 -13
  109. coati_payroll/templates/auth/login.html +5 -0
  110. coati_payroll/templates/base.html +5 -1
  111. coati_payroll/templates/index.html +5 -0
  112. coati_payroll/templates/macros.html +5 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +5 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +5 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +292 -974
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +5 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +5 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +5 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +5 -0
  120. coati_payroll/templates/modules/configuracion/index.html +24 -27
  121. coati_payroll/templates/modules/currency/form.html +5 -0
  122. coati_payroll/templates/modules/currency/index.html +5 -0
  123. coati_payroll/templates/modules/custom_field/form.html +5 -0
  124. coati_payroll/templates/modules/custom_field/index.html +5 -0
  125. coati_payroll/templates/modules/deduccion/form.html +5 -0
  126. coati_payroll/templates/modules/deduccion/index.html +5 -0
  127. coati_payroll/templates/modules/employee/form.html +5 -0
  128. coati_payroll/templates/modules/employee/index.html +5 -0
  129. coati_payroll/templates/modules/empresa/form.html +5 -0
  130. coati_payroll/templates/modules/empresa/index.html +5 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +5 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +5 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +5 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +5 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +5 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +5 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +5 -0
  138. coati_payroll/templates/modules/percepcion/form.html +5 -0
  139. coati_payroll/templates/modules/percepcion/index.html +5 -0
  140. coati_payroll/templates/modules/planilla/config.html +5 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +5 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +5 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +5 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +5 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +5 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +5 -0
  147. coati_payroll/templates/modules/planilla/form.html +5 -0
  148. coati_payroll/templates/modules/planilla/index.html +5 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +5 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +5 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +5 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +5 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +5 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +5 -0
  155. coati_payroll/templates/modules/plugins/index.html +5 -0
  156. coati_payroll/templates/modules/prestacion/form.html +5 -0
  157. coati_payroll/templates/modules/prestacion/index.html +5 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +5 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +5 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +5 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +5 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +5 -0
  163. coati_payroll/templates/modules/prestamo/form.html +5 -0
  164. coati_payroll/templates/modules/prestamo/index.html +5 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +5 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +5 -0
  167. coati_payroll/templates/modules/report/admin_index.html +5 -0
  168. coati_payroll/templates/modules/report/detail.html +5 -0
  169. coati_payroll/templates/modules/report/execute.html +5 -0
  170. coati_payroll/templates/modules/report/index.html +5 -0
  171. coati_payroll/templates/modules/report/permissions.html +5 -0
  172. coati_payroll/templates/modules/settings/index.html +5 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +19 -12
  174. coati_payroll/templates/modules/shared/concept_index.html +39 -27
  175. coati_payroll/templates/modules/tipo_planilla/form.html +5 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +5 -0
  177. coati_payroll/templates/modules/user/form.html +5 -0
  178. coati_payroll/templates/modules/user/index.html +5 -0
  179. coati_payroll/templates/modules/user/profile.html +5 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +5 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +5 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +5 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +5 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +5 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +5 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +5 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +5 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +5 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +5 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +5 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +5 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +5 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +2963 -3561
  195. coati_payroll/vacation_service.py +2 -13
  196. coati_payroll/version.py +3 -14
  197. coati_payroll/vistas/__init__.py +2 -13
  198. coati_payroll/vistas/calculation_rule.py +14 -24
  199. coati_payroll/vistas/carga_inicial_prestacion.py +2 -13
  200. coati_payroll/vistas/config_calculos.py +2 -13
  201. coati_payroll/vistas/configuracion.py +29 -39
  202. coati_payroll/vistas/constants.py +2 -13
  203. coati_payroll/vistas/currency.py +2 -13
  204. coati_payroll/vistas/custom_field.py +2 -13
  205. coati_payroll/vistas/employee.py +2 -13
  206. coati_payroll/vistas/empresa.py +2 -13
  207. coati_payroll/vistas/exchange_rate.py +2 -13
  208. coati_payroll/vistas/liquidacion.py +2 -13
  209. coati_payroll/vistas/payroll_concepts.py +2 -13
  210. coati_payroll/vistas/planilla/__init__.py +2 -13
  211. coati_payroll/vistas/planilla/association_routes.py +8 -17
  212. coati_payroll/vistas/planilla/config_routes.py +2 -13
  213. coati_payroll/vistas/planilla/export_routes.py +28 -35
  214. coati_payroll/vistas/planilla/helpers/__init__.py +2 -13
  215. coati_payroll/vistas/planilla/helpers/association_helpers.py +2 -13
  216. coati_payroll/vistas/planilla/helpers/excel_helpers.py +2 -13
  217. coati_payroll/vistas/planilla/helpers/form_helpers.py +2 -13
  218. coati_payroll/vistas/planilla/nomina_routes.py +2 -13
  219. coati_payroll/vistas/planilla/novedad_routes.py +2 -13
  220. coati_payroll/vistas/planilla/routes.py +2 -13
  221. coati_payroll/vistas/planilla/services/__init__.py +2 -13
  222. coati_payroll/vistas/planilla/services/export_service.py +4 -15
  223. coati_payroll/vistas/planilla/services/nomina_service.py +2 -13
  224. coati_payroll/vistas/planilla/services/novedad_service.py +3 -16
  225. coati_payroll/vistas/planilla/services/planilla_service.py +2 -13
  226. coati_payroll/vistas/planilla/validators/__init__.py +2 -13
  227. coati_payroll/vistas/planilla/validators/planilla_validators.py +2 -13
  228. coati_payroll/vistas/prestacion.py +2 -13
  229. coati_payroll/vistas/prestamo.py +2 -13
  230. coati_payroll/vistas/report.py +2 -13
  231. coati_payroll/vistas/settings.py +2 -13
  232. coati_payroll/vistas/tipo_planilla.py +2 -13
  233. coati_payroll/vistas/user.py +2 -13
  234. coati_payroll/vistas/vacation.py +15 -13
  235. coati_payroll/wsgi_server.py +37 -0
  236. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/METADATA +11 -4
  237. coati_payroll-0.0.13.dist-info/RECORD +248 -0
  238. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  239. coati_payroll/translations/es/LC_MESSAGES/messages.po +0 -7374
  240. coati_payroll-0.0.12.dist-info/RECORD +0 -248
  241. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/LICENSE +0 -0
  242. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/WHEEL +0 -0
  243. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/entry_points.txt +0 -0
  244. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,13 @@
1
- {% extends 'base.html' %}
2
- {% from 'macros.html' import render_messages %}
1
+ {#-
2
+ SPDX-License-Identifier: Apache-2.0
3
+ SPDX-FileCopyrightText: 2025 - 2026 BMO Soluciones, S.A.
4
+ -#}
3
5
 
4
- {% block content %}
6
+ {% extends 'base.html' %} {% from 'macros.html' import render_messages %} {%
7
+ block content %}
5
8
  <div class="container-fluid">
6
9
  {{ render_messages() }}
7
-
10
+
8
11
  <div class="d-flex justify-content-between align-items-center mb-4">
9
12
  <div>
10
13
  <h2>
@@ -12,14 +15,21 @@
12
15
  {{ _('Editor de Esquema') }}: {{ rule.nombre }}
13
16
  </h2>
14
17
  <small class="text-muted">
15
- {{ rule.codigo }} v{{ rule.version }} | {{ rule.jurisdiccion or _('Sin jurisdicción') }}
18
+ {{ rule.codigo }} v{{ rule.version }} | {{ rule.jurisdiccion or
19
+ _('Sin jurisdicción') }}
16
20
  </small>
17
21
  </div>
18
22
  <div>
19
- <a href="{{ url_for('calculation_rule.edit', id=rule.id) }}" class="btn btn-secondary">
23
+ <a
24
+ href="{{ url_for('calculation_rule.edit', id=rule.id) }}"
25
+ class="btn btn-secondary"
26
+ >
20
27
  <i class="bi bi-pencil me-1"></i>{{ _('Editar Metadatos') }}
21
28
  </a>
22
- <a href="{{ url_for('calculation_rule.index') }}" class="btn btn-outline-secondary">
29
+ <a
30
+ href="{{ url_for('calculation_rule.index') }}"
31
+ class="btn btn-outline-secondary"
32
+ >
23
33
  <i class="bi bi-arrow-left me-1"></i>{{ _('Volver') }}
24
34
  </a>
25
35
  </div>
@@ -36,42 +46,71 @@
36
46
  <!-- Meta Section -->
37
47
  <div class="schema-section mb-4">
38
48
  <h5 class="section-title">
39
- <i class="bi bi-info-circle me-2"></i>{{ _('Configuración') }}
49
+ <i class="bi bi-info-circle me-2"></i>{{
50
+ _('Configuración') }}
40
51
  </h5>
41
52
  <div class="row">
42
53
  <div class="col-md-6">
43
54
  <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') }}">
55
+ <label class="form-label"
56
+ >{{ _('Nombre de la regla') }}</label
57
+ >
58
+ <input
59
+ type="text"
60
+ class="form-control"
61
+ id="meta-name"
62
+ placeholder="{{ _('Ej: Income Tax Rule') }}"
63
+ />
47
64
  </div>
48
65
  </div>
49
66
  <div class="col-md-6">
50
67
  <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') }}">
68
+ <label class="form-label"
69
+ >{{ _('Moneda de Referencia') }}</label
70
+ >
71
+ <input
72
+ type="text"
73
+ class="form-control"
74
+ id="meta-reference-currency"
75
+ placeholder="{{ _('Ej: NIO, USD') }}"
76
+ />
54
77
  <div class="form-text">
55
- {{ _('Moneda base para los cálculos. La moneda de la planilla se define en el Tipo de Planilla.') }}
78
+ {{ _('Moneda base para los cálculos. La
79
+ moneda de la planilla se define en el
80
+ Tipo de Planilla.') }}
56
81
  </div>
57
82
  </div>
58
83
  </div>
59
84
  </div>
60
85
  <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>
86
+ <label class="form-label"
87
+ >{{ _('Descripción') }}</label
88
+ >
89
+ <textarea
90
+ class="form-control"
91
+ id="meta-description"
92
+ rows="2"
93
+ placeholder="{{ _('Descripción de lo que calcula esta regla') }}"
94
+ ></textarea>
64
95
  </div>
65
96
  </div>
66
97
 
67
98
  <!-- Inputs Section -->
68
99
  <div class="schema-section mb-4">
69
- <div class="d-flex justify-content-between align-items-center mb-3">
100
+ <div
101
+ class="d-flex justify-content-between align-items-center mb-3"
102
+ >
70
103
  <h5 class="section-title mb-0">
71
- <i class="bi bi-box-arrow-in-right me-2"></i>{{ _('Variables de Entrada') }}
104
+ <i class="bi bi-box-arrow-in-right me-2"></i>{{
105
+ _('Variables de Entrada') }}
72
106
  </h5>
73
- <button type="button" class="btn btn-sm btn-primary" onclick="addInput()">
74
- <i class="bi bi-plus-lg me-1"></i>{{ _('Agregar') }}
107
+ <button
108
+ type="button"
109
+ class="btn btn-sm btn-primary"
110
+ onclick="addInput()"
111
+ >
112
+ <i class="bi bi-plus-lg me-1"></i>{{
113
+ _('Agregar') }}
75
114
  </button>
76
115
  </div>
77
116
  <div id="inputs-container">
@@ -79,33 +118,75 @@
79
118
  </div>
80
119
  <div class="text-muted small mt-2">
81
120
  <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.') }}
121
+ {{ _('Las variables de entrada pueden venir de la
122
+ base de datos (empleado.salario_base) o ser valores
123
+ estáticos.') }}
83
124
  </div>
84
125
  </div>
85
126
 
86
127
  <!-- Steps Section -->
87
128
  <div class="schema-section mb-4">
88
- <div class="d-flex justify-content-between align-items-center mb-3">
129
+ <div
130
+ class="d-flex justify-content-between align-items-center mb-3"
131
+ >
89
132
  <h5 class="section-title mb-0">
90
- <i class="bi bi-list-ol me-2"></i>{{ _('Pasos de Cálculo') }}
133
+ <i class="bi bi-list-ol me-2"></i>{{ _('Pasos de
134
+ Cálculo') }}
91
135
  </h5>
92
136
  <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') }}
137
+ <button
138
+ type="button"
139
+ class="btn btn-primary dropdown-toggle"
140
+ data-bs-toggle="dropdown"
141
+ >
142
+ <i class="bi bi-plus-lg me-1"></i>{{
143
+ _('Agregar Paso') }}
95
144
  </button>
96
145
  <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>
146
+ <li>
147
+ <a
148
+ class="dropdown-item"
149
+ href="#"
150
+ onclick="addStep('calculation')"
151
+ >
152
+ <i class="bi bi-calculator me-2"></i
153
+ >{{ _('Cálculo') }}
154
+ </a>
155
+ </li>
156
+ <li>
157
+ <a
158
+ class="dropdown-item"
159
+ href="#"
160
+ onclick="addStep('conditional')"
161
+ >
162
+ <i
163
+ class="bi bi-signpost-split me-2"
164
+ ></i
165
+ >{{ _('Condicional') }}
166
+ </a>
167
+ </li>
168
+ <li>
169
+ <a
170
+ class="dropdown-item"
171
+ href="#"
172
+ onclick="addStep('tax_lookup')"
173
+ >
174
+ <i class="bi bi-table me-2"></i>{{
175
+ _('Búsqueda en Tabla') }}
176
+ </a>
177
+ </li>
178
+ <li>
179
+ <a
180
+ class="dropdown-item"
181
+ href="#"
182
+ onclick="addStep('assignment')"
183
+ >
184
+ <i
185
+ class="bi bi-arrow-right-circle me-2"
186
+ ></i
187
+ >{{ _('Asignación') }}
188
+ </a>
189
+ </li>
109
190
  </ul>
110
191
  </div>
111
192
  </div>
@@ -116,12 +197,20 @@
116
197
 
117
198
  <!-- Tax Tables Section -->
118
199
  <div class="schema-section mb-4">
119
- <div class="d-flex justify-content-between align-items-center mb-3">
200
+ <div
201
+ class="d-flex justify-content-between align-items-center mb-3"
202
+ >
120
203
  <h5 class="section-title mb-0">
121
- <i class="bi bi-table me-2"></i>{{ _('Tablas de Impuestos') }}
204
+ <i class="bi bi-table me-2"></i>{{ _('Tablas de
205
+ Impuestos') }}
122
206
  </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') }}
207
+ <button
208
+ type="button"
209
+ class="btn btn-sm btn-primary"
210
+ onclick="addTaxTable()"
211
+ >
212
+ <i class="bi bi-plus-lg me-1"></i>{{ _('Agregar
213
+ Tabla') }}
125
214
  </button>
126
215
  </div>
127
216
  <div id="tax-tables-container">
@@ -132,15 +221,22 @@
132
221
  <!-- Output Section -->
133
222
  <div class="schema-section">
134
223
  <h5 class="section-title">
135
- <i class="bi bi-box-arrow-right me-2"></i>{{ _('Resultado Final') }}
224
+ <i class="bi bi-box-arrow-right me-2"></i>{{
225
+ _('Resultado Final') }}
136
226
  </h5>
137
227
  <div class="mb-3">
138
- <label class="form-label">{{ _('Variable de salida') }}</label>
228
+ <label class="form-label"
229
+ >{{ _('Variable de salida') }}</label
230
+ >
139
231
  <select class="form-select" id="output-variable">
140
- <option value="">{{ _('Seleccionar variable de resultado...') }}</option>
232
+ <option value="">
233
+ {{ _('Seleccionar variable de resultado...')
234
+ }}
235
+ </option>
141
236
  </select>
142
237
  <div class="form-text">
143
- {{ _('Seleccione la variable que contiene el resultado final del cálculo.') }}
238
+ {{ _('Seleccione la variable que contiene el
239
+ resultado final del cálculo.') }}
144
240
  </div>
145
241
  </div>
146
242
  </div>
@@ -152,38 +248,64 @@
152
248
  <div class="col-lg-5">
153
249
  <!-- JSON Preview -->
154
250
  <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>
251
+ <div
252
+ class="card-header bg-dark text-white d-flex justify-content-between align-items-center"
253
+ >
254
+ <span
255
+ ><i class="bi bi-code-slash me-2"></i>{{ _('Vista Previa
256
+ JSON') }}</span
257
+ >
157
258
  <div class="btn-group btn-group-sm">
158
- <button type="button" class="btn btn-outline-light" onclick="copyJson()">
259
+ <button
260
+ type="button"
261
+ class="btn btn-outline-light"
262
+ onclick="copyJson()"
263
+ >
159
264
  <i class="bi bi-clipboard"></i>
160
265
  </button>
161
- <button type="button" class="btn btn-outline-light" onclick="formatJson()">
266
+ <button
267
+ type="button"
268
+ class="btn btn-outline-light"
269
+ onclick="formatJson()"
270
+ >
162
271
  <i class="bi bi-magic"></i>
163
272
  </button>
164
273
  </div>
165
274
  </div>
166
275
  <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>
276
+ <textarea
277
+ id="json-preview"
278
+ class="form-control font-monospace"
279
+ rows="15"
280
+ style="border: none; border-radius: 0; resize: vertical"
281
+ ></textarea>
169
282
  </div>
170
283
  </div>
171
284
 
172
285
  <!-- Test Panel -->
173
286
  <div class="card mb-4">
174
287
  <div class="card-header bg-info text-white">
175
- <i class="bi bi-play-circle me-2"></i>{{ _('Probar Cálculo') }}
288
+ <i class="bi bi-play-circle me-2"></i>{{ _('Probar Cálculo')
289
+ }}
176
290
  </div>
177
291
  <div class="card-body">
178
292
  <div id="test-inputs-container">
179
293
  <!-- Dynamic test inputs will be generated -->
180
294
  </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') }}
295
+ <button
296
+ type="button"
297
+ class="btn btn-info w-100 mt-3"
298
+ onclick="testCalculation()"
299
+ >
300
+ <i class="bi bi-play-fill me-1"></i>{{ _('Ejecutar
301
+ Prueba') }}
183
302
  </button>
184
- <div id="test-result" class="mt-3" style="display: none;">
303
+ <div id="test-result" class="mt-3" style="display: none">
185
304
  <h6>{{ _('Resultado:') }}</h6>
186
- <pre class="bg-light p-3 rounded" id="test-result-content"></pre>
305
+ <pre
306
+ class="bg-light p-3 rounded"
307
+ id="test-result-content"
308
+ ></pre>
187
309
  </div>
188
310
  </div>
189
311
  </div>
@@ -192,15 +314,36 @@
192
314
  <div class="card">
193
315
  <div class="card-body">
194
316
  <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') }}
317
+ <button
318
+ type="button"
319
+ class="btn btn-success btn-lg"
320
+ onclick="saveSchema()"
321
+ >
322
+ <i class="bi bi-check-lg me-2"></i>{{ _('Guardar
323
+ Esquema') }}
197
324
  </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') }}
325
+ <button
326
+ type="button"
327
+ class="btn btn-outline-primary"
328
+ onclick="document.getElementById('json-file-input').click()"
329
+ >
330
+ <i class="bi bi-upload me-2"></i>{{ _('Cargar desde
331
+ Archivo JSON') }}
200
332
  </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') }}
333
+ <input
334
+ type="file"
335
+ id="json-file-input"
336
+ accept=".json"
337
+ style="display: none"
338
+ onchange="loadJsonFile(event)"
339
+ />
340
+ <button
341
+ type="button"
342
+ class="btn btn-outline-secondary"
343
+ onclick="loadExample()"
344
+ >
345
+ <i class="bi bi-file-earmark-code me-2"></i>{{
346
+ _('Cargar Ejemplo de Impuesto Progresivo') }}
204
347
  </button>
205
348
  </div>
206
349
  </div>
@@ -210,949 +353,124 @@
210
353
  </div>
211
354
 
212
355
  <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
- });
356
+ .schema-section {
357
+ background: #f8f9fa;
358
+ border-radius: 8px;
359
+ padding: 1.25rem;
360
+ border: 1px solid #e9ecef;
362
361
  }
363
- }
364
362
 
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 || '';
363
+ .section-title {
364
+ color: #495057;
365
+ font-weight: 600;
366
+ margin-bottom: 1rem;
371
367
  }
372
368
 
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));
369
+ .input-item,
370
+ .step-item,
371
+ .tax-table-item {
372
+ background: white;
373
+ border: 1px solid #dee2e6;
374
+ border-radius: 6px;
375
+ padding: 1rem;
376
+ margin-bottom: 0.75rem;
377
+ position: relative;
378
+ cursor: move;
378
379
  }
379
380
 
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));
381
+ .input-item .drag-handle,
382
+ .step-item .drag-handle {
383
+ position: absolute;
384
+ left: 0.5rem;
385
+ top: 50%;
386
+ transform: translateY(-50%);
387
+ color: #6c757d;
388
+ cursor: grab;
389
+ font-size: 1.2rem;
385
390
  }
386
391
 
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
- });
392
+ .input-item .drag-handle:active,
393
+ .step-item .drag-handle:active {
394
+ cursor: grabbing;
394
395
  }
395
396
 
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>';
397
+ .reorder-buttons {
398
+ position: absolute;
399
+ right: 3rem;
400
+ top: 0.5rem;
401
+ display: flex;
402
+ gap: 0.25rem;
420
403
  }
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
404
 
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;
405
+ .reorder-buttons .btn {
406
+ padding: 0.125rem 0.375rem;
407
+ font-size: 0.75rem;
511
408
  }
512
- }
513
-
514
- function removeInput(id) {
515
- document.getElementById(`input-${id}`).remove();
516
- updateJsonPreview();
517
- }
518
409
 
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();
410
+ .input-item:hover,
411
+ .step-item:hover,
412
+ .tax-table-item:hover {
413
+ border-color: #0d6efd;
414
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.1);
525
415
  }
526
- }
527
416
 
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();
417
+ .remove-btn {
418
+ position: absolute;
419
+ top: 0.5rem;
420
+ right: 0.5rem;
421
+ padding: 0.25rem 0.5rem;
422
+ font-size: 0.875rem;
534
423
  }
535
- }
536
424
 
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' : '' }>&gt;</option>
571
- <option value=">=" ${ data?.condition?.operator === '>=' ? 'selected' : '' }>&gt;=</option>
572
- <option value="<" ${ data?.condition?.operator === '<' ? 'selected' : '' }>&lt;</option>
573
- <option value="<=" ${ data?.condition?.operator === '<=' ? 'selected' : '' }>&lt;=</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;
425
+ .step-type-badge {
426
+ font-size: 0.75rem;
427
+ font-weight: 600;
428
+ text-transform: uppercase;
631
429
  }
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
430
 
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();
431
+ .bracket-row {
432
+ background: #f8f9fa;
433
+ border-radius: 4px;
434
+ padding: 0.75rem;
435
+ margin-bottom: 0.5rem;
678
436
  }
679
- }
680
437
 
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();
438
+ #json-preview {
439
+ font-size: 0.85rem;
440
+ background: #1e1e1e;
441
+ color: #d4d4d4;
687
442
  }
688
- }
689
443
 
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));
444
+ .sortable-ghost {
445
+ opacity: 0.4;
718
446
  }
719
-
720
- updateJsonPreview();
721
- }
722
447
 
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
- }
448
+ .sortable-drag {
449
+ opacity: 0.8;
450
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
451
+ }
452
+ </style>
791
453
 
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
- }
454
+ <script>
455
+ // Schema data from backend - using tojson filter for proper escaping
456
+ let schema = {{ schema_data|tojson }};
457
+ console.log("Initial schema:", schema);
906
458
 
907
- function updateJsonPreview() {
908
- schema = collectSchemaFromEditor();
909
- document.getElementById('json-preview').value = JSON.stringify(schema, null, 2);
910
- updateOutputSelect();
911
- generateTestInputs();
912
- }
459
+ const exampleSchema = {{ example_schema_data|tojson }};
913
460
 
914
- function generateTestInputs() {
915
- const container = document.getElementById('test-inputs-container');
916
- container.innerHTML = '';
461
+ // Available data sources from the database
462
+ const availableSources = {{ available_sources_data|tojson }};
917
463
 
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
- }
464
+ // Store rule ID in body dataset for later use
465
+ document.body.dataset.ruleId = '{{ rule.id }}';
930
466
 
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));
467
+ document.addEventListener('DOMContentLoaded', function() {
951
468
  loadSchemaToEditor();
952
469
  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
- }
470
+ initializeSortable();
471
+ });
1155
472
  </script>
473
+ <script src="{{ url_for('static', filename='schema_editor.js') }}"></script>
1156
474
 
1157
475
  <!-- Include SortableJS library -->
1158
476
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>