Der Wortanker nahm jedes geteilte Wort ab vier Zeichen - also auch
"vegan". Damit passte jedes vegane Produkt auf jede vegane Zutat: "vegane
Butter" landete auf "veganes Muehlen Knusper Bleu", "veganer griechischer
Joghurt" auf "Vegane Muehlen Nuggets" und "veganer Lime Curd" auf "Vegane
Muehlen-Schnitzel auf Basis von Soja". Geteilt wurde dort nur die
Eigenschaft, nicht die Sache.
Beschreibende Woerter begruenden deshalb keinen Treffer mehr - vegan,
vegetarisch, bio, frisch, geschmolzen, ungesalzen, geschaelt, geschnitten,
Zimmertemperatur, oder, optional und so weiter, dazu reine Zahlen. Als
Wortanfaenge aufgefuehrt, weil Deutsch beugt ("vegan", "vegane",
"veganer", "veganes"). Sie sperren aber nicht: "vegane Butter" findet
weiterhin eine "Vegane Butter", nur eben ueber das Wort "Butter".
Wichtig war, die Liste auf BEIDEN Seiten anzuwenden. Sonst steckt "vegan"
weiter im Produktnamen und der Teilstring-Vergleich greift von dort aus.
Gegenprobe mit dem echten Katalog: Von zehn Zutaten bleiben die drei
richtigen Treffer (Mehl, Vollmilch, Aepfel), die sieben erfundenen sind
weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
391 lines
13 KiB
Python
391 lines
13 KiB
Python
"""Rezeptzutaten gegen den Bestand: Kann ich das kochen?
|
||
|
||
Ohne Mealie – die Zutaten stehen hier so im Test, wie Mealie sie liefern würde.
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from app.models import (
|
||
BaseUnit,
|
||
Category,
|
||
CategoryMeasure,
|
||
CategoryTracking,
|
||
Group,
|
||
Measure,
|
||
Product,
|
||
)
|
||
from app.services.rezepte import faktor_fuer_portionen, kochbar, pruefe_zutaten
|
||
from app.services.stock import check_in
|
||
|
||
|
||
def _zutat(name, menge=None, einheit=None, text=None):
|
||
"""Eine Zutat im Format von Mealie."""
|
||
return {
|
||
"quantity": menge,
|
||
"unit": {"name": einheit, "abbreviation": einheit} if einheit else None,
|
||
"food": {"name": name},
|
||
"display": text or f"{menge or ''} {einheit or ''} {name}".strip(),
|
||
}
|
||
|
||
|
||
def _kategorie(db, name):
|
||
cat = Category(name=name, tracking=CategoryTracking.food.value)
|
||
db.add(cat)
|
||
db.commit()
|
||
db.refresh(cat)
|
||
return cat
|
||
|
||
|
||
def _artikel(db, name, cat, base=BaseUnit.gram, package_size=1000, bestand=0.0):
|
||
product = Product(name=name, base_unit=base, package_size=package_size,
|
||
category_id=cat.id, source="manual")
|
||
db.add(product)
|
||
db.commit()
|
||
db.refresh(product)
|
||
if bestand:
|
||
check_in(db, product, bestand, base.value, None, None, None)
|
||
db.commit()
|
||
return product
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Portionen
|
||
# --------------------------------------------------------------------------
|
||
def test_portionen_skalieren_die_mengen(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=5000)
|
||
faktor, hinweis = faktor_fuer_portionen({"recipeServings": 4}, 6)
|
||
|
||
assert faktor == 1.5
|
||
assert hinweis == ""
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")], faktor=faktor)
|
||
assert zutat.menge == 300
|
||
assert zutat.benoetigt_base == 300
|
||
|
||
|
||
def test_ohne_portionsangabe_wird_nicht_skaliert(db):
|
||
faktor, hinweis = faktor_fuer_portionen({"recipeServings": 0}, 6)
|
||
|
||
assert faktor == 1.0
|
||
assert "keine Portionszahl" in hinweis
|
||
|
||
|
||
def test_ohne_wunsch_bleibt_es_wie_im_rezept(db):
|
||
assert faktor_fuer_portionen({"recipeServings": 4}, None) == (1.0, "")
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Reicht der Bestand?
|
||
# --------------------------------------------------------------------------
|
||
def test_genug_vorhanden(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")])
|
||
|
||
assert zutat.status == "vorhanden"
|
||
assert zutat.fehlt_base == 0
|
||
assert kochbar([zutat]) is True
|
||
|
||
|
||
def test_zu_wenig_meldet_die_fehlmenge(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=150)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 500, "g")])
|
||
|
||
assert zutat.status == "zu_wenig"
|
||
assert zutat.fehlt_base == 350
|
||
assert kochbar([zutat]) is False
|
||
|
||
|
||
def test_gar_nichts_da(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=0)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 500, "g")])
|
||
|
||
assert zutat.status == "fehlt"
|
||
assert zutat.fehlt_base == 500
|
||
|
||
|
||
def test_unbekannte_zutat_wird_nicht_still_als_vorhanden_gewertet(db):
|
||
_kategorie(db, "Trockenwaren")
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Safranfäden", 1, "g")])
|
||
|
||
assert zutat.status == "kein_treffer"
|
||
assert kochbar([zutat]) is False
|
||
|
||
|
||
def test_zutat_ohne_menge_ist_unklar(db):
|
||
cat = _kategorie(db, "Gewürze")
|
||
_artikel(db, "Salz", cat, bestand=500)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Salz", None, None, text="Salz nach Geschmack")])
|
||
|
||
assert zutat.status == "menge_unbekannt"
|
||
assert kochbar([zutat]) is False
|
||
|
||
|
||
def test_ohne_menge_und_ohne_bestand_fehlt_es(db):
|
||
cat = _kategorie(db, "Gewürze")
|
||
_artikel(db, "Salz", cat, bestand=0)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Salz", None, None)])
|
||
|
||
assert zutat.status == "fehlt"
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Zuordnung
|
||
# --------------------------------------------------------------------------
|
||
def test_aehnlicher_name_wird_gefunden(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl Typ 550", cat, bestand=2000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")])
|
||
|
||
assert zutat.art == "product"
|
||
assert zutat.ziel_name == "Weissmehl Typ 550"
|
||
|
||
|
||
def test_gruppe_zaehlt_marken_zusammen(db):
|
||
"""„Milch" ist markenuebergreifend gemeint – dafuer gibt es Gruppen."""
|
||
gruppe = Group(name="Milch")
|
||
db.add(gruppe)
|
||
db.commit()
|
||
cat = _kategorie(db, "Molkerei")
|
||
a = _artikel(db, "Valflora Vollmilch", cat, base=BaseUnit.milliliter, bestand=1000)
|
||
b = _artikel(db, "Emmi Vollmilch", cat, base=BaseUnit.milliliter, bestand=500)
|
||
a.group_id = gruppe.id
|
||
b.group_id = gruppe.id
|
||
db.commit()
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Milch", 1200, "ml")])
|
||
|
||
assert zutat.art == "group"
|
||
assert zutat.bestand_base == 1500 # beide Marken zusammen
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
def test_gemerkte_zuordnung_schlaegt_die_suche(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
richtig = _artikel(db, "Dinkelmehl", cat, bestand=2000)
|
||
|
||
[zutat] = pruefe_zutaten(
|
||
db, [_zutat("Mehl", 100, "g")],
|
||
zuordnung={"mehl": ("product", richtig.id)},
|
||
)
|
||
|
||
assert zutat.ziel_id == richtig.id
|
||
assert zutat.ziel_name == "Dinkelmehl"
|
||
|
||
|
||
def test_voellig_fremde_zutat_liefert_gar_keinen_vorschlag(db):
|
||
"""Lieber nichts als Unsinn.
|
||
|
||
Fruehere Fassung schlug hier „Weissmehl" vor, weil sich Buchstaben
|
||
ueberlappen. Ein Vorschlag, den niemand nachvollziehen kann, ist schlimmer
|
||
als keiner – er verleitet zum Durchklicken.
|
||
"""
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Kurkuma", 1, "g")])
|
||
|
||
assert zutat.status == "kein_treffer"
|
||
assert zutat.ziel_id is None
|
||
assert zutat.kandidaten == []
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Was NICHT passen darf – die Faelle aus dem echten Katalog
|
||
# --------------------------------------------------------------------------
|
||
def test_gegenstaende_kommen_nicht_in_frage(db):
|
||
"""Ein Joystick ist kein Zucker und eine Waffelplatte kein Apfel."""
|
||
essen = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", essen, bestand=1000)
|
||
kram = Category(name="Elektronik", tracking=CategoryTracking.object.value)
|
||
db.add(kram)
|
||
db.commit()
|
||
db.refresh(kram)
|
||
for name in ("Joystick für WII", "Snack Collection Platten 4 (Waffeln)",
|
||
"Shirt Casual Fit"):
|
||
db.add(Product(name=name, base_unit=BaseUnit.piece, category_id=kram.id,
|
||
source="manual"))
|
||
db.commit()
|
||
|
||
for zutat_name in ("Zucker", "Apfel", "Salz"):
|
||
[zutat] = pruefe_zutaten(db, [_zutat(zutat_name, 1, "g")])
|
||
assert zutat.status == "kein_treffer", zutat_name
|
||
assert all("Joystick" not in k.name for k in zutat.kandidaten)
|
||
|
||
|
||
def test_mehl_trifft_nicht_das_geschnetzelte(db):
|
||
"""Buchstaben-Aehnlichkeit allein reicht nicht – es braucht ein ganzes Wort."""
|
||
fleisch = _kategorie(db, "Fleisch & Fisch")
|
||
_artikel(db, "Mühlen Geschnetzeltes", fleisch, bestand=500)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Mehl Typ 405", 125, "g")])
|
||
|
||
assert zutat.status == "kein_treffer"
|
||
|
||
|
||
def test_eier_treffen_nicht_den_reis(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Reis", cat, bestand=2000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Eier", 3, None)])
|
||
|
||
assert zutat.status == "kein_treffer"
|
||
|
||
|
||
def test_kategorie_wird_zuerst_durchsucht(db):
|
||
"""Steht die Zutat in einer passenden Kategorie, gilt nur die."""
|
||
fleisch = _kategorie(db, "Fleisch & Fisch")
|
||
_artikel(db, "Mühlen Geschnetzeltes", fleisch, bestand=500)
|
||
backen = _kategorie(db, "Mehl & Zucker")
|
||
richtig = _artikel(db, "Weissmehl Typ 405", backen, bestand=2000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Mehl Typ 405", 125, "g")])
|
||
|
||
assert zutat.ziel_id == richtig.id
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
def test_vegan_allein_begruendet_keinen_treffer(db):
|
||
"""„vegan" ist eine Eigenschaft, kein Lebensmittel.
|
||
|
||
Sonst passt jedes vegane Produkt auf jede vegane Zutat: „vegane Butter"
|
||
landete auf „veganes Mühlen Knusper Bleu", „veganer Lime Curd" auf
|
||
„Vegane Mühlen-Schnitzel".
|
||
"""
|
||
fleisch = _kategorie(db, "Fleischersatz")
|
||
_artikel(db, "veganes Mühlen Knusper Bleu", fleisch, bestand=400)
|
||
_artikel(db, "Vegane Mühlen Nuggets Klassisch", fleisch, bestand=400)
|
||
_artikel(db, "Vegane Mühlen-Schnitzel auf Basis von Soja", fleisch, bestand=400)
|
||
|
||
zutaten = [
|
||
_zutat("vegane Butter, Block, ungesalzen", 45, "g"),
|
||
_zutat("veganer griechischer Joghurt oder Skyr", 80, "g"),
|
||
_zutat("veganer Lime Curd", 120, "g"),
|
||
]
|
||
for zutat in pruefe_zutaten(db, zutaten):
|
||
assert zutat.status == "kein_treffer", zutat.text
|
||
assert zutat.ziel_name == ""
|
||
|
||
|
||
def test_vegane_zutat_findet_das_passende_vegane_produkt(db):
|
||
"""Die Eigenschaft darf nicht sperren, nur nicht allein tragen."""
|
||
molk = _kategorie(db, "Molkereiprodukte")
|
||
butter = _artikel(db, "Vegane Butter", molk, bestand=500)
|
||
_artikel(db, "Vegane Mühlen-Schnitzel", molk, bestand=400)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("vegane Butter, Block, ungesalzen", 45, "g")])
|
||
|
||
assert zutat.ziel_id == butter.id
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
def test_zutat_aus_lauter_eigenschaften_trifft_nichts(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("frisch gemahlen, optional", 1, "g")])
|
||
|
||
assert zutat.status == "kein_treffer"
|
||
|
||
|
||
def test_beiwoerter_stoeren_den_echten_treffer_nicht(db):
|
||
cat = _kategorie(db, "Obst & Gemüse")
|
||
aepfel = _artikel(db, "Äpfel", cat, bestand=3000)
|
||
|
||
[zutat] = pruefe_zutaten(
|
||
db, [_zutat("Apfel geschält und in 3 mm dünne Scheiben geschnitten", 900, "g")]
|
||
)
|
||
|
||
assert zutat.ziel_id == aepfel.id
|
||
|
||
|
||
def test_umlaut_und_mehrzahl_finden_sich(db):
|
||
"""Rezept schreibt „Apfel", im Katalog stehen „Äpfel"."""
|
||
cat = _kategorie(db, "Obst & Gemüse")
|
||
aepfel = _artikel(db, "Äpfel", cat, base=BaseUnit.gram, bestand=2000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Apfel", 900, "g")])
|
||
|
||
assert zutat.ziel_id == aepfel.id
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
def test_vollmilch_findet_die_h_vollmilch(db):
|
||
cat = _kategorie(db, "Molkerei")
|
||
milch = _artikel(db, "H Vollmilch", cat, base=BaseUnit.milliliter, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Vollmilch Zimmertemperatur", 120, "ml")])
|
||
|
||
assert zutat.ziel_id == milch.id
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Hausmaße
|
||
# --------------------------------------------------------------------------
|
||
def test_essloeffel_wird_umgerechnet(db):
|
||
cat = _kategorie(db, "Mehl")
|
||
mehl = _artikel(db, "Weissmehl", cat, bestand=1000)
|
||
el = db.query(Measure).filter(Measure.name == "Esslöffel").one()
|
||
db.add(CategoryMeasure(measure_id=el.id, category_id=cat.id, amount=10,
|
||
base=BaseUnit.gram.value))
|
||
db.commit()
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 2, "EL")])
|
||
|
||
assert zutat.benoetigt_base == 20
|
||
assert zutat.status == "vorhanden"
|
||
|
||
|
||
def test_fehlendes_mass_wird_zum_anlegen_gemeldet(db):
|
||
"""Daraus baut die Oberflaeche „Maß jetzt anlegen"."""
|
||
cat = _kategorie(db, "Hülsenfrüchte")
|
||
_artikel(db, "Berglinsen", cat, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Berglinsen", 1, "Tasse")])
|
||
|
||
assert zutat.status == "menge_unbekannt"
|
||
assert zutat.fehlendes_mass == "Tasse"
|
||
assert zutat.kategorie_id == cat.id
|
||
assert "kein Maß" in zutat.hinweis
|
||
|
||
|
||
def test_unbekannte_einheit_wird_benannt(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
|
||
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 1, "furlong")])
|
||
|
||
assert zutat.status == "menge_unbekannt"
|
||
assert "furlong" in zutat.hinweis
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Ganzes Rezept
|
||
# --------------------------------------------------------------------------
|
||
def test_ein_rezept_mit_allem(db):
|
||
cat = _kategorie(db, "Trockenwaren")
|
||
_artikel(db, "Weissmehl", cat, bestand=1000)
|
||
_artikel(db, "Zucker", cat, bestand=50)
|
||
|
||
zutaten = [
|
||
_zutat("Weissmehl", 200, "g"), # reicht
|
||
_zutat("Zucker", 100, "g"), # zu wenig
|
||
_zutat("Safran", 1, "g"), # kennt niemand
|
||
]
|
||
ergebnis = pruefe_zutaten(db, zutaten)
|
||
|
||
assert [z.status for z in ergebnis] == ["vorhanden", "zu_wenig", "kein_treffer"]
|
||
assert kochbar(ergebnis) is False
|