coati-payroll 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coati-payroll might be problematic. Click here for more details.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Calculation rules CRUD routes."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+
20
+ from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify
21
+ from flask_login import current_user
22
+
23
+ from coati_payroll.forms import ReglaCalculoForm
24
+ from coati_payroll.i18n import _
25
+ from coati_payroll.rbac import require_read_access, require_write_access
26
+ from coati_payroll.model import ReglaCalculo, db
27
+ from coati_payroll.vistas.constants import PER_PAGE
28
+ from coati_payroll.formula_engine import (
29
+ FormulaEngine,
30
+ FormulaEngineError,
31
+ EXAMPLE_IR_NICARAGUA_SCHEMA,
32
+ get_available_sources_for_ui,
33
+ )
34
+
35
+ calculation_rule_bp = Blueprint("calculation_rule", __name__, url_prefix="/calculation-rule")
36
+
37
+ # Constants
38
+ ERROR_RULE_NOT_FOUND = "Regla no encontrada"
39
+
40
+
41
+ @calculation_rule_bp.route("/")
42
+ @require_read_access()
43
+ def index():
44
+ """List all calculation rules with pagination."""
45
+ page = request.args.get("page", 1, type=int)
46
+ pagination = db.paginate(
47
+ db.select(ReglaCalculo).order_by(ReglaCalculo.codigo, ReglaCalculo.version.desc()),
48
+ page=page,
49
+ per_page=PER_PAGE,
50
+ error_out=False,
51
+ )
52
+ return render_template(
53
+ "modules/calculation_rule/index.html",
54
+ rules=pagination.items,
55
+ pagination=pagination,
56
+ )
57
+
58
+
59
+ # Default schema structure for new rules
60
+ DEFAULT_SCHEMA = {
61
+ "meta": {
62
+ "name": "",
63
+ "currency": "",
64
+ "description": "",
65
+ },
66
+ "inputs": [],
67
+ "steps": [],
68
+ "tax_tables": {},
69
+ "output": "",
70
+ }
71
+
72
+
73
+ @calculation_rule_bp.route("/new", methods=["GET", "POST"])
74
+ @require_write_access()
75
+ def new():
76
+ """Create a new calculation rule."""
77
+ form = ReglaCalculoForm()
78
+
79
+ if form.validate_on_submit():
80
+ rule = ReglaCalculo()
81
+ rule.codigo = form.codigo.data
82
+ rule.nombre = form.nombre.data
83
+ rule.descripcion = form.descripcion.data
84
+ rule.jurisdiccion = form.jurisdiccion.data
85
+ rule.moneda_referencia = form.moneda_referencia.data
86
+ rule.version = form.version.data
87
+ rule.tipo_regla = form.tipo_regla.data
88
+ rule.vigente_desde = form.vigente_desde.data
89
+ rule.vigente_hasta = form.vigente_hasta.data
90
+ rule.activo = form.activo.data
91
+ # Initialize with default schema structure
92
+ # Note: The rule's reference currency is for calculation purposes.
93
+ # The actual payroll currency is defined in TipoPlanilla.
94
+ rule.esquema_json = {
95
+ **DEFAULT_SCHEMA,
96
+ "meta": {
97
+ "name": form.nombre.data,
98
+ "reference_currency": form.moneda_referencia.data or "",
99
+ "description": form.descripcion.data or "",
100
+ },
101
+ }
102
+ rule.creado_por = current_user.usuario
103
+
104
+ db.session.add(rule)
105
+ db.session.commit()
106
+ flash(_("Regla de cálculo creada exitosamente."), "success")
107
+ return redirect(url_for("calculation_rule.edit_schema", id=rule.id))
108
+
109
+ return render_template(
110
+ "modules/calculation_rule/form.html",
111
+ form=form,
112
+ title=_("Nueva Regla de Cálculo"),
113
+ )
114
+
115
+
116
+ @calculation_rule_bp.route("/edit/<string:id>", methods=["GET", "POST"])
117
+ @require_write_access()
118
+ def edit(id: str):
119
+ """Edit an existing calculation rule metadata."""
120
+ rule = db.session.get(ReglaCalculo, id)
121
+ if not rule:
122
+ flash(_(ERROR_RULE_NOT_FOUND), "error")
123
+ return redirect(url_for("calculation_rule.index"))
124
+
125
+ form = ReglaCalculoForm(obj=rule)
126
+
127
+ if form.validate_on_submit():
128
+ rule.codigo = form.codigo.data
129
+ rule.nombre = form.nombre.data
130
+ rule.descripcion = form.descripcion.data
131
+ rule.jurisdiccion = form.jurisdiccion.data
132
+ rule.moneda_referencia = form.moneda_referencia.data
133
+ rule.version = form.version.data
134
+ rule.tipo_regla = form.tipo_regla.data
135
+ rule.vigente_desde = form.vigente_desde.data
136
+ rule.vigente_hasta = form.vigente_hasta.data
137
+ rule.activo = form.activo.data
138
+ rule.modificado_por = current_user.usuario
139
+
140
+ db.session.commit()
141
+ flash(_("Regla de cálculo actualizada exitosamente."), "success")
142
+ return redirect(url_for("calculation_rule.index"))
143
+
144
+ return render_template(
145
+ "modules/calculation_rule/form.html",
146
+ form=form,
147
+ title=_("Editar Regla de Cálculo"),
148
+ rule=rule,
149
+ )
150
+
151
+
152
+ @calculation_rule_bp.route("/edit-schema/<string:id>", methods=["GET"])
153
+ @require_write_access()
154
+ def edit_schema(id: str):
155
+ """Edit the JSON schema of a calculation rule."""
156
+ rule = db.session.get(ReglaCalculo, id)
157
+ if not rule:
158
+ flash(_(ERROR_RULE_NOT_FOUND), "error")
159
+ return redirect(url_for("calculation_rule.index"))
160
+
161
+ # Get available data sources for the UI
162
+ available_sources = get_available_sources_for_ui()
163
+
164
+ return render_template(
165
+ "modules/calculation_rule/schema_editor.html",
166
+ rule=rule,
167
+ schema_json=json.dumps(rule.esquema_json or {}, indent=2),
168
+ example_schema=json.dumps(EXAMPLE_IR_NICARAGUA_SCHEMA, indent=2),
169
+ available_sources=json.dumps(available_sources),
170
+ )
171
+
172
+
173
+ @calculation_rule_bp.route("/api/save-schema/<string:id>", methods=["POST"])
174
+ @require_write_access()
175
+ def save_schema(id: str):
176
+ """API endpoint to save the JSON schema."""
177
+ rule = db.session.get(ReglaCalculo, id)
178
+ if not rule:
179
+ return jsonify({"success": False, "error": ERROR_RULE_NOT_FOUND}), 404
180
+
181
+ try:
182
+ data = request.get_json()
183
+ schema = data.get("schema", {})
184
+
185
+ # Validate schema by trying to create a FormulaEngine instance
186
+ try:
187
+ FormulaEngine(schema)
188
+ except FormulaEngineError as e:
189
+ return jsonify({"success": False, "error": f"Esquema inválido: {e}"}), 400
190
+
191
+ rule.esquema_json = schema
192
+ rule.modificado_por = current_user.usuario
193
+ db.session.commit()
194
+
195
+ return jsonify({"success": True, "message": "Esquema guardado exitosamente"})
196
+ except json.JSONDecodeError as e:
197
+ return jsonify({"success": False, "error": f"JSON inválido: {e}"}), 400
198
+ except Exception as e:
199
+ return jsonify({"success": False, "error": str(e)}), 500
200
+
201
+
202
+ @calculation_rule_bp.route("/api/validate-schema/<string:id>", methods=["POST"])
203
+ @require_write_access()
204
+ def validate_schema_api(id: str):
205
+ """API endpoint to validate a JSON schema without saving it."""
206
+ rule = db.session.get(ReglaCalculo, id)
207
+ if not rule:
208
+ return jsonify({"success": False, "error": ERROR_RULE_NOT_FOUND}), 404
209
+
210
+ try:
211
+ data = request.get_json()
212
+ schema = data.get("schema", {})
213
+
214
+ # Validate schema structure and formula safety
215
+ from coati_payroll.schema_validator import validate_schema_deep
216
+
217
+ try:
218
+ validate_schema_deep(schema)
219
+ except Exception as e:
220
+ return jsonify({"success": False, "error": f"Esquema inválido: {e}"}), 400
221
+
222
+ # Also validate by trying to create a FormulaEngine instance
223
+ try:
224
+ FormulaEngine(schema)
225
+ except FormulaEngineError as e:
226
+ return jsonify({"success": False, "error": f"Esquema inválido: {e}"}), 400
227
+
228
+ return jsonify({"success": True, "message": "Esquema válido"})
229
+ except json.JSONDecodeError as e:
230
+ return jsonify({"success": False, "error": f"JSON inválido: {e}"}), 400
231
+ except Exception as e:
232
+ return jsonify({"success": False, "error": str(e)}), 500
233
+
234
+
235
+ @calculation_rule_bp.route("/api/test-schema/<string:id>", methods=["POST"])
236
+ @require_write_access()
237
+ def test_schema(id: str):
238
+ """API endpoint to test the calculation schema with sample data."""
239
+ rule = db.session.get(ReglaCalculo, id)
240
+ if not rule:
241
+ return jsonify({"success": False, "error": ERROR_RULE_NOT_FOUND}), 404
242
+
243
+ try:
244
+ data = request.get_json()
245
+ schema = data.get("schema", rule.esquema_json or {})
246
+ test_inputs = data.get("inputs", {})
247
+
248
+ engine = FormulaEngine(schema)
249
+ result = engine.execute(test_inputs)
250
+
251
+ return jsonify({"success": True, "result": result})
252
+ except FormulaEngineError as e:
253
+ return jsonify({"success": False, "error": str(e)}), 400
254
+ except Exception as e:
255
+ return jsonify({"success": False, "error": str(e)}), 500
256
+
257
+
258
+ @calculation_rule_bp.route("/delete/<string:id>", methods=["POST"])
259
+ @require_write_access()
260
+ def delete(id: str):
261
+ """Delete a calculation rule."""
262
+ rule = db.session.get(ReglaCalculo, id)
263
+ if not rule:
264
+ flash(_(ERROR_RULE_NOT_FOUND), "error")
265
+ return redirect(url_for("calculation_rule.index"))
266
+
267
+ db.session.delete(rule)
268
+ db.session.commit()
269
+ flash(_("Regla de cálculo eliminada exitosamente."), "success")
270
+ return redirect(url_for("calculation_rule.index"))
271
+
272
+
273
+ @calculation_rule_bp.route("/duplicate/<string:id>", methods=["POST"])
274
+ @require_write_access()
275
+ def duplicate(id: str):
276
+ """Duplicate a calculation rule with a new version."""
277
+ rule = db.session.get(ReglaCalculo, id)
278
+ if not rule:
279
+ flash(_(ERROR_RULE_NOT_FOUND), "error")
280
+ return redirect(url_for("calculation_rule.index"))
281
+
282
+ # Create a new rule with incremented version
283
+ new_rule = ReglaCalculo()
284
+ new_rule.codigo = rule.codigo
285
+ new_rule.nombre = rule.nombre
286
+ new_rule.descripcion = rule.descripcion
287
+ new_rule.jurisdiccion = rule.jurisdiccion
288
+ new_rule.moneda_referencia = rule.moneda_referencia
289
+ new_rule.tipo_regla = rule.tipo_regla
290
+ new_rule.vigente_desde = rule.vigente_desde
291
+ new_rule.vigente_hasta = rule.vigente_hasta
292
+ new_rule.activo = False # New version starts inactive
293
+ new_rule.esquema_json = rule.esquema_json.copy() if rule.esquema_json else {}
294
+ new_rule.creado_por = current_user.usuario
295
+
296
+ # Increment version
297
+ try:
298
+ parts = rule.version.split(".")
299
+ parts[-1] = str(int(parts[-1]) + 1)
300
+ new_rule.version = ".".join(parts)
301
+ except (ValueError, IndexError):
302
+ new_rule.version = rule.version + ".1"
303
+
304
+ db.session.add(new_rule)
305
+ db.session.commit()
306
+ flash(_("Regla de cálculo duplicada exitosamente."), "success")
307
+ return redirect(url_for("calculation_rule.edit_schema", id=new_rule.id))