Ich hatte das nach dem frueheren wirksamen Datum sortiert, sodass eine ungeoeffnete Packung mit knappem MHD vor einer offenen drankam. Das ist falsch herum: Das MHD einer geschlossenen Packung ist konservativ und wird real meist ueberlebt, eine offene verdirbt dagegen tatsaechlich. Und wer die zweite aufmacht, waehrend die erste offen steht, hat am Ende zwei angebrochene. Angebrochenes kommt also immer zuerst; innerhalb beider Gruppen gilt weiter FEFO ueber das naehere von MHD und Verbrauchsdatum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
6.2 KiB
Python
180 lines
6.2 KiB
Python
"""Zwei Daten an einer Charge: aufgedrucktes MHD und „zu verbrauchen bis".
|
||
|
||
Beide bleiben getrennt sichtbar, gewarnt wird nach dem früheren. Der heikle
|
||
Fall ist die angebrochene Packung **ohne** aufgedrucktes MHD – die fiel vorher
|
||
durch den Filter `best_before IS NOT NULL`.
|
||
"""
|
||
|
||
from datetime import date, timedelta
|
||
|
||
import pytest
|
||
|
||
from app.models import BaseUnit, Lot, Product, Role, User
|
||
from app.routers.dashboard import _zustand
|
||
from app.routers.stock import open_lot
|
||
from app.routers.views import expiring
|
||
from app.schemas import LotOpen
|
||
from app.services.stock import _fefo_lots
|
||
|
||
|
||
@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):
|
||
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, opened_on=None, consume_by=None):
|
||
lot = Lot(
|
||
product_id=product.id, quantity=menge, best_before=best_before,
|
||
opened_on=opened_on, consume_by=consume_by,
|
||
)
|
||
db.add(lot)
|
||
db.commit()
|
||
db.refresh(lot)
|
||
return lot
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Ablaufliste
|
||
# --------------------------------------------------------------------------
|
||
def test_angebrochene_charge_ohne_mhd_taucht_auf(db, user, milch):
|
||
"""Der Fall, den der alte Filter verschluckt hätte."""
|
||
_lot(db, milch, 500, best_before=None,
|
||
opened_on=date.today(), consume_by=date.today() + timedelta(days=2))
|
||
|
||
liste = expiring(days=7, db=db, _=user)
|
||
|
||
assert len(liste) == 1
|
||
assert liste[0].best_before is None
|
||
assert liste[0].warn_source == "consume_by"
|
||
assert liste[0].days_left == 2
|
||
|
||
|
||
def test_beide_daten_stehen_getrennt_in_der_liste(db, user, milch):
|
||
mhd = date.today() + timedelta(days=30)
|
||
verbrauch = date.today() + timedelta(days=3)
|
||
_lot(db, milch, 500, best_before=mhd, opened_on=date.today(), consume_by=verbrauch)
|
||
|
||
eintrag = expiring(days=7, db=db, _=user)[0]
|
||
|
||
assert eintrag.best_before == mhd # bleibt sichtbar
|
||
assert eintrag.consume_by == verbrauch
|
||
assert eintrag.warn_date == verbrauch # gewarnt wird nach dem früheren
|
||
assert eintrag.days_left == 3
|
||
|
||
|
||
def test_frueheres_mhd_gewinnt_gegen_verbrauchsdatum(db, user, milch):
|
||
mhd = date.today() + timedelta(days=1)
|
||
_lot(db, milch, 500, best_before=mhd,
|
||
opened_on=date.today(), consume_by=date.today() + timedelta(days=5))
|
||
|
||
eintrag = expiring(days=7, db=db, _=user)[0]
|
||
|
||
assert eintrag.warn_date == mhd
|
||
assert eintrag.warn_source == "best_before"
|
||
|
||
|
||
def test_ungeoeffnete_charge_verhaelt_sich_wie_bisher(db, user, milch):
|
||
mhd = date.today() + timedelta(days=3)
|
||
_lot(db, milch, 1000, best_before=mhd)
|
||
|
||
eintrag = expiring(days=7, db=db, _=user)[0]
|
||
|
||
assert eintrag.warn_date == mhd
|
||
assert eintrag.warn_source == "best_before"
|
||
assert eintrag.consume_by is None
|
||
|
||
|
||
def test_ausserhalb_der_frist_bleibt_draussen(db, user, milch):
|
||
_lot(db, milch, 500, opened_on=date.today(),
|
||
consume_by=date.today() + timedelta(days=30))
|
||
|
||
assert expiring(days=7, db=db, _=user) == []
|
||
|
||
|
||
def test_liste_ist_nach_dem_wirksamen_datum_sortiert(db, user, milch):
|
||
_lot(db, milch, 100, best_before=date.today() + timedelta(days=6))
|
||
_lot(db, milch, 100, opened_on=date.today(), consume_by=date.today() + timedelta(days=1))
|
||
_lot(db, milch, 100, best_before=date.today() + timedelta(days=3))
|
||
|
||
tage = [e.days_left for e in expiring(days=7, db=db, _=user)]
|
||
|
||
assert tage == [1, 3, 6]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Zustand fürs Dashboard
|
||
# --------------------------------------------------------------------------
|
||
def test_zustand_angebrochen_ist_ein_eigener(db):
|
||
heute = date.today()
|
||
assert _zustand(heute + timedelta(days=30), heute, 7, angebrochen=True) == "opened"
|
||
|
||
|
||
def test_dringlichkeit_schlaegt_angebrochen(db):
|
||
heute = date.today()
|
||
# Angebrochen UND bald ablaufend: die Warnung muss gewinnen.
|
||
assert _zustand(heute + timedelta(days=2), heute, 7, angebrochen=True) == "soon"
|
||
assert _zustand(heute - timedelta(days=1), heute, 7, angebrochen=True) == "expired"
|
||
|
||
|
||
def test_zustand_ohne_daten_bleibt_no_date(db):
|
||
heute = date.today()
|
||
assert _zustand(None, heute, 7) == "no_date"
|
||
# Angebrochen ohne jedes Datum ist trotzdem angebrochen.
|
||
assert _zustand(None, heute, 7, angebrochen=True) == "opened"
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# FEFO
|
||
# --------------------------------------------------------------------------
|
||
def test_angebrochenes_wird_zuerst_geleert(db, milch):
|
||
spaet = _lot(db, milch, 1000, best_before=date.today() + timedelta(days=30))
|
||
offen = _lot(db, milch, 500, best_before=date.today() + timedelta(days=30),
|
||
opened_on=date.today(), consume_by=date.today() + timedelta(days=2))
|
||
|
||
reihenfolge = [lot.id for lot in _fefo_lots(db, milch.id)]
|
||
|
||
assert reihenfolge == [offen.id, spaet.id]
|
||
|
||
|
||
def test_angebrochenes_geht_auch_vor_knappem_mhd(db, milch):
|
||
"""Offen zuerst, selbst wenn eine geschlossene Packung früher datiert ist.
|
||
|
||
Das MHD einer ungeöffneten Packung ist konservativ und wird real meist
|
||
überlebt; eine offene verdirbt dagegen tatsächlich. Und wer die zweite
|
||
aufmacht, während die erste offen steht, hat am Ende zwei angebrochene.
|
||
"""
|
||
morgen = _lot(db, milch, 1000, best_before=date.today() + timedelta(days=1))
|
||
offen = _lot(db, milch, 500, opened_on=date.today(),
|
||
consume_by=date.today() + timedelta(days=5))
|
||
|
||
reihenfolge = [lot.id for lot in _fefo_lots(db, milch.id)]
|
||
|
||
assert reihenfolge == [offen.id, morgen.id]
|
||
|
||
|
||
def test_unter_den_angebrochenen_gilt_weiter_fefo(db, milch):
|
||
spaet = _lot(db, milch, 500, opened_on=date.today(),
|
||
consume_by=date.today() + timedelta(days=5))
|
||
frueh = _lot(db, milch, 500, opened_on=date.today(),
|
||
consume_by=date.today() + timedelta(days=1))
|
||
|
||
reihenfolge = [lot.id for lot in _fefo_lots(db, milch.id)]
|
||
|
||
assert reihenfolge == [frueh.id, spaet.id]
|