Files
Vorrania/backend/app/routers/stock.py
Scarriffle 4c17705290 Einlagern-UX: 3-Wege-Barcode, Inline-Anlage aus OFF, Mehr-Chargen mit MHD
Einlagern:
- Barcode-Erkennung dreiteilig: bekannt -> direkt Menge; unbekannt aber in OFF
  -> "Anlegen & einlagern" inline; gar nicht gefunden -> manuell anlegen.
- Mehrere Chargen pro Vorgang: je Zeile eigene Menge + eigenes MHD
  (z.B. 5 Glaeser mit unterschiedlichen Daten). Neuer Endpoint /stock/checkin/batch.

Produkte/OFF:
- OFF-Fuellmenge (quantity) wird in Basiseinheit + Packungsgroesse geparst
  (kg->g, l->ml, cl/dl); parse_quantity + Tests.
- Produktformular uebernimmt Barcode aus ?barcode= und schlaegt automatisch nach,
  fuellt Packungsgroesse; gemeinsame offUtils (guessGroup/suggestionToProduct).

Lagerorte:
- Baum jetzt rekursiv ueber beliebig viele Ebenen (Schrank->Fach->Kiste->...).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:40:54 +02:00

125 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..crud import resolve_product
from ..database import get_db
from ..deps import get_current_user
from ..models import Lot, User
from ..schemas import (
BatchCheckInRequest,
BatchCheckInResponse,
CheckInRequest,
CheckInResponse,
CheckOutRequest,
CheckOutResponse,
LotOut,
)
from ..services.stock import StockError, check_in, check_out, current_stock
from ..services.units import UnitError
router = APIRouter(tags=["stock"])
@router.post("/stock/checkin", response_model=CheckInResponse)
def stock_checkin(
payload: CheckInRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> CheckInResponse:
product = resolve_product(db, payload.product_id, payload.barcode)
try:
lot = check_in(
db,
product=product,
quantity=payload.quantity,
unit=payload.unit,
best_before=payload.best_before,
location_id=payload.location_id,
user=user,
note=payload.note,
)
except UnitError as exc:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
db.commit()
db.refresh(lot)
return CheckInResponse(
lot=LotOut.model_validate(lot), product_stock=current_stock(db, product.id)
)
@router.post("/stock/checkin/batch", response_model=BatchCheckInResponse)
def stock_checkin_batch(
payload: BatchCheckInRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> BatchCheckInResponse:
"""Mehrere Chargen desselben Produkts in einem Vorgang einlagern.
Jede Zeile erzeugt eine eigene Charge mit eigener Menge und eigenem MHD
z.B. 5 Gläser mit unterschiedlichen Mindesthaltbarkeitsdaten.
"""
product = resolve_product(db, payload.product_id, payload.barcode)
lots: list[Lot] = []
try:
for line in payload.lines:
lots.append(
check_in(
db,
product=product,
quantity=line.quantity,
unit=payload.unit,
best_before=line.best_before,
location_id=line.location_id,
user=user,
note=payload.note,
)
)
except UnitError as exc:
db.rollback()
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
db.commit()
for lot in lots:
db.refresh(lot)
return BatchCheckInResponse(
lots=[LotOut.model_validate(lot) for lot in lots],
product_stock=current_stock(db, product.id),
)
@router.post("/stock/checkout", response_model=CheckOutResponse)
def stock_checkout(
payload: CheckOutRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> CheckOutResponse:
product = resolve_product(db, payload.product_id, payload.barcode)
try:
affected = check_out(
db,
product=product,
quantity=payload.quantity,
unit=payload.unit,
user=user,
note=payload.note,
)
except UnitError as exc:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
except StockError as exc:
raise HTTPException(status.HTTP_409_CONFLICT, str(exc)) from exc
db.commit()
return CheckOutResponse(
affected_lots=affected, product_stock=current_stock(db, product.id)
)
@router.get("/lots", response_model=list[LotOut])
def list_lots(
product_id: int | None = None,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[Lot]:
query = db.query(Lot)
if product_id is not None:
query = query.filter(Lot.product_id == product_id)
return query.order_by(Lot.best_before.is_(None), Lot.best_before).all()