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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user