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:
143
backend/app/routers/field_definitions.py
Normal file
143
backend/app/routers/field_definitions.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Verwaltung der selbst definierten Felder je Kategorie."""
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..deps import get_current_user, require_admin
|
||||
from ..models import Category, FieldDefinition, FieldType, ProductFieldValue, User
|
||||
from ..schemas import FieldDefinitionCreate, FieldDefinitionOut, FieldDefinitionUpdate
|
||||
from ..services.fields import options_list, slugify
|
||||
|
||||
router = APIRouter(prefix="/field-definitions", tags=["field-definitions"])
|
||||
|
||||
|
||||
def to_out(fd: FieldDefinition, inherited: bool = False) -> FieldDefinitionOut:
|
||||
return FieldDefinitionOut(
|
||||
id=fd.id,
|
||||
category_id=fd.category_id,
|
||||
label=fd.label,
|
||||
key=fd.key,
|
||||
field_type=FieldType(fd.field_type),
|
||||
unit=fd.unit,
|
||||
options=options_list(fd),
|
||||
required=fd.required,
|
||||
position=fd.position,
|
||||
is_builtin=fd.is_builtin,
|
||||
inherited=inherited,
|
||||
)
|
||||
|
||||
|
||||
def _unique_key(db: Session, category_id: int, label: str, exclude_id: int | None = None) -> str:
|
||||
"""Eindeutigen Schlüssel je Kategorie erzeugen (kapazitaet, kapazitaet_2, …)."""
|
||||
basis = slugify(label)
|
||||
kandidat = basis
|
||||
n = 1
|
||||
while True:
|
||||
query = db.query(FieldDefinition).filter(
|
||||
FieldDefinition.category_id == category_id, FieldDefinition.key == kandidat
|
||||
)
|
||||
if exclude_id is not None:
|
||||
query = query.filter(FieldDefinition.id != exclude_id)
|
||||
if query.first() is None:
|
||||
return kandidat
|
||||
n += 1
|
||||
kandidat = f"{basis}_{n}"
|
||||
|
||||
|
||||
def _options_json(field_type: FieldType, options: list[str] | None) -> str | None:
|
||||
if field_type != FieldType.select:
|
||||
return None
|
||||
return json.dumps([o.strip() for o in (options or []) if o.strip()])
|
||||
|
||||
|
||||
@router.get("", response_model=list[FieldDefinitionOut])
|
||||
def list_field_definitions(
|
||||
category_id: int | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_user),
|
||||
) -> list[FieldDefinitionOut]:
|
||||
"""Felder auflisten – ohne category_id alle, sonst nur die dieser Kategorie."""
|
||||
query = db.query(FieldDefinition)
|
||||
if category_id is not None:
|
||||
query = query.filter(FieldDefinition.category_id == category_id)
|
||||
rows = query.order_by(
|
||||
FieldDefinition.category_id, FieldDefinition.position, FieldDefinition.id
|
||||
).all()
|
||||
return [to_out(fd) for fd in rows]
|
||||
|
||||
|
||||
@router.post("", response_model=FieldDefinitionOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_field_definition(
|
||||
payload: FieldDefinitionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> FieldDefinitionOut:
|
||||
if db.get(Category, payload.category_id) is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Kategorie nicht gefunden")
|
||||
fd = FieldDefinition(
|
||||
category_id=payload.category_id,
|
||||
label=payload.label.strip(),
|
||||
key=_unique_key(db, payload.category_id, payload.label),
|
||||
field_type=payload.field_type.value,
|
||||
unit=(payload.unit or None),
|
||||
options=_options_json(payload.field_type, payload.options),
|
||||
required=bool(payload.required),
|
||||
position=payload.position,
|
||||
)
|
||||
db.add(fd)
|
||||
db.commit()
|
||||
db.refresh(fd)
|
||||
return to_out(fd)
|
||||
|
||||
|
||||
@router.patch("/{field_id}", response_model=FieldDefinitionOut)
|
||||
def update_field_definition(
|
||||
field_id: int,
|
||||
payload: FieldDefinitionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> FieldDefinitionOut:
|
||||
fd = db.get(FieldDefinition, field_id)
|
||||
if fd is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Feld nicht gefunden")
|
||||
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
if "label" in data and data["label"]:
|
||||
fd.label = data["label"].strip()
|
||||
fd.key = _unique_key(db, fd.category_id, fd.label, exclude_id=fd.id)
|
||||
if "field_type" in data and data["field_type"] is not None:
|
||||
fd.field_type = data["field_type"].value
|
||||
if "unit" in data:
|
||||
fd.unit = data["unit"] or None
|
||||
if "required" in data and data["required"] is not None:
|
||||
fd.required = bool(data["required"])
|
||||
if "position" in data and data["position"] is not None:
|
||||
fd.position = data["position"]
|
||||
# Optionen immer passend zum (ggf. neuen) Typ ablegen.
|
||||
if "options" in data or "field_type" in data:
|
||||
neuer_typ = FieldType(fd.field_type)
|
||||
optionen = data["options"] if "options" in data else options_list(fd)
|
||||
fd.options = _options_json(neuer_typ, optionen)
|
||||
db.commit()
|
||||
db.refresh(fd)
|
||||
return to_out(fd)
|
||||
|
||||
|
||||
@router.delete("/{field_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_field_definition(
|
||||
field_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> None:
|
||||
"""Löscht das Feld samt aller dazu erfassten Werte (ON DELETE CASCADE)."""
|
||||
fd = db.get(FieldDefinition, field_id)
|
||||
if fd is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Feld nicht gefunden")
|
||||
db.query(ProductFieldValue).filter(
|
||||
ProductFieldValue.field_definition_id == field_id
|
||||
).delete()
|
||||
db.delete(fd)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user