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:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

View File

@@ -42,15 +42,16 @@ def parse_quantity(quantity: str | None) -> tuple[str, float | None]:
return base_unit, (package_size if package_size > 0 else None)
def lookup_barcode(barcode: str) -> dict | None:
"""Fragt Open Food Facts nach einem Barcode.
def _lookup_at(barcode: str, base_url: str, source: str) -> dict | None:
"""Fragt eine Open-Facts-Instanz (OFF oder Open Products Facts) nach einem Barcode.
Gibt ein vorbefülltes Produkt-Dict zurück oder None, wenn nicht gefunden.
Beide Dienste teilen sich dieselbe API-Struktur; es unterscheidet sich nur die
Basis-URL. Gibt ein vorbefülltes Produkt-Dict zurück oder None.
"""
# v0-API: liefert bei Treffer status=1 + product, bei Nicht-Treffer status=0
# (HTTP 200). Die v2-API hat kein status-Feld und antwortet mit HTTP 404,
# weshalb wir bewusst v0 nutzen.
url = f"{settings.off_base_url}/api/v0/product/{barcode}.json"
url = f"{base_url}/api/v0/product/{barcode}.json"
try:
resp = httpx.get(
url,
@@ -96,6 +97,26 @@ def lookup_barcode(barcode: str) -> dict | None:
"quantity_text": product.get("quantity"),
"category_suggestion": categories.split(",")[0].strip() if categories else None,
"category_tags": category_tags,
"source": "off",
"source": source,
"off_raw": json.dumps(product)[:20000],
}
def lookup_barcode(barcode: str, prefer: str | None = None) -> dict | None:
"""Sucht einen Barcode in den offenen Datenbanken.
Beim Scannen steht die Kategorie noch nicht fest, deshalb werden beide Quellen
der Reihe nach befragt: Lebensmittel (Open Food Facts) und allgemeine Produkte
(Open Products Facts). ``prefer="object"`` stellt die allgemeine Produkt-DB nach
vorn (z.B. beim erneuten Abgleich eines Gegenstands), sonst gewinnt Essen.
"""
off = (settings.off_base_url, "off")
opf = (settings.opf_base_url, "opf")
reihenfolge = [opf, off] if prefer == "object" else [off, opf]
for base_url, source in reihenfolge:
if not base_url:
continue
treffer = _lookup_at(barcode, base_url, source)
if treffer is not None:
return treffer
return None