Rezept-Zuordnung: die Einheit verraet das falsche Produkt
"200 ml Schlagsahne" landete auf "Kraeuter Sahne Haehnchen" - beide teilen
das Wort "Sahne", der Rest ist Zufall. Die Einheit haette es verraten: Das
Haehnchen wird in Gramm gefuehrt, das Rezept sagt Milliliter. Wo sich die
Rezept-Einheit gar nicht umrechnen laesst, wird deshalb nicht mehr
zugeordnet, sondern offen gelassen - lieber "nichts Passendes gefunden"
samt Begruendung als eine falsche Behauptung.
Entscheidend ist, WARUM die Umrechnung scheitert, und nur einer der Gruende
sagt etwas ueber den Artikel:
- Bekannte Einheit falscher Art ohne Zweiteinheit -> spricht gegen den
Artikel. Milliliter an einem Gramm-Produkt ist ein anderes Produkt.
- Unbekanntes Wort oder nicht hinterlegtes Hausmass ("1 Tasse Linsen") ->
sagt gar nichts. Die Linsen koennen voellig richtig sein, es fehlt bloss
die Massangabe, und die laesst sich nachtragen.
- Gar keine Einheit ("3 Eier") -> passt immer.
Die erste Fassung hat das nicht unterschieden und damit "1 Tasse Linsen"
zu "nicht zugeordnet" gemacht, obwohl der Artikel stimmte - die beiden
bestehenden Tests haben das sofort gezeigt.
Bei mehreren Kandidaten gewinnt jetzt der mit passender Einheit, auch gegen
eine hoehere Namensaehnlichkeit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
41d262cb2d
commit
2a56cca3c8
@@ -21,7 +21,16 @@ from dataclasses import dataclass, field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Category, CategoryTracking, Group, Product
|
||||
from .conversion import ConversionError, article_unit, group_min_context, to_base
|
||||
from .conversion import (
|
||||
BASE_OF_KIND,
|
||||
ConversionError,
|
||||
article_unit,
|
||||
find_unit,
|
||||
group_min_context,
|
||||
kind_of_product,
|
||||
to_base,
|
||||
zweit_faktor,
|
||||
)
|
||||
from .matching import match_score, normalisieren
|
||||
from .stock import current_stock, summe_bestand_gewichtet
|
||||
|
||||
@@ -40,6 +49,9 @@ class Kandidat:
|
||||
id: int
|
||||
name: str
|
||||
score: int
|
||||
# Laesst sich die Rezept-Einheit fuer diesen Kandidaten umrechnen? Passt sie
|
||||
# nicht, ist es meist das falsche Produkt (siehe ``_einheit_passt``).
|
||||
einheit_passt: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -203,7 +215,37 @@ def _kategorien_zur_zutat(db: Session, zutat_woerter: list[str]) -> set[int]:
|
||||
return passende
|
||||
|
||||
|
||||
def _kandidaten(db: Session, name: str) -> list[Kandidat]:
|
||||
def _einheit_passt(db: Session, produkt: Product, token: str) -> bool:
|
||||
"""Lässt sich die Rezept-Einheit für diesen Artikel überhaupt umrechnen?
|
||||
|
||||
Das ist ein überraschend gutes Zeichen für „falsches Produkt": Sagt das
|
||||
Rezept Milliliter und der Kandidat wird in Gramm geführt, ist es meist
|
||||
etwas anderes – „200 ml Schlagsahne" ist kein „Kräuter Sahne Hähnchen".
|
||||
Eine Zweiteinheit am Artikel („3 Stück ≙ 250 g") gilt weiterhin als
|
||||
passend, dafür ist sie da.
|
||||
|
||||
Entscheidend ist, WARUM eine Umrechnung scheitert – nur einer der Gründe
|
||||
sagt etwas über den Artikel:
|
||||
|
||||
* **Bekannte Einheit falscher Art, ohne Brücke** → spricht gegen den
|
||||
Artikel. Milliliter an einem Gramm-Produkt ist ein anderes Produkt.
|
||||
* **Unbekanntes Wort oder nicht hinterlegtes Hausmaß** („1 Tasse Linsen")
|
||||
→ sagt gar nichts. Der Artikel kann völlig richtig sein, es fehlt nur die
|
||||
Maßangabe – die lässt sich nachtragen.
|
||||
* **Keine Einheit** (etwa „3 Eier") → passt immer.
|
||||
"""
|
||||
if not token:
|
||||
return True
|
||||
unit = find_unit(db, token)
|
||||
if unit is None:
|
||||
# Unbekannt oder Hausmass: kein Urteil ueber den Artikel.
|
||||
return True
|
||||
if unit.kind == kind_of_product(produkt):
|
||||
return True
|
||||
return zweit_faktor(produkt, BASE_OF_KIND[unit.kind]) is not None
|
||||
|
||||
|
||||
def _kandidaten(db: Session, name: str, einheit: str = "") -> list[Kandidat]:
|
||||
"""Artikel und Gruppen, die zur Zutat passen können – bester zuerst.
|
||||
|
||||
Drei Stufen, in dieser Reihenfolge:
|
||||
@@ -247,11 +289,15 @@ def _kandidaten(db: Session, name: str) -> list[Kandidat]:
|
||||
for produkt in lebensmittel:
|
||||
voll = f"{produkt.name} {produkt.brand or ''}".strip()
|
||||
if _hat_anker(woerter, voll):
|
||||
treffer.append(Kandidat("product", produkt.id, produkt.name, match_score(name, voll)))
|
||||
k = Kandidat("product", produkt.id, produkt.name, match_score(name, voll))
|
||||
k.einheit_passt = _einheit_passt(db, produkt, einheit)
|
||||
treffer.append(k)
|
||||
|
||||
# Gruppen bei Gleichstand vor Artikeln: „Milch" ist markenuebergreifend
|
||||
# gemeint, und genau das ist eine Gruppe.
|
||||
treffer.sort(key=lambda k: (-k.score, 0 if k.art == "group" else 1, k.name))
|
||||
# Reihenfolge: passende Einheit zuerst, dann Punkte; Gruppen bei Gleichstand
|
||||
# vor Artikeln, weil „Milch" markenuebergreifend gemeint ist.
|
||||
treffer.sort(key=lambda k: (
|
||||
0 if k.einheit_passt else 1, -k.score, 0 if k.art == "group" else 1, k.name,
|
||||
))
|
||||
return treffer[:5]
|
||||
|
||||
|
||||
@@ -319,14 +365,24 @@ def pruefe_zutaten(
|
||||
if gemerkt is not None:
|
||||
pruefung.art, pruefung.ziel_id = gemerkt
|
||||
else:
|
||||
pruefung.kandidaten = _kandidaten(db, name)
|
||||
pruefung.kandidaten = _kandidaten(db, name, pruefung.einheit)
|
||||
bester = pruefung.kandidaten[0] if pruefung.kandidaten else None
|
||||
if bester is not None:
|
||||
# Passt die Einheit beim besten Kandidaten nicht, wird NICHT
|
||||
# zugeordnet: Sagt das Rezept Milliliter und der Artikel wird in
|
||||
# Gramm gefuehrt, ist es meist etwas anderes. Lieber offen lassen
|
||||
# und von Hand zuordnen lassen, als etwas Falsches behaupten.
|
||||
if bester is not None and bester.einheit_passt:
|
||||
pruefung.art, pruefung.ziel_id = bester.art, bester.id
|
||||
elif bester is not None:
|
||||
pruefung.hinweis = (
|
||||
f"Nichts Passendes gefunden: '{bester.name}' würde zum Namen "
|
||||
f"passen, wird aber nicht in '{pruefung.einheit}' geführt."
|
||||
)
|
||||
|
||||
if pruefung.ziel_id is None:
|
||||
pruefung.status = "kein_treffer"
|
||||
pruefung.hinweis = f"'{name}' ist hier keinem Artikel zugeordnet."
|
||||
if not pruefung.hinweis:
|
||||
pruefung.hinweis = f"'{name}' ist hier keinem Artikel zugeordnet."
|
||||
ergebnis.append(pruefung)
|
||||
continue
|
||||
|
||||
|
||||
@@ -310,6 +310,57 @@ def test_beiwoerter_stoeren_den_echten_treffer_nicht(db):
|
||||
assert zutat.ziel_id == aepfel.id
|
||||
|
||||
|
||||
def test_falsche_einheit_spricht_gegen_den_artikel(db):
|
||||
"""„200 ml Schlagsahne" ist kein „Kräuter Sahne Hähnchen"."""
|
||||
fleisch = _kategorie(db, "Fleisch & Fisch")
|
||||
_artikel(db, "Kräuter Sahne Hähnchen", fleisch, base=BaseUnit.gram, bestand=800)
|
||||
|
||||
[zutat] = pruefe_zutaten(db, [_zutat("Schlagsahne", 200, "ml")])
|
||||
|
||||
assert zutat.status == "kein_treffer"
|
||||
assert "nicht in 'ml' geführt" in zutat.hinweis
|
||||
|
||||
|
||||
def test_passende_einheit_gewinnt_gegen_hoehere_punktzahl(db):
|
||||
molk = _kategorie(db, "Molkereiprodukte")
|
||||
fleisch = _kategorie(db, "Fleisch & Fisch")
|
||||
_artikel(db, "Sahne Hähnchen", fleisch, base=BaseUnit.gram, bestand=800)
|
||||
richtig = _artikel(db, "Schlagsahne Rahm", molk, base=BaseUnit.milliliter, bestand=500)
|
||||
|
||||
[zutat] = pruefe_zutaten(db, [_zutat("Schlagsahne", 200, "ml")])
|
||||
|
||||
assert zutat.ziel_id == richtig.id
|
||||
|
||||
|
||||
def test_fehlendes_hausmass_spricht_nicht_gegen_den_artikel(db):
|
||||
"""Nur eine BEKANNTE Einheit falscher Art ist ein Hinweis.
|
||||
|
||||
„1 Tasse Linsen" heisst nicht, dass die Linsen falsch sind – es fehlt bloss
|
||||
die Maßangabe, und die laesst sich nachtragen.
|
||||
"""
|
||||
cat = _kategorie(db, "Hülsenfrüchte")
|
||||
linsen = _artikel(db, "Berglinsen", cat, bestand=1000)
|
||||
|
||||
[zutat] = pruefe_zutaten(db, [_zutat("Berglinsen", 1, "Tasse")])
|
||||
|
||||
assert zutat.ziel_id == linsen.id
|
||||
assert zutat.status == "menge_unbekannt"
|
||||
|
||||
|
||||
def test_zweiteinheit_laesst_die_fremde_einheit_zu(db):
|
||||
cat = _kategorie(db, "Backwaren")
|
||||
broetchen = _artikel(db, "Brötchen", cat, base=BaseUnit.piece, package_size=6,
|
||||
bestand=12)
|
||||
broetchen.secondary_base = BaseUnit.gram.value
|
||||
broetchen.secondary_count = 1
|
||||
broetchen.secondary_amount = 50
|
||||
db.commit()
|
||||
|
||||
[zutat] = pruefe_zutaten(db, [_zutat("Brötchen", 100, "g")])
|
||||
|
||||
assert zutat.ziel_id == broetchen.id
|
||||
|
||||
|
||||
def test_umlaut_und_mehrzahl_finden_sich(db):
|
||||
"""Rezept schreibt „Apfel", im Katalog stehen „Äpfel"."""
|
||||
cat = _kategorie(db, "Obst & Gemüse")
|
||||
|
||||
Reference in New Issue
Block a user