"""Angebrochene Packungen: Abspalten, Verbrauchsdatum, Teilverbrauch, Frist-Vererbung.""" from datetime import date, timedelta import pytest from fastapi import HTTPException from app.models import ( BaseUnit, Category, CategoryTracking, Lot, Movement, MovementType, Product, Role, Setting, User, ) from app.routers.stock import consume_lot, list_all_lots, open_lot from app.schemas import LotConsume, LotOpen from app.services.stock import check_in, current_stock from app.services.vererbung import frist_herkunft, oeffnungsfrist @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 @pytest.fixture() def milch(db): """Milliliter-Artikel mit 1000 ml je Packung, in einer Lebensmittel-Kategorie.""" cat = Category(name="Molkereiprodukte", tracking=CategoryTracking.food.value) db.add(cat) db.flush() product = Product( name="Vollmilch", base_unit=BaseUnit.milliliter, package_size=1000, category_id=cat.id, source="manual", ) db.add(product) db.commit() db.refresh(product) return product def _charge(db, product, menge_base, best_before=None): lot = Lot(product_id=product.id, quantity=menge_base, best_before=best_before) db.add(lot) db.commit() db.refresh(lot) return lot # -------------------------------------------------------------------------- # Anbrechen spaltet eine Packung ab # -------------------------------------------------------------------------- def test_anbrechen_spaltet_eine_packung_ab(db, user, milch): lot = _charge(db, milch, 4000) # 4 x 1 l in einer Zeile offen = open_lot(lot.id, LotOpen(), db=db, _=user) assert offen.quantity == 1000 assert offen.opened_on == date.today() assert offen.opened_quantity == 1000 # Der Rest bleibt zu – sonst trüge er das kurze Verbrauchsdatum mit. rest = db.query(Lot).filter(Lot.id != offen.id).one() assert rest.quantity == 3000 assert rest.opened_on is None assert rest.consume_by is None # Der Gesamtbestand ändert sich beim Anbrechen nicht. assert current_stock(db, milch.id) == 4000 def test_anbrechen_einer_einzelnen_packung_spaltet_nichts(db, user, milch): lot = _charge(db, milch, 1000) offen = open_lot(lot.id, LotOpen(), db=db, _=user) assert offen.id == lot.id assert db.query(Lot).count() == 1 assert offen.quantity == 1000 def test_anbrechen_nimmt_hoechstens_die_ganze_charge(db, user, milch): lot = _charge(db, milch, 400) # weniger als eine Packung offen = open_lot(lot.id, LotOpen(), db=db, _=user) assert offen.quantity == 400 assert db.query(Lot).count() == 1 def test_anbrechen_mit_eigener_menge(db, user, milch): lot = _charge(db, milch, 4000) offen = open_lot(lot.id, LotOpen(quantity=500), db=db, _=user) assert offen.quantity == 500 assert db.query(Lot).filter(Lot.id != offen.id).one().quantity == 3500 def test_zweimal_anbrechen_aendert_nichts(db, user, milch): lot = _charge(db, milch, 4000) offen = open_lot(lot.id, LotOpen(), db=db, _=user) nochmal = open_lot(offen.id, LotOpen(), db=db, _=user) assert nochmal.id == offen.id assert nochmal.quantity == 1000 assert db.query(Lot).count() == 2 def test_verbrauchsdatum_kommt_aus_der_frist(db, user, milch): db.add(Setting(key="open_days", value="5")) db.commit() lot = _charge(db, milch, 1000) offen = open_lot(lot.id, LotOpen(), db=db, _=user) assert offen.consume_by == date.today() + timedelta(days=5) def test_mhd_bleibt_neben_dem_verbrauchsdatum_stehen(db, user, milch): mhd = date.today() + timedelta(days=20) lot = _charge(db, milch, 1000, best_before=mhd) offen = open_lot(lot.id, LotOpen(), db=db, _=user) assert offen.best_before == mhd # unangetastet assert offen.consume_by is not None # Gewarnt wird nach dem früheren der beiden. frisch = db.get(Lot, offen.id) assert frisch.faellig_am == frisch.consume_by def test_frueheres_mhd_schlaegt_das_verbrauchsdatum(db, user, milch): mhd = date.today() + timedelta(days=1) # läuft vor der Öffnungsfrist ab lot = _charge(db, milch, 1000, best_before=mhd) open_lot(lot.id, LotOpen(), db=db, _=user) frisch = db.query(Lot).one() assert frisch.faellig_am == mhd # -------------------------------------------------------------------------- # Verbrauchen # -------------------------------------------------------------------------- def test_verbrauchen_oeffnet_in_einem_schritt(db, user, milch): lot = _charge(db, milch, 4000) ergebnis = consume_lot(lot.id, LotConsume(quantity=100, unit="ml"), db=db, user=user) assert ergebnis.opened is True assert ergebnis.lot is not None assert ergebnis.lot.quantity == 900 # 1000 geöffnet, 100 verbraucht assert ergebnis.lot.opened_quantity == 1000 assert current_stock(db, milch.id) == 3900 # Der Verbrauch ist eine Bewegung, das Anbrechen allein nicht. bewegungen = db.query(Movement).all() assert len(bewegungen) == 1 assert bewegungen[0].type == MovementType.out assert bewegungen[0].quantity == 100 def test_verbrauchen_ohne_anbrechen_wenn_abgewaehlt(db, user, milch): lot = _charge(db, milch, 4000) ergebnis = consume_lot( lot.id, LotConsume(quantity=100, unit="ml", auto_open=False), db=db, user=user ) assert ergebnis.opened is False assert db.query(Lot).count() == 1 assert db.query(Lot).one().opened_on is None assert current_stock(db, milch.id) == 3900 def test_weiterer_verbrauch_nutzt_die_offene_packung(db, user, milch): lot = _charge(db, milch, 4000) consume_lot(lot.id, LotConsume(quantity=100, unit="ml"), db=db, user=user) offen = db.query(Lot).filter(Lot.opened_on.isnot(None)).one() consume_lot(offen.id, LotConsume(quantity=200, unit="ml"), db=db, user=user) assert db.get(Lot, offen.id).quantity == 700 assert db.query(Lot).count() == 2 def test_leere_offene_packung_verschwindet(db, user, milch): lot = _charge(db, milch, 1000) open_lot(lot.id, LotOpen(), db=db, _=user) ergebnis = consume_lot(lot.id, LotConsume(quantity=1000, unit="ml"), db=db, user=user) assert ergebnis.lot is None assert db.query(Lot).count() == 0 assert current_stock(db, milch.id) == 0 def test_mehr_verbrauchen_als_drin_ist_scheitert(db, user, milch): lot = _charge(db, milch, 1000) with pytest.raises(HTTPException) as fehler: consume_lot(lot.id, LotConsume(quantity=2000, unit="ml"), db=db, user=user) assert fehler.value.status_code == 409 def test_unbekannte_einheit_bricht_nichts_an(db, user, milch): """Erst rechnen, dann anbrechen – sonst stünde eine Packung grundlos offen.""" lot = _charge(db, milch, 4000) with pytest.raises(HTTPException) as fehler: consume_lot(lot.id, LotConsume(quantity=1, unit="furlong"), db=db, user=user) assert fehler.value.status_code == 400 assert db.query(Lot).one().opened_on is None def test_verbrauchen_in_packungen(db, user, rice, milch): """„1 Packung" muss weiter gehen – die Hausmaße dürfen das nicht verdrängen.""" lot = check_in(db, rice, 3, "package", None, None, None) db.commit() consume_lot(lot.id, LotConsume(quantity=1, unit="package"), db=db, user=user) assert current_stock(db, rice.id) == 1000 # -------------------------------------------------------------------------- # Frist: Artikel > Kategorie > Oberkategorie > global # -------------------------------------------------------------------------- def test_frist_vererbung_ueber_die_ebenen(db, milch): # 1. Ohne alles gilt der Standard aus der Konfiguration. assert oeffnungsfrist(db, milch) == 3 assert frist_herkunft(db, milch)[1] == "Einstellungen" # 2. Globale Einstellung. db.add(Setting(key="open_days", value="7")) db.commit() assert oeffnungsfrist(db, milch) == 7 # 3. Oberkategorie. ober = db.get(Category, milch.category_id) unter = Category(name="Frischmilch", parent_id=ober.id, tracking=CategoryTracking.food.value) db.add(unter) db.flush() milch.category_id = unter.id ober.open_days = 4 db.commit() assert oeffnungsfrist(db, milch) == 4 assert frist_herkunft(db, milch)[1] == "Kategorie: Molkereiprodukte" # 4. Die nähere Kategorie gewinnt. unter.open_days = 2 db.commit() assert oeffnungsfrist(db, milch) == 2 assert frist_herkunft(db, milch)[1] == "Kategorie: Frischmilch" # 5. Der Artikel schlägt alles. milch.open_days = 1 db.commit() assert oeffnungsfrist(db, milch) == 1 assert frist_herkunft(db, milch)[1] == "Artikel" def test_kaputte_globale_einstellung_faellt_auf_den_standard(db, milch): db.add(Setting(key="open_days", value="viele")) db.commit() assert oeffnungsfrist(db, milch) == 3 def test_die_chargenliste_zeigt_den_offenen_zustand(db, user, milch): """Ohne diese Felder saehe die Liste eine offene Packung wie jede andere. Die Zeile traegt dann auch die falsche Ablaufwarnung, denn gewarnt wird nach dem fruheren von MHD und Verbrauchsdatum. """ check_in(db, milch, 4000, "ml", date.today() + timedelta(days=30), None, None) db.commit() [charge] = db.query(Lot).all() open_lot(charge.id, LotOpen(quantity=None), db=db, _=user) zeilen = {z.quantity: z for z in list_all_lots(db=db, _=user)} offen = zeilen[1000] zu = zeilen[3000] assert offen.opened_on == date.today() assert offen.consume_by == date.today() + timedelta(days=3) assert offen.opened_quantity == 1000 assert zu.opened_on is None assert zu.consume_by is None