v0.1.31 - Mix Editor multi edit
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user