Files
Vorrania/backend/app/services/zuordnung.py
T
ScarriffleandClaude Opus 5 8072e7f566 Zutaten von Hand zuordnen - und Vorrania merkt es sich
Der unscharfe Abgleich muss raten und wird das immer tun: "Lime Curd" hat
mit "Zitronencreme" kein Wort gemeinsam, und manchmal liegt er daneben.
Bisher war das eine Sackgasse - die Zutat blieb unzugeordnet, bei jedem
Aufruf neu.

Jetzt traegt jede Zutatenzeile einen Knopf "Zuordnen". Entschieden wird
gegen Artikel UND Gruppen, gemerkt unter dem normalisierten Namen des
Lebensmittels: "Schlagsahne", "schlagsahne" und "SCHLAGSAHNE " treffen
dieselbe Zeile. Ab dann gilt die Entscheidung fuer jedes Rezept, in dem
diese Zutat vorkommt - eine Zutat wird hoechstens einmal zugeordnet.

Schluessel ist der Name, nicht die Mealie-ID: IDs sind UUIDs, die beim
Neuanlegen eines Lebensmittels wechseln, und nur ueber den Namen greift
die Zuordnung auch fuer Zutaten ohne verknuepftes Lebensmittel ("2 Eier").

Im Auswahldialog stehen oben die Vorschlaege, die der Abgleich selbst
gefunden hat - meist ist der Richtige darunter, nur nicht an erster
Stelle -, darunter die freie Suche ueber alles. Die Basiseinheit steht
dabei, damit nicht wieder Milliliter auf ein Gramm-Produkt wandern.

Beim Raten bleiben Gegenstaende aussen vor; von Hand darf alles gewaehlt
werden ausser Einzelstuecken. Backpapier ist kein Lebensmittel und steht
trotzdem in Rezepten, waehrend ein Einzelstueck nie eine Zutat ist.

Zeigt das Ziel ins Leere, weil der Artikel geloescht wurde, faellt die
Zeile beim Laden still heraus: Auf SQLite greift kein Fremdschluessel,
und ein geloeschter Artikel darf kein Rezept mehr sprengen.

Die Zuordnungs-Routen stehen bewusst vor /recipes/{slug}, sonst liest
FastAPI "zuordnungen" als Rezept-Slug und fragt Mealie danach. Ein Test
wacht ueber die Reihenfolge, weil ein Umsortieren der Datei den Fehler
sonst still einbauen wuerde.

16 neue Tests, Suite bei 487.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 13:40:23 +02:00

95 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Gemerkte Zuordnungen: Mealie-Lebensmittel → eigener Artikel oder Gruppe.
Der unscharfe Abgleich in :mod:`services.rezepte` ist eine Heuristik und wird
immer Fälle haben, die er nicht erraten kann — „Lime Curd", „Ras el Hanout",
oder einfach ein Hausname. Hier liegt der Korrekturweg: Einmal von Hand
zugeordnet, gilt es für jedes Rezept, in dem dasselbe Lebensmittel vorkommt.
Der Schlüssel ist der normalisierte Name, nicht die Mealie-ID. Mealie-IDs sind
UUIDs, die beim Neuanlegen eines Lebensmittels wechseln; der Name bleibt. Und
nur über den Namen greift die Zuordnung auch für Zutaten, die in Mealie gar
kein verknüpftes Lebensmittel haben (freier Text „2 Eier").
"""
from __future__ import annotations
from sqlalchemy.orm import Session
from ..models import Group, MealieFoodMap, Product
from .matching import normalisieren
class ZuordnungFehler(ValueError):
"""Unbrauchbare Zuordnung – der Router macht daraus eine 400/404."""
def schluessel(name: str) -> str:
"""Normalisierter Schlüssel. Leer heisst „damit lässt sich nichts merken"."""
return normalisieren(name)[:200]
def lade(db: Session) -> dict[str, tuple[str, int]]:
"""Alle gültigen Zuordnungen als ``{schluessel: (art, id)}``.
Zeilen, deren Ziel es nicht mehr gibt, fallen hier still heraus statt eine
Ausnahme zu werfen: Auf SQLite greift kein Fremdschlüssel, und ein
gelöschter Artikel darf nicht dazu führen, dass sich kein Rezept mehr
öffnen lässt.
"""
ergebnis: dict[str, tuple[str, int]] = {}
for zeile in db.query(MealieFoodMap).all():
if zeile.product_id is not None:
if db.get(Product, zeile.product_id) is not None:
ergebnis[zeile.food_key] = ("product", zeile.product_id)
elif zeile.group_id is not None:
if db.get(Group, zeile.group_id) is not None:
ergebnis[zeile.food_key] = ("group", zeile.group_id)
return ergebnis
def ziel_name(db: Session, art: str, ziel_id: int) -> str:
if art == "product":
produkt = db.get(Product, ziel_id)
return produkt.name if produkt is not None else ""
gruppe = db.get(Group, ziel_id)
return gruppe.name if gruppe is not None else ""
def setze(db: Session, food: str, art: str, ziel_id: int) -> MealieFoodMap:
"""Zuordnung anlegen oder ändern. Prüft, dass das Ziel existiert."""
key = schluessel(food)
if not key:
raise ZuordnungFehler(
"Zu dieser Zutat steht kein Name da – ohne Namen lässt sich die "
"Zuordnung nicht merken."
)
if art not in ("product", "group"):
raise ZuordnungFehler("Zugeordnet wird auf einen Artikel oder eine Gruppe.")
if art == "product" and db.get(Product, ziel_id) is None:
raise ZuordnungFehler("Diesen Artikel gibt es nicht.")
if art == "group" and db.get(Group, ziel_id) is None:
raise ZuordnungFehler("Diese Gruppe gibt es nicht.")
zeile = db.get(MealieFoodMap, key)
if zeile is None:
zeile = MealieFoodMap(food_key=key)
db.add(zeile)
# Immer beide Felder setzen, damit ein Wechsel Artikel → Gruppe nicht
# beide gefüllt zurücklässt.
zeile.food_name = (food or "").strip()[:200]
zeile.product_id = ziel_id if art == "product" else None
zeile.group_id = ziel_id if art == "group" else None
db.commit()
db.refresh(zeile)
return zeile
def loesche(db: Session, food: str) -> bool:
"""Zuordnung vergessen. ``False``, wenn es keine gab."""
zeile = db.get(MealieFoodMap, schluessel(food))
if zeile is None:
return False
db.delete(zeile)
db.commit()
return True