"""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, 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