Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln

Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

View File

@@ -1,10 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..crud import resolve_product
from ..crud import product_tracking, resolve_product
from ..database import get_db
from ..deps import get_current_user
from ..models import Lot, Movement, MovementType, User
from ..models import CategoryTracking, Lot, Movement, MovementType, User
from ..schemas import (
BatchCheckInRequest,
BatchCheckInResponse,
@@ -14,6 +14,9 @@ from ..schemas import (
CheckOutResponse,
LotOut,
LotUpdate,
RelocateRequest,
RemoveRequest,
StockActionResponse,
)
from ..services.conversion import ConversionError
from ..services.dates import clean_precision, normalize_best_before
@@ -23,10 +26,15 @@ from ..services.stock import (
check_out,
check_out_lot,
current_stock,
object_add,
object_relocate,
object_remove,
)
router = APIRouter(tags=["stock"])
OBJECT = CategoryTracking.object.value
@router.post("/stock/checkin", response_model=CheckInResponse)
def stock_checkin(
@@ -35,6 +43,21 @@ def stock_checkin(
user: User = Depends(get_current_user),
) -> CheckInResponse:
product = resolve_product(db, payload.product_id, payload.barcode)
if product_tracking(db, product) == OBJECT:
# Gegenstände: Menge am Lagerort erhöhen, kein MHD, keine Charge-Auswahl.
lot = object_add(
db,
product=product,
quantity=payload.quantity,
location_id=payload.location_id,
user=user,
note=payload.note,
)
db.commit()
db.refresh(lot)
return CheckInResponse(
lot=LotOut.model_validate(lot), product_stock=current_stock(db, product.id)
)
try:
lot = check_in(
db,
@@ -103,6 +126,12 @@ def stock_checkout(
user: User = Depends(get_current_user),
) -> CheckOutResponse:
product = resolve_product(db, payload.product_id, payload.barcode)
if product_tracking(db, product) == OBJECT:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Gegenstände werden über „Entfernen“ (mit Grund) oder „Umlagern“ gebucht, "
"nicht ausgecheckt.",
)
try:
if payload.lot_id is not None:
lot = db.get(Lot, payload.lot_id)
@@ -138,6 +167,63 @@ def stock_checkout(
)
@router.post("/stock/relocate", response_model=StockActionResponse)
def stock_relocate(
payload: RelocateRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> StockActionResponse:
"""Gegenstands-Menge von einem Lagerort zum anderen umbuchen (ohne Grund)."""
product = resolve_product(db, payload.product_id, payload.barcode)
if product_tracking(db, product) != OBJECT:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Umlagern gibt es nur für Gegenstände."
)
try:
object_relocate(
db,
product=product,
quantity=payload.quantity,
from_location_id=payload.from_location_id,
to_location_id=payload.to_location_id,
user=user,
note=payload.note,
)
except StockError as exc:
raise HTTPException(status.HTTP_409_CONFLICT, str(exc)) from exc
db.commit()
return StockActionResponse(product_stock=current_stock(db, product.id))
@router.post("/stock/remove", response_model=StockActionResponse)
def stock_remove(
payload: RemoveRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> StockActionResponse:
"""Gegenstands-Menge mit Pflicht-Grund aus dem Bestand entfernen."""
product = resolve_product(db, payload.product_id, payload.barcode)
if product_tracking(db, product) != OBJECT:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Entfernen mit Grund gibt es nur für Gegenstände.",
)
try:
object_remove(
db,
product=product,
quantity=payload.quantity,
location_id=payload.location_id,
reason=payload.reason.value,
user=user,
note=payload.note,
)
except StockError as exc:
raise HTTPException(status.HTTP_409_CONFLICT, str(exc)) from exc
db.commit()
return StockActionResponse(product_stock=current_stock(db, product.id))
@router.get("/lots", response_model=list[LotOut])
def list_lots(
product_id: int | None = None,