Motor de cálculo
Serconsa implementa una imitación de Presto para el cálculo de presupuestos. Las reglas están definidas en docs/imitacion-presto/reglas-calculo-presto.md y en el código de backend/app/services/budget_recalc_conceptos.py y backend/app/repositories/budget_item_serialization.py.
Tipos de concepto
Sección titulada «Tipos de concepto»Presto distingue tres tipos semánticos, identificados por el código:
| Tipo | Código | Cálculo |
|---|---|---|
| Partida | Cualquier código sin Ʃ ni % |
Importe = cantidad × precio unitario, o Σ recursos. |
| Subtotal | Empieza por Ʃ |
Suma importes de conceptos anteriores del mismo nivel. |
| Porcentaje | Contiene % |
Importe = base × pct / 100. |
Detección automática en backend/app/models/budget_item.py:115:
@staticmethoddef detectar_tipo_concepto(codigo: str, unidad: str | None = None) -> str: if codigo.startswith("Ʃ"): return "subtotal" if "%" in codigo or unidad == "%": return "porcentaje" return "partida"Subtotal (Ʃ)
Sección titulada «Subtotal (Ʃ)»- Suma solo conceptos del mismo nivel (mismo
parent_id). - “Anteriores” =
ordenmenor dentro del mismo padre. - En raíz, la base son los capítulos anteriores completos.
- En un capítulo, la base son solo las partidas de ese capítulo.
- No se suma al importe del padre; es informativo.
Implementación: backend/app/services/budget_recalc_conceptos.py:25 (calcular_subtotal).
def calcular_subtotal(item, children_map) -> Decimal: total = sum( (_safe_importe(child.importe) for child in _hermanos_anteriores(item, children_map)), start=Decimal("0"), ) item.importe = total.quantize(DEC2, rounding=ROUND_HALF_UP) return item.importePorcentaje (%)
Sección titulada «Porcentaje (%)»- Aplica porcentaje sobre la suma de importes anteriores del mismo nivel.
- Incluye subtotales anteriores.
- Sí se suma al importe del padre porque representa un coste real.
- Redondeo a 2 decimales
ROUND_HALF_UP.
Implementación: backend/app/services/budget_recalc_conceptos.py:35 (calcular_porcentaje).
def calcular_porcentaje(item, children_map) -> Decimal: base = sum( (_safe_importe(child.importe) for child in _hermanos_anteriores(item, children_map)), start=Decimal("0"), ) pct = item.porcentaje if item.porcentaje is not None else item.precio_unitario pct = pct or Decimal("0") item.importe = (base * pct / Decimal("100")).quantize(DEC2, rounding=ROUND_HALF_UP) return item.importeTotal de un nodo
Sección titulada «Total de un nodo»El importe de un capítulo/padre es la suma de los importes de sus hijos:
- Excluye hijos de tipo
subtotal. - Incluye hijos de tipo
porcentaje. - Se aplica recursivamente.
Esto se implementa en backend/app/repositories/budget_item_serialization.py:516 (calculate_subtotals) y en backend/app/repositories/budget_item_repository.py (recalculate_bottom_up).
Cálculo de una partida
Sección titulada «Cálculo de una partida»backend/app/repositories/budget_item_serialization.py:342 (_calc_partida):
- Si la partida no tiene descompuesto:
importe = cantidad × precio_unitario. - Si la partida tiene descompuesto:
- Cantidad de obra = mediciones, o recursos
principal, o fallback. importe = Σ importe_recursos.precio_unitario = importe / cantidad_obra(si hay cantidad).
- Cantidad de obra = mediciones, o recursos
Variables en fórmulas
Sección titulada «Variables en fórmulas»Antes de operar con Decimal, se resuelven las variables del presupuesto. Ejemplo de fórmula de cantidad:
{BASE} * {ALTURA}Resolución en backend/app/services/budget_variable_service.py:125 (resolve_with_dict).
Reglas de redondeo
Sección titulada «Reglas de redondeo»| Campo | Decimales | Redondeo |
|---|---|---|
| Dimensiones (N, L, A, H) | 2 | ROUND_HALF_UP |
| Parcial de medición (N×L×A×H) | 2 | ROUND_HALF_UP |
| Total de medición | 2 | ROUND_HALF_UP |
| Importe de recurso | 2 | ROUND_HALF_UP |
| Precio unitario partida | 2 | ROUND_HALF_UP |
| Importe de partida | 2 | ROUND_HALF_UP |
Subtotal Ʃ / Porcentaje % |
2 | ROUND_HALF_UP |
Recálculo bottom-up
Sección titulada «Recálculo bottom-up»El endpoint POST /api/v1/budgets/{id}/recalculate ejecuta:
BudgetService.recalculate_totals(budget_id): recalcula importes desde las hojas hasta la raíz.BudgetPricingService.recalcular_precios_budget(budget_id): recalcula precios de venta.
Se invoca también tras editar mediciones, descompuestos o variables.
Tests de referencia
Sección titulada «Tests de referencia»backend/tests/test_budget_recalc_conceptos.pybackend/tests/test_budget_recalc_conceptos_scope.pybackend/tests/test_budget_rounding.py
Ejecutar:
cd backenduv run pytest backend/tests/test_budget_recalc_conceptos.py backend/tests/test_budget_recalc_conceptos_scope.py backend/tests/test_budget_rounding.py -qLecturas recomendadas: docs/imitacion-presto/reglas-calculo-presto.md