v0.1.31 - Mix Editor multi edit

This commit is contained in:
2026-06-18 15:15:46 +12:00
parent 1062c038e8
commit 10722a65a6
9 changed files with 555 additions and 23 deletions
+39
View File
@@ -423,6 +423,45 @@ def update_editor_mix(
return _serialize_mix_row(mix, visible_count=visible_count, product_count=total)
@router.delete("/mixes/{mix_id}", status_code=204)
def delete_editor_mix(
mix_id: int,
session: AuthSession = Depends(_require_editor_session),
db: Session = Depends(get_db),
):
"""Delete a mix that no product depends on.
A product must reference a mix (`products.mix_id` is NOT NULL), so a mix that
still drives products can't be removed without orphaning them — those should
be marked inactive instead. The mix's own ingredient rows cascade away with
it via the `delete-orphan` relationship.
"""
mix = db.scalar(select(Mix).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id))
if mix is None:
raise HTTPException(status_code=404, detail="Mix not found")
product_total = (
db.scalar(
select(func.count())
.select_from(Product)
.where(Product.tenant_id == session.tenant_id, Product.mix_id == mix_id)
)
or 0
)
if product_total > 0:
raise HTTPException(
status_code=409,
detail=(
f"This mix has {product_total} linked product"
f"{'s' if product_total != 1 else ''}. Mark it inactive or remove its products first."
),
)
db.delete(mix)
db.commit()
return None
@router.get("/mixes/{mix_id}/ingredients", response_model=EditorMixFormulaRead)
def get_editor_mix_ingredients(
mix_id: int,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hunter-backend"
version = "0.1.30"
version = "0.1.31"
description = "Costing platform MVP backend (API for Hunter)"
requires-python = ">=3.11"
dependencies = [
+46 -1
View File
@@ -10,8 +10,11 @@ from __future__ import annotations
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
import pytest
from fastapi import HTTPException
from app.api.deps import AuthSession
from app.api.editor import replace_editor_mix_formula
from app.api.editor import delete_editor_mix, replace_editor_mix_formula
from app.db.session import Base
from app.models.mix import Mix, MixIngredient
from app.models.product import Product, ProductIngredient
@@ -175,6 +178,48 @@ def test_replace_product_formula_writes_product_ingredients():
]
def test_delete_mix_without_products_removes_mix_and_ingredients():
"""A product-less mix can be deleted; its ingredient rows cascade away."""
db = _session()
maize = _raw(db, "Maize")
mix = Mix(tenant_id=TENANT, client_name="Hunter", name="Plain Mix")
db.add(mix)
db.flush()
db.add(MixIngredient(tenant_id=TENANT, mix_id=mix.id, raw_material_id=maize.id, quantity_kg=50))
db.commit()
mix_id = mix.id
delete_editor_mix(mix_id, session=_editor_session(), db=db)
assert db.scalar(select(Mix).where(Mix.id == mix_id)) is None
assert db.scalars(select(MixIngredient).where(MixIngredient.mix_id == mix_id)).first() is None
def test_delete_mix_with_products_is_refused():
"""A mix that still drives products can't be deleted (409) — products must
keep a mix, so the user marks it inactive instead."""
db = _session()
mix = Mix(tenant_id=TENANT, client_name="Hunter", name="Layer Mix")
db.add(mix)
db.flush()
db.add(
Product(
tenant_id=TENANT, client_name="Hunter", name="Layer 20kg", mix_id=mix.id,
unit_of_measure="20kg bag", visible=True,
)
)
db.commit()
mix_id = mix.id
with pytest.raises(HTTPException) as excinfo:
delete_editor_mix(mix_id, session=_editor_session(), db=db)
assert excinfo.value.status_code == 409
assert "linked product" in excinfo.value.detail
# The mix is left intact.
assert db.scalar(select(Mix).where(Mix.id == mix_id)) is not None
def test_representative_product_prefers_20kg_bag():
db = _session()
maize = _raw(db, "Maize")