Mehrere EAN-Codes je Produkt und je Gruppe (mit Notiz) + Gruppen umbenennen

Barcodes:
- Neue Tabelle barcodes (code, note, product_id ODER group_id). Damit lassen sich
  einem Produkt mehrere Codes geben und einer Gruppe beliebig viele Marken
  zuordnen (z.B. alle Mehlmarken zu "Mehl"). Je Code eine Notiz wie
  "Mehl bei Aldi".
- Lookup loest jetzt auch Alias-Codes auf; ist ein Code einer Gruppe zugeordnet,
  liefert die API group_id/group_name zurueck. Beim Anlegen aus einem Scan wird
  diese Gruppe automatisch gesetzt (Vorrang vor dem Kategorie-Rateversuch).
- Endpunkte: POST/DELETE /products/{id}/barcodes und /groups/{id}/barcodes.
- Web: wiederverwendbare BarcodeList-Komponente, eingebunden im Produktformular
  und auf der Gruppen-Seite.

Gruppen:
- Namen lassen sich jetzt direkt in der Tabelle aendern (PATCH war vorhanden,
  nur die Oberflaeche fehlte).

UI-Fix: Auswahlfelder in Tabellen richten sich nach ihrem Text statt nach der
Zellenbreite (Gruppen-/Benutzer-Dropdowns waren abgeschnitten).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 14:48:14 +02:00
parent 8079d2d3f0
commit de6c8632a1
12 changed files with 341 additions and 48 deletions

View File

@@ -3,8 +3,8 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Group, Product, User
from ..schemas import GroupCreate, GroupOut, GroupUpdate
from ..models import Barcode, Group, Product, User
from ..schemas import BarcodeCreate, BarcodeOut, GroupCreate, GroupOut, GroupUpdate
from ..services.conversion import BASE_OF_KIND
from ..services.stock import current_stock
@@ -15,6 +15,10 @@ def _group_to_out(db: Session, group: Group) -> GroupOut:
out = GroupOut.model_validate(group)
products = db.query(Product).filter(Product.group_id == group.id).all()
out.product_count = len(products)
out.barcodes = [
BarcodeOut.model_validate(b)
for b in db.query(Barcode).filter(Barcode.group_id == group.id).order_by(Barcode.id).all()
]
unit = group.min_stock_unit
if unit is not None:
# Bestand nur über Produkte gleicher Art, in der Gruppen-Einheit ausgedrückt.
@@ -82,6 +86,44 @@ def update_group(
return _group_to_out(db, group)
@router.post("/{group_id}/barcodes", response_model=GroupOut, status_code=status.HTTP_201_CREATED)
def add_group_barcode(
group_id: int,
payload: BarcodeCreate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> GroupOut:
"""EAN einer Gruppe zuordnen (z.B. alle Mehl-Marken zur Gruppe "Mehl")."""
group = db.get(Group, group_id)
if group is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
code = payload.code.strip()
if db.query(Barcode).filter(Barcode.code == code).first() or (
db.query(Product).filter(Product.barcode == code).first()
):
raise HTTPException(status.HTTP_409_CONFLICT, "Dieser Code ist bereits vergeben")
db.add(Barcode(code=code, note=(payload.note or None), group_id=group.id))
db.commit()
db.refresh(group)
return _group_to_out(db, group)
@router.delete("/{group_id}/barcodes/{code}", status_code=status.HTTP_204_NO_CONTENT)
def delete_group_barcode(
group_id: int,
code: str,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> None:
entry = (
db.query(Barcode).filter(Barcode.group_id == group_id, Barcode.code == code).first()
)
if entry is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Code nicht gefunden")
db.delete(entry)
db.commit()
@router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_group(
group_id: int,