Ir al contenido

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.

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:

@staticmethod
def detectar_tipo_concepto(codigo: str, unidad: str | None = None) -> str:
if codigo.startswith("Ʃ"):
return "subtotal"
if "%" in codigo or unidad == "%":
return "porcentaje"
return "partida"
  • Suma solo conceptos del mismo nivel (mismo parent_id).
  • “Anteriores” = orden menor 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.importe
  • 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.importe

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).

backend/app/repositories/budget_item_serialization.py:342 (_calc_partida):

  1. Si la partida no tiene descompuesto: importe = cantidad × precio_unitario.
  2. 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).

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).

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

El endpoint POST /api/v1/budgets/{id}/recalculate ejecuta:

  1. BudgetService.recalculate_totals(budget_id): recalcula importes desde las hojas hasta la raíz.
  2. BudgetPricingService.recalcular_precios_budget(budget_id): recalcula precios de venta.

Se invoca también tras editar mediciones, descompuestos o variables.

  • backend/tests/test_budget_recalc_conceptos.py
  • backend/tests/test_budget_recalc_conceptos_scope.py
  • backend/tests/test_budget_rounding.py

Ejecutar:

Ventana de terminal
cd backend
uv run pytest backend/tests/test_budget_recalc_conceptos.py backend/tests/test_budget_recalc_conceptos_scope.py backend/tests/test_budget_rounding.py -q

Lecturas recomendadas: docs/imitacion-presto/reglas-calculo-presto.md