"""Mindestbestände hängen nur noch an Lagerorten – „Überall" ist Ort NULL. „Überall" (egal wo, Hauptsache im Haus) ersetzt den früheren Gesamt-Mindest- bestand und liegt als Wurzel über allen Lagerorten. Dadurch verrechnet dieselbe Faltung wie bei verschachtelten Orten auch Überall gegen Küche & Co. – ein Artikel steht nicht mehr doppelt in der Einkaufsliste. """ import pytest from sqlalchemy.exc import IntegrityError from app.models import ( BaseUnit, Group, GroupLocationMinStock, Location, Lot, Product, ProductLocationMinStock, Role, Setting, Unit, User, ) from app.crud import product_to_out from app.routers.views import shopping_list, shopping_list_by_location from app.services.min_stock import ( MIGRATION_KEY, UEBERALL_NAME, lies_ueberall, migriere_mindestbestaende, schreibe_ueberall, ) @pytest.fixture() def user(db): person = User(username="tester", password_hash="x", role=Role.admin) db.add(person) db.commit() db.refresh(person) return person def _artikel(db, name="Gurken", **kwargs) -> Product: # package_size=1 -> Artikeleinheit == Basiseinheit, macht die Rechnung klar. p = Product(name=name, base_unit=BaseUnit.gram, package_size=1, **kwargs) db.add(p) db.commit() db.refresh(p) return p def test_ueberall_zeile_wird_gelesen(db, user): p = _artikel(db) schreibe_ueberall(db, p, 500) db.commit() out = product_to_out(db, p) assert out.min_stock == pytest.approx(500) (zeile,) = out.location_min_stocks assert zeile.location_id is None assert zeile.location_name == UEBERALL_NAME assert lies_ueberall(p.location_min_stocks) == pytest.approx(500) def test_ueberall_auf_null_setzen_entfernt_die_zeile(db, user): p = _artikel(db) schreibe_ueberall(db, p, 500) db.commit() schreibe_ueberall(db, p, None) db.commit() assert p.location_min_stocks == [] assert product_to_out(db, p).min_stock is None def test_zwei_ueberall_zeilen_werden_abgelehnt(db, user): """Der Teilindex ersetzt den UNIQUE-Schutz, den NULLs nicht bekommen.""" p = _artikel(db) db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=500)) db.commit() db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=900)) with pytest.raises(IntegrityError): db.commit() db.rollback() def test_ueberall_wird_gegen_lagerort_verrechnet(db, user): """Überall 5, Küche 2, je 1 vorhanden -> nicht 6 kaufen. Für die Küche fehlt 1. Das liegt danach auch im Haus und deckt Überall mit: dort fehlen 5 - (1 vorhanden + 1 gekauft) = 3. """ p = _artikel(db, "Gurken") kueche = Location(name="Küche") db.add(kueche) db.flush() db.add(Lot(product_id=p.id, quantity=1, location_id=kueche.id)) db.add_all([ ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=5), ProductLocationMinStock(product_id=p.id, location_id=kueche.id, min_stock=2), ]) db.commit() (gesamt,) = shopping_list(db=db, _=user) assert gesamt.deficit == pytest.approx(3) nach_ort = {n.location_name: n for n in shopping_list_by_location(db=db, _=user)} assert nach_ort["Küche"].products[0].deficit == pytest.approx(1) # „Überall" gehört nicht in die Ortsliste. assert UEBERALL_NAME not in nach_ort def test_ueberall_zaehlt_chargen_ohne_ort_mit(db, user): """Eine Charge ohne Lagerort liegt in keinem Ort-Subtree, aber im Haus.""" p = _artikel(db, "Butter") db.add(Lot(product_id=p.id, quantity=4, location_id=None)) db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=5)) db.commit() (gesamt,) = shopping_list(db=db, _=user) assert gesamt.stock == pytest.approx(4) assert gesamt.deficit == pytest.approx(1) # ---- Datenwanderung ---- def _gramm(db) -> Unit: return db.query(Unit).filter(Unit.name == "Gramm").one() def test_migration_macht_gesamtwerte_zu_ueberall(db, user): """Alter Stand: min_stock am Artikel, Ort-Werte in Artikeleinheiten.""" p = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=150, package_label="Glas", min_stock=600) db.add(p) db.flush() keller = Location(name="Keller") db.add(keller) db.flush() # 2 Gläser à 150 g – frueher so gespeichert (Artikeleinheiten). db.add(ProductLocationMinStock(product_id=p.id, location_id=keller.id, min_stock=2)) db.commit() migriere_mindestbestaende(db) db.refresh(p) assert p.min_stock is None # Quelle geleert werte = {e.location_id: e.min_stock for e in p.location_min_stocks} assert werte[None] == pytest.approx(600) # war schon Basiseinheiten assert werte[keller.id] == pytest.approx(300) # 2 Gläser * 150 g def test_migration_rechnet_gruppenwerte_ueber_den_divisor(db, user): gruppe = Group(name="Pesto", min_stock=3, min_stock_unit=_gramm(db), package_size=150, package_label="Glas", min_stock_in_packages=True) db.add(gruppe) db.flush() keller = Location(name="Keller") db.add(keller) db.flush() db.add(GroupLocationMinStock(group_id=gruppe.id, location_id=keller.id, min_stock=2)) db.commit() migriere_mindestbestaende(db) db.refresh(gruppe) assert gruppe.min_stock is None werte = {e.location_id: e.min_stock for e in gruppe.location_min_stocks} assert werte[None] == pytest.approx(450) # 3 Gläser * 150 g assert werte[keller.id] == pytest.approx(300) # 2 Gläser * 150 g def test_migration_laeuft_nur_einmal(db, user): """Der zweite Lauf darf die Werte nicht erneut hochrechnen.""" p = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=150, min_stock=600) db.add(p) db.flush() keller = Location(name="Keller") db.add(keller) db.flush() db.add(ProductLocationMinStock(product_id=p.id, location_id=keller.id, min_stock=2)) db.commit() migriere_mindestbestaende(db) migriere_mindestbestaende(db) db.refresh(p) werte = {e.location_id: e.min_stock for e in p.location_min_stocks} assert werte[keller.id] == pytest.approx(300) # nicht 45000 assert db.get(Setting, MIGRATION_KEY) is not None