"""Zusammenfassen gleichartiger Chargen – und die Ausnahme für angebrochene. Diese Logik war bisher ungetestet, obwohl vier Stellen sie aufrufen (`check_in`, `bulk_lot_location`, `split_lot`, `update_lot`). Mit dem Anbrechen wird sie gefährlich: Ohne die Ausnahme würde eine offene Packung beim nächsten Einlagern am selben Ort still mit einer ungeöffneten verschmolzen und ihr kürzeres Verbrauchsdatum verlieren. """ from datetime import date, timedelta import pytest from app.models import BaseUnit, Location, Lot, Movement, MovementType, Product, Role, User from app.routers.stock import bulk_lot_location, open_lot from app.schemas import BulkLotLocation, LotOpen from app.services.stock import ( check_in, consolidate_duplicate_lots, consolidate_lot_group, current_stock, ) @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 keller(db): ort = Location(name="Keller") db.add(ort) db.commit() db.refresh(ort) return ort @pytest.fixture() def milch(db): product = Product( name="Vollmilch", base_unit=BaseUnit.milliliter, package_size=1000, source="manual", ) db.add(product) db.commit() db.refresh(product) return product def _lot(db, product, menge, best_before=None, ort=None): lot = Lot( product_id=product.id, quantity=menge, best_before=best_before, location_id=ort.id if ort else None, ) db.add(lot) db.commit() db.refresh(lot) return lot # -------------------------------------------------------------------------- # Das bisherige Verhalten muss bleiben # -------------------------------------------------------------------------- def test_gleiche_chargen_werden_zusammengefasst(db, milch, keller): mhd = date.today() + timedelta(days=10) _lot(db, milch, 1000, mhd, keller) _lot(db, milch, 2000, mhd, keller) survivor = consolidate_lot_group(db, milch.id, mhd, "day", keller.id) db.commit() assert survivor.quantity == 3000 assert db.query(Lot).count() == 1 def test_verschiedenes_mhd_bleibt_getrennt(db, milch, keller): _lot(db, milch, 1000, date.today() + timedelta(days=5), keller) _lot(db, milch, 1000, date.today() + timedelta(days=9), keller) consolidate_duplicate_lots(db) assert db.query(Lot).count() == 2 def test_bewegungen_haengen_an_der_verbleibenden_charge(db, milch, keller, user): mhd = date.today() + timedelta(days=10) alt = _lot(db, milch, 1000, mhd, keller) neu = _lot(db, milch, 1000, mhd, keller) db.add(Movement(product_id=milch.id, lot_id=neu.id, type=MovementType.in_, quantity=1000, unit_used="ml")) db.commit() survivor = consolidate_lot_group(db, milch.id, mhd, "day", keller.id) db.commit() assert survivor.id == alt.id assert db.query(Movement).one().lot_id == alt.id def test_einlagern_fasst_mit_der_bestehenden_charge_zusammen(db, milch, keller): mhd = date.today() + timedelta(days=10) check_in(db, milch, 1, "package", mhd, keller.id, None) check_in(db, milch, 1, "package", mhd, keller.id, None) db.commit() assert db.query(Lot).count() == 1 assert current_stock(db, milch.id) == 2000 # -------------------------------------------------------------------------- # Die Ausnahme: Angebrochenes bleibt für sich # -------------------------------------------------------------------------- def test_angebrochene_charge_wird_nicht_eingesammelt(db, milch, keller, user): mhd = date.today() + timedelta(days=20) lot = _lot(db, milch, 2000, mhd, keller) offen = open_lot(lot.id, LotOpen(), db=db, _=user) verbrauchsdatum = offen.consume_by consolidate_lot_group(db, milch.id, mhd, "day", keller.id) db.commit() assert db.query(Lot).count() == 2 frisch = db.get(Lot, offen.id) assert frisch is not None assert frisch.consume_by == verbrauchsdatum def test_einlagern_laesst_die_offene_packung_in_ruhe(db, milch, keller, user): """Der gefährlichste Pfad: nachkaufen, während eine Packung offen ist.""" mhd = date.today() + timedelta(days=20) lot = _lot(db, milch, 2000, mhd, keller) offen = open_lot(lot.id, LotOpen(), db=db, _=user) check_in(db, milch, 1, "package", mhd, keller.id, None) db.commit() frisch = db.get(Lot, offen.id) assert frisch.quantity == 1000 assert frisch.opened_on is not None # Die zugekaufte Packung verschmilzt mit der verbliebenen ungeöffneten. zu = db.query(Lot).filter(Lot.opened_on.is_(None)).all() assert len(zu) == 1 assert zu[0].quantity == 2000 assert current_stock(db, milch.id) == 3000 def test_umlagern_mehrerer_chargen_laesst_offenes_getrennt(db, milch, keller, user): mhd = date.today() + timedelta(days=20) lot = _lot(db, milch, 3000, mhd, None) offen = open_lot(lot.id, LotOpen(), db=db, _=user) zweite = _lot(db, milch, 1000, mhd, None) bulk_lot_location( BulkLotLocation(lot_ids=[offen.id, zweite.id, lot.id], location_id=keller.id), db=db, _=user, ) offene = db.query(Lot).filter(Lot.opened_on.isnot(None)).all() assert len(offene) == 1 assert offene[0].quantity == 1000 assert offene[0].location_id == keller.id assert current_stock(db, milch.id) == 4000 def test_start_aufraeumer_ruehrt_offenes_nicht_an(db, milch, keller, user): mhd = date.today() + timedelta(days=20) lot = _lot(db, milch, 2000, mhd, keller) open_lot(lot.id, LotOpen(), db=db, _=user) # Zwei ungeöffnete Dubletten dazu, die zusammengefasst werden sollen. _lot(db, milch, 500, mhd, keller) entfernt = consolidate_duplicate_lots(db) assert entfernt == 1 assert db.query(Lot).filter(Lot.opened_on.isnot(None)).count() == 1 assert db.query(Lot).filter(Lot.opened_on.is_(None)).count() == 1 assert current_stock(db, milch.id) == 2500