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:
88
backend/app/routers/shops.py
Normal file
88
backend/app/routers/shops.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Shops / Bezugsquellen: verwaltbare Liste für „gekauft bei“ (nur Gegenstände)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..deps import get_current_user, require_admin
|
||||
from ..models import Product, Shop, User
|
||||
from ..schemas import ShopCreate, ShopOut, ShopUpdate
|
||||
|
||||
router = APIRouter(prefix="/shops", tags=["shops"])
|
||||
|
||||
|
||||
def _to_out(db: Session, shop: Shop) -> ShopOut:
|
||||
out = ShopOut.model_validate(shop)
|
||||
out.product_count = db.query(Product).filter(Product.shop_id == shop.id).count()
|
||||
return out
|
||||
|
||||
|
||||
@router.get("", response_model=list[ShopOut])
|
||||
def list_shops(
|
||||
db: Session = Depends(get_db), _: User = Depends(get_current_user)
|
||||
) -> list[ShopOut]:
|
||||
return [_to_out(db, s) for s in db.query(Shop).order_by(Shop.name).all()]
|
||||
|
||||
|
||||
@router.post("", response_model=ShopOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_shop(
|
||||
payload: ShopCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> ShopOut:
|
||||
name = payload.name.strip()
|
||||
if db.query(Shop).filter(func.lower(Shop.name) == name.lower()).first():
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Diesen Shop gibt es bereits")
|
||||
shop = Shop(name=name, website=(payload.website or None))
|
||||
db.add(shop)
|
||||
db.commit()
|
||||
db.refresh(shop)
|
||||
return _to_out(db, shop)
|
||||
|
||||
|
||||
@router.patch("/{shop_id}", response_model=ShopOut)
|
||||
def update_shop(
|
||||
shop_id: int,
|
||||
payload: ShopUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> ShopOut:
|
||||
shop = db.get(Shop, shop_id)
|
||||
if shop is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Shop nicht gefunden")
|
||||
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
if "name" in data and data["name"]:
|
||||
name = data["name"].strip()
|
||||
doppelt = (
|
||||
db.query(Shop)
|
||||
.filter(func.lower(Shop.name) == name.lower(), Shop.id != shop_id)
|
||||
.first()
|
||||
)
|
||||
if doppelt is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Diesen Shop gibt es bereits")
|
||||
shop.name = name
|
||||
if "website" in data:
|
||||
shop.website = data["website"] or None
|
||||
db.commit()
|
||||
db.refresh(shop)
|
||||
return _to_out(db, shop)
|
||||
|
||||
|
||||
@router.delete("/{shop_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_shop(
|
||||
shop_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> None:
|
||||
"""Artikel bleiben bestehen, sie verlieren nur die Bezugsquelle (SET NULL)."""
|
||||
shop = db.get(Shop, shop_id)
|
||||
if shop is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Shop nicht gefunden")
|
||||
# SQLite setzt Fremdschlüssel nicht ohne Weiteres um, deshalb ausdrücklich.
|
||||
db.query(Product).filter(Product.shop_id == shop_id).update(
|
||||
{Product.shop_id: None}
|
||||
)
|
||||
db.delete(shop)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user