Gebinde verwalten - mit Einzahl und Mehrzahl

Bisher war die Auswahl eine fest im Frontend verdrahtete Liste und es gab nur
eine Form: ueberall stand "3 Glas".

Neu unter Verwaltung > Gebinde: anlegen, umbenennen, loeschen, jeweils mit
Einzahl und Mehrzahl. Die eingebauten Gebinde lassen sich in der Schreibweise
aendern, aber nicht loeschen; ein Gebinde, das ein Artikel verwendet, ebenfalls
nicht.

Der Artikel speichert weiterhin nur die Einzahl als Text - so bleiben
vorhandene Artikel, Sicherungen und CSV-Dateien gueltig, und eine unbekannte
Bezeichnung faellt schlicht auf die Einzahl zurueck. Deshalb zieht ein
Umbenennen die Artikel mit; sonst zeigten sie auf eine Bezeichnung, die es
nicht mehr gibt.

Die Mehrzahl greift jetzt in Artikelliste, Artikelseite (Chargen und Gebinde-
Auswahl), Auslagern und in allen Ablauf- und Einkaufslisten samt Startseite.
Einheiten wie Gramm oder Liter bleiben unveraendert - die haben im Deutschen
keine Mehrzahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 14:24:42 +02:00
parent 0ba968985f
commit fd9045226b
14 changed files with 511 additions and 22 deletions

View File

@@ -0,0 +1,102 @@
"""Gebinde-Bezeichnungen mit Einzahl und Mehrzahl.
Der Artikel speichert nur die Einzahl als Text; hier steht die passende
Mehrzahl. Deshalb zieht ein Umbenennen die Artikel mit: Wird "Glas" zu
"Konservenglas", laufen die Artikel sonst auf eine Bezeichnung, die es nicht
mehr gibt, und fielen stillschweigend auf die Einzahl zurück.
"""
from __future__ import annotations
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 PackageType, Product, User
from ..schemas import PackageTypeCreate, PackageTypeOut, PackageTypeUpdate
router = APIRouter(prefix="/package-types", tags=["package-types"])
def _doppelt(db: Session, singular: str, ausser_id: int | None = None) -> bool:
query = db.query(PackageType).filter(func.lower(PackageType.singular) == singular.lower())
if ausser_id is not None:
query = query.filter(PackageType.id != ausser_id)
return query.first() is not None
@router.get("", response_model=list[PackageTypeOut])
def list_package_types(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[PackageType]:
return db.query(PackageType).order_by(PackageType.singular).all()
@router.post("", response_model=PackageTypeOut, status_code=status.HTTP_201_CREATED)
def create_package_type(
payload: PackageTypeCreate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> PackageType:
singular = payload.singular.strip()
if _doppelt(db, singular):
raise HTTPException(status.HTTP_409_CONFLICT, "Dieses Gebinde gibt es bereits")
eintrag = PackageType(
singular=singular, plural=payload.plural.strip() or singular, is_builtin=False
)
db.add(eintrag)
db.commit()
db.refresh(eintrag)
return eintrag
@router.patch("/{type_id}", response_model=PackageTypeOut)
def update_package_type(
type_id: int,
payload: PackageTypeUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> PackageType:
eintrag = db.get(PackageType, type_id)
if eintrag is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gebinde nicht gefunden")
daten = payload.model_dump(exclude_unset=True)
neue_einzahl = (daten.get("singular") or "").strip()
if neue_einzahl and neue_einzahl != eintrag.singular:
if _doppelt(db, neue_einzahl, type_id):
raise HTTPException(status.HTTP_409_CONFLICT, "Dieses Gebinde gibt es bereits")
# Artikel mitziehen - sie verweisen ueber den Text, nicht ueber eine ID.
db.query(Product).filter(Product.package_label == eintrag.singular).update(
{Product.package_label: neue_einzahl}, synchronize_session=False
)
eintrag.singular = neue_einzahl
if daten.get("plural"):
eintrag.plural = daten["plural"].strip()
db.commit()
db.refresh(eintrag)
return eintrag
@router.delete("/{type_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_package_type(
type_id: int,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> None:
eintrag = db.get(PackageType, type_id)
if eintrag is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gebinde nicht gefunden")
if eintrag.is_builtin:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Eingebaute Gebinde können nicht gelöscht werden"
)
benutzt = db.query(Product).filter(Product.package_label == eintrag.singular).first()
if benutzt:
raise HTTPException(
status.HTTP_409_CONFLICT, "Dieses Gebinde wird noch von Artikeln verwendet"
)
db.delete(eintrag)
db.commit()