"""Hausmaße: „2 EL" wiegt bei Mehl anders als bei Zucker. Die Menge gehört an die Kategorie bzw. den Artikel, nicht an den Namen – ein globaler Faktor wie bei den verwalteten Einheiten wäre hier zwangsläufig falsch. """ import pytest from app.models import ( BaseUnit, Category, CategoryMeasure, CategoryTracking, Measure, Product, ProductMeasure, ) from app.routers.measures import category_measures from app.seed import ensure_builtin_measures from app.services.conversion import ConversionError, to_base from app.services.stock import check_in, check_out, current_stock from app.services.vererbung import ( geerbte_kategorie_frist, mass_aufloesen, verfuegbare_masse, ) @pytest.fixture() def masse(db): ensure_builtin_measures(db) return {m.name: m for m in db.query(Measure).all()} def _kategorie(db, name, parent=None): cat = Category( name=name, parent_id=parent.id if parent else None, 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): 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) return product def _mass_an_kategorie(db, measure, cat, amount, base=BaseUnit.gram): db.add(CategoryMeasure( measure_id=measure.id, category_id=cat.id, amount=amount, base=base.value )) db.commit() # -------------------------------------------------------------------------- # Derselbe Löffel, zwei Gewichte # -------------------------------------------------------------------------- def test_esslöffel_wiegt_je_kategorie_anders(db, masse): el = masse["Esslöffel"] mehl_kat = _kategorie(db, "Mehl") zucker_kat = _kategorie(db, "Zucker") mehl = _artikel(db, "Weissmehl", mehl_kat) zucker = _artikel(db, "Kristallzucker", zucker_kat) _mass_an_kategorie(db, el, mehl_kat, 10) _mass_an_kategorie(db, el, zucker_kat, 12) assert to_base(db, mehl, 2, "EL", hausmasse=True) == 20 assert to_base(db, zucker, 2, "EL", hausmasse=True) == 24 def test_kurzform_und_grossschreibung(db, masse): el = masse["Esslöffel"] kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) _mass_an_kategorie(db, el, kat, 10) for token in ("EL", "el", "Esslöffel", "esslöffel", " EL "): assert to_base(db, mehl, 1, token, hausmasse=True) == 10 def test_tasse_in_millilitern(db, masse): tasse = masse["Tasse"] kat = _kategorie(db, "Molkerei") milch = _artikel(db, "Vollmilch", kat, base=BaseUnit.milliliter) _mass_an_kategorie(db, tasse, kat, 200, base=BaseUnit.milliliter) assert to_base(db, milch, 1.5, "Tasse", hausmasse=True) == 300 # -------------------------------------------------------------------------- # Vererbung: Artikel > nächste Kategorie > Oberkategorie # -------------------------------------------------------------------------- def test_unterkategorie_erbt_von_oben(db, masse): el = masse["Esslöffel"] trocken = _kategorie(db, "Trockenwaren") mehl_kat = _kategorie(db, "Mehl", parent=trocken) mehl = _artikel(db, "Weissmehl", mehl_kat) _mass_an_kategorie(db, el, trocken, 11) assert to_base(db, mehl, 1, "EL", hausmasse=True) == 11 angabe = mass_aufloesen(db, mehl, "EL") assert angabe.herkunft == "Kategorie: Trockenwaren" def test_naehere_kategorie_schlaegt_die_obere(db, masse): el = masse["Esslöffel"] trocken = _kategorie(db, "Trockenwaren") mehl_kat = _kategorie(db, "Mehl", parent=trocken) mehl = _artikel(db, "Weissmehl", mehl_kat) _mass_an_kategorie(db, el, trocken, 11) _mass_an_kategorie(db, el, mehl_kat, 9) assert to_base(db, mehl, 1, "EL", hausmasse=True) == 9 def test_artikel_schlaegt_die_kategorie(db, masse): el = masse["Esslöffel"] kat = _kategorie(db, "Zucker") puder = _artikel(db, "Puderzucker", kat) _mass_an_kategorie(db, el, kat, 12) db.add(ProductMeasure( measure_id=el.id, product_id=puder.id, amount=8, base=BaseUnit.gram.value )) db.commit() assert to_base(db, puder, 1, "EL", hausmasse=True) == 8 assert mass_aufloesen(db, puder, "EL").herkunft == "Artikel" # -------------------------------------------------------------------------- # Lücken und Fehler # -------------------------------------------------------------------------- def test_mass_ohne_menge_meldet_sich_deutlich(db, masse): """Die Meldung ist die Grundlage für „Maß jetzt anlegen" im Rezeptabgleich.""" kat = _kategorie(db, "Hülsenfrüchte") linsen = _artikel(db, "Berglinsen", kat) with pytest.raises(ConversionError) as fehler: to_base(db, linsen, 1, "Tasse", hausmasse=True) assert "kein Maß 'Tasse' hinterlegt" in str(fehler.value) def test_voellig_unbekannte_einheit_bleibt_unbekannte_einheit(db, masse): kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) with pytest.raises(ConversionError) as fehler: to_base(db, mehl, 1, "furlong", hausmasse=True) assert "Unbekannte Einheit" in str(fehler.value) def test_artfremdes_mass_braucht_die_zweiteinheit(db, masse): """Ein Gramm-Maß an einem Stück-Artikel geht nur über die Brücke.""" el = masse["Esslöffel"] kat = _kategorie(db, "Backwaren") broetchen = _artikel(db, "Brötchen", kat, base=BaseUnit.piece, package_size=6) _mass_an_kategorie(db, el, kat, 20) # in Gramm with pytest.raises(ConversionError) as fehler: to_base(db, broetchen, 1, "EL", hausmasse=True) assert "Zweiteinheit" in str(fehler.value) # Mit Brücke „3 Stück ≙ 150 g" sind 20 g dann 0,4 Stück. broetchen.secondary_base = BaseUnit.gram.value broetchen.secondary_count = 3 broetchen.secondary_amount = 150 db.commit() assert to_base(db, broetchen, 1, "EL", hausmasse=True) == pytest.approx(0.4) def test_packung_schlaegt_das_hausmass(db, masse): """„package" wird zuerst geprüft – daran darf ein Hausmaß nichts ändern.""" kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat, package_size=1000) neu = Measure(name="Packung", short="Pck", position=99) db.add(neu) db.commit() _mass_an_kategorie(db, neu, kat, 7) assert to_base(db, mehl, 1, "package", hausmasse=True) == 1000 def test_ohne_schalter_gilt_das_hausmass_nicht(db, masse): """Einlagern und Mindestbestände dürfen keine Schätzwerte annehmen.""" el = masse["Esslöffel"] kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) _mass_an_kategorie(db, el, kat, 10) with pytest.raises(ConversionError) as fehler: to_base(db, mehl, 2, "EL") # ohne hausmasse=True assert "Unbekannte Einheit" in str(fehler.value) def test_einlagern_nimmt_kein_hausmass(db, masse): el = masse["Esslöffel"] kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) _mass_an_kategorie(db, el, kat, 10) with pytest.raises(ConversionError): check_in(db, mehl, 2, "EL", None, None, None) def test_auslagern_nimmt_ein_hausmass(db, masse): el = masse["Esslöffel"] kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) _mass_an_kategorie(db, el, kat, 10) check_in(db, mehl, 1, "package", None, None, None) db.commit() check_out(db, mehl, 2, "EL", None) db.commit() assert current_stock(db, mehl.id) == 980 def test_einheit_schlaegt_gleichnamiges_hausmass(db, masse): """Ein Maß „Liter" darf die echte Einheit nicht verdecken.""" kat = _kategorie(db, "Getränke") saft = _artikel(db, "Apfelsaft", kat, base=BaseUnit.milliliter, package_size=1000) liter_mass = Measure(name="Liter", short="Ltr", position=99) db.add(liter_mass) db.commit() _mass_an_kategorie(db, liter_mass, kat, 5, base=BaseUnit.milliliter) assert to_base(db, saft, 1, "Liter", hausmasse=True) == 1000 def test_verfuegbare_masse_listet_nur_hinterlegte(db, masse): el = masse["Esslöffel"] kat = _kategorie(db, "Mehl") mehl = _artikel(db, "Weissmehl", kat) _mass_an_kategorie(db, el, kat, 10) liste = verfuegbare_masse(db, mehl) assert [a.measure.name for a in liste] == ["Esslöffel"] assert liste[0].amount == 10 assert liste[0].herkunft == "Kategorie: Mehl" def test_artikel_ohne_kategorie_hat_keine_masse(db, masse): product = Product(name="Irgendwas", base_unit=BaseUnit.gram, source="manual") db.add(product) db.commit() assert verfuegbare_masse(db, product) == [] assert mass_aufloesen(db, product, "EL") is None # -------------------------------------------------------------------------- # Bearbeitungsansicht: eigene und geerbte Mengen # -------------------------------------------------------------------------- def test_kategorie_sieht_ihre_eigenen_mengen(db, masse): """Ohne Vererbung ist die Liste die reine Bearbeitungsansicht.""" ober = _kategorie(db, "Mehl & Zucker") _mass_an_kategorie(db, masse["Esslöffel"], ober, 10) liste = category_measures(ober.id, db=db, _=None) assert [(e.name, e.amount, e.herkunft) for e in liste] == [("Esslöffel", 10, "Kategorie")] def test_unterkategorie_zeigt_das_geerbte_als_solches(db, masse): """Der Platzhalter in der Maske: Was gilt, wenn ich das Feld leer lasse?""" ober = _kategorie(db, "Mehl & Zucker") unter = _kategorie(db, "Backzutaten", parent=ober) _mass_an_kategorie(db, masse["Esslöffel"], ober, 10) ohne = category_measures(unter.id, db=db, _=None) mit = category_measures(unter.id, vererbt=True, db=db, _=None) assert ohne == [] assert [(e.name, e.amount, e.herkunft) for e in mit] == [ ("Esslöffel", 10, "Kategorie: Mehl & Zucker"), ] def test_eigener_wert_verdeckt_den_geerbten_in_der_liste(db, masse): """Sonst stünde dieselbe Zeile zweimal da, mit verschiedenen Zahlen.""" ober = _kategorie(db, "Mehl & Zucker") unter = _kategorie(db, "Backzutaten", parent=ober) _mass_an_kategorie(db, masse["Esslöffel"], ober, 10) _mass_an_kategorie(db, masse["Esslöffel"], unter, 12) liste = category_measures(unter.id, vererbt=True, db=db, _=None) assert [(e.amount, e.herkunft) for e in liste] == [(12, "Kategorie")] def test_geerbte_frist_nennt_die_kategorie_die_sie_setzt(db): ober = _kategorie(db, "Molkerei") unter = _kategorie(db, "Frischmilch", parent=ober) ober.open_days = 5 db.commit() assert geerbte_kategorie_frist(db, unter) == (5, "Kategorie: Molkerei") # Die Kategorie, die den Wert selbst setzt, erbt ihn NICHT von sich. assert geerbte_kategorie_frist(db, ober)[1] == "Einstellungen"