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>
125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
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()
|