Backend: Mindestbestand je Lagerort für Produkte und Gruppen

Zusaetzlich zum globalen Mindestbestand: je Produkt und je Gruppe laesst sich pro
Lagerort ein Mindestbestand (in Artikeleinheiten) hinterlegen.
- Neue Tabellen product_location_min_stock / group_location_min_stock.
- PUT /products/{id}/location-min-stock und /groups/{id}/location-min-stock
  ersetzen die Eintraege; Produkt-/Gruppen-Ausgabe liefert sie mit.
- Neue Einkaufsliste GET /shopping-list/by-location: Bedarfe je Ort (Produkte +
  Gruppen), Bestand-am-Ort gegen Mindestbestand-am-Ort.
- Helfer location_stock_base (Bestand je Ort). 4 neue Tests, Suite 144 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 06:58:08 +02:00
parent 2bc871f229
commit d0d854be5a
8 changed files with 343 additions and 6 deletions

View File

@@ -3,7 +3,7 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Barcode, Group, Product, User
from ..models import Barcode, Group, GroupLocationMinStock, Location, Product, User
from ..schemas import (
BarcodeCreate,
BarcodeNoteUpdate,
@@ -11,6 +11,8 @@ from ..schemas import (
GroupCreate,
GroupOut,
GroupUpdate,
LocationMinStockIn,
LocationMinStockOut,
ProductBarcodeOut,
)
from ..services.conversion import BASE_OF_KIND
@@ -76,6 +78,15 @@ def _group_to_out(db: Session, group: Group) -> GroupOut:
out.kind = unit.kind.value
else:
out.stock = float(sum(current_stock(db, p.id) for p in products))
out.location_min_stocks = [
LocationMinStockOut(
location_id=e.location_id,
location_name=e.location.name if e.location else None,
min_stock=e.min_stock,
)
for e in sorted(group.location_min_stocks, key=lambda x: x.id)
]
return out
@@ -87,6 +98,38 @@ def list_groups(
return [_group_to_out(db, g) for g in groups]
@router.put("/{group_id}/location-min-stock", response_model=GroupOut)
def set_group_location_min_stock(
group_id: int,
payload: list[LocationMinStockIn],
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> GroupOut:
"""Gruppen-Mindestbestände je Lagerort ersetzen (Menge 0 = Eintrag entfällt)."""
group = db.get(Group, group_id)
if group is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
db.query(GroupLocationMinStock).filter(
GroupLocationMinStock.group_id == group_id
).delete()
gesehen: set[int] = set()
for eintrag in payload:
if eintrag.min_stock <= 0 or eintrag.location_id in gesehen:
continue
if db.get(Location, eintrag.location_id) is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
gesehen.add(eintrag.location_id)
db.add(GroupLocationMinStock(
group_id=group_id,
location_id=eintrag.location_id,
min_stock=eintrag.min_stock,
))
db.commit()
db.refresh(group)
return _group_to_out(db, group)
@router.post("", response_model=GroupOut, status_code=status.HTTP_201_CREATED)
def create_group(
payload: GroupCreate,