Eine Karte mit dieser Ueberschrift, in der bereits Ueberfaelliges steht, ist irrefuehrend. KarteAblauf bekommt daher einen Modus soon/expired/alle statt des bisherigen Schalters "nurAbgelaufen". Neu ist die Karte "Ablauf-Uebersicht" (alle), die beides zusammen zeigt. Sie steht auch in der eingebauten Vorgabe, damit dort ohne Zutun weiterhin Ueberfaelliges sichtbar ist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
204 lines
7.7 KiB
Python
204 lines
7.7 KiB
Python
from datetime import date, timedelta # noqa: F401 (date fuer die Zustands-Tests)
|
||
|
||
import pytest
|
||
|
||
from app.models import BaseUnit, Category, Product, Role, User
|
||
from app.routers import dashboard
|
||
from app.services.conversion import article_unit, article_units
|
||
from app.services.stock import check_in, check_out
|
||
|
||
|
||
@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
|
||
|
||
|
||
# ---------------------------------------------------------------- Einheiten
|
||
def test_artikeleinheit_ist_das_gebinde(rice):
|
||
# rice: 500 g je Packung -> eine Artikeleinheit sind 500 g
|
||
faktor, label = article_unit(rice)
|
||
assert faktor == 500
|
||
assert label == "Packung"
|
||
assert article_units(rice, 1500) == 3
|
||
|
||
|
||
def test_artikeleinheit_ohne_gebinde_ist_die_produkteinheit(db):
|
||
eier = Product(name="Eier", base_unit=BaseUnit.piece)
|
||
db.add(eier)
|
||
db.commit()
|
||
faktor, _ = article_unit(eier)
|
||
assert faktor == 1
|
||
assert article_units(eier, 6) == 6
|
||
|
||
|
||
def test_gebinde_bezeichnung_wird_uebernommen(db):
|
||
pesto = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=195,
|
||
package_label="Glas")
|
||
db.add(pesto)
|
||
db.commit()
|
||
assert article_unit(pesto) == (195, "Glas")
|
||
|
||
|
||
# ------------------------------------------------------------ Ablaufzustand
|
||
def test_zustand_unterscheidet_die_vier_faelle():
|
||
heute = date(2026, 7, 1)
|
||
assert dashboard._zustand(None, heute, 7) == "no_date"
|
||
assert dashboard._zustand(heute - timedelta(days=1), heute, 7) == "expired"
|
||
assert dashboard._zustand(heute + timedelta(days=3), heute, 7) == "soon"
|
||
assert dashboard._zustand(heute, heute, 7) == "soon" # heute ist noch nicht abgelaufen
|
||
assert dashboard._zustand(heute + timedelta(days=30), heute, 7) == "ok"
|
||
|
||
|
||
def test_expiry_split_zaehlt_in_artikeleinheiten(db, rice, user):
|
||
heute = date.today()
|
||
check_in(db, rice, 2, "package", heute + timedelta(days=60), None, user) # ok
|
||
check_in(db, rice, 1, "package", heute + timedelta(days=3), None, user) # bald
|
||
check_in(db, rice, 3, "package", heute - timedelta(days=2), None, user) # abgelaufen
|
||
check_in(db, rice, 4, "package", None, None, user) # ohne MHD
|
||
db.commit()
|
||
|
||
split = dashboard.expiry_split(db=db, _=user)
|
||
assert split.ok == 2
|
||
assert split.soon == 1
|
||
assert split.expired == 3
|
||
assert split.no_date == 4
|
||
|
||
|
||
def test_by_category_bündelt_und_sortiert(db, user):
|
||
suess = Category(name="Süßwaren")
|
||
db.add(suess)
|
||
db.commit()
|
||
|
||
chips = Product(name="Chips", base_unit=BaseUnit.gram, package_size=175,
|
||
package_label="Tüte", category_id=suess.id)
|
||
ohne = Product(name="Salz", base_unit=BaseUnit.gram, package_size=500)
|
||
db.add_all([chips, ohne])
|
||
db.commit()
|
||
|
||
check_in(db, chips, 5, "package", None, None, user)
|
||
check_in(db, ohne, 2, "package", None, None, user)
|
||
db.commit()
|
||
|
||
anteile = dashboard.by_category(db=db, _=user)
|
||
assert [a.name for a in anteile] == ["Süßwaren", "Ohne Kategorie"]
|
||
assert anteile[0].article_units == 5
|
||
assert anteile[1].article_units == 2
|
||
|
||
|
||
# ----------------------------------------------------------------- Verlauf
|
||
def test_schrittweite_haengt_am_zeitraum():
|
||
"""Kurze Zeitraeume werden fein abgetastet, lange grob."""
|
||
assert dashboard._schrittweite(1) == timedelta(hours=1)
|
||
assert dashboard._schrittweite(2) == timedelta(hours=1)
|
||
assert dashboard._schrittweite(7) == timedelta(hours=6)
|
||
assert dashboard._schrittweite(90) == timedelta(days=1)
|
||
assert dashboard._schrittweite(365) == timedelta(days=7)
|
||
|
||
|
||
def test_schrittweite_haelt_die_punktzahl_im_rahmen():
|
||
"""Egal welcher Zeitraum – die Kurve bleibt zeichenbar."""
|
||
for tage in (1, 2, 7, 14, 30, 90, 180, 365, 730):
|
||
anzahl = timedelta(days=tage) / dashboard._schrittweite(tage)
|
||
assert 20 <= anzahl <= 180, f"{tage} Tage ergeben {anzahl} Punkte"
|
||
|
||
|
||
def test_timeline_rechnet_bewegungen_zurueck(db, rice, user):
|
||
"""Der Verlauf entsteht rueckwaerts aus dem heutigen Bestand."""
|
||
check_in(db, rice, 4, "package", None, None, user) # +4 Einheiten jetzt
|
||
check_out(db, rice, 1, "package", user) # -1 Einheit jetzt
|
||
db.commit()
|
||
|
||
punkte = dashboard.timeline(days=3, product_id=None, db=db, _=user)
|
||
# Jetzt 3 Einheiten; vor dem Einlagern war das Lager leer.
|
||
assert punkte[-1].article_units == 3
|
||
assert punkte[0].article_units == 0
|
||
# Aufsteigend sortiert, damit die Linie von links nach rechts laeuft.
|
||
assert punkte[0].at < punkte[-1].at
|
||
|
||
|
||
def test_timeline_kann_auf_ein_produkt_eingegrenzt_werden(db, rice, user):
|
||
anderes = Product(name="Zucker", base_unit=BaseUnit.gram, package_size=1000)
|
||
db.add(anderes)
|
||
db.commit()
|
||
check_in(db, rice, 2, "package", None, None, user)
|
||
check_in(db, anderes, 5, "package", None, None, user)
|
||
db.commit()
|
||
|
||
gesamt = dashboard.timeline(days=2, product_id=None, db=db, _=user)
|
||
nur_reis = dashboard.timeline(days=2, product_id=rice.id, db=db, _=user)
|
||
assert gesamt[-1].article_units == 7
|
||
assert nur_reis[-1].article_units == 2
|
||
|
||
|
||
def test_activity_zaehlt_ein_und_auslagerungen(db, rice, user):
|
||
check_in(db, rice, 2, "package", None, None, user)
|
||
check_in(db, rice, 1, "package", None, None, user)
|
||
check_out(db, rice, 1, "package", user)
|
||
db.commit()
|
||
|
||
abschnitte = dashboard.activity(days=3, db=db, _=user)
|
||
# Alles gerade eben passiert -> liegt im juengsten Abschnitt.
|
||
juengster = abschnitte[-1]
|
||
assert juengster.checked_in == 2
|
||
assert juengster.checked_out == 1
|
||
assert sum(a.checked_in for a in abschnitte) == 2
|
||
assert abschnitte[0].at < juengster.at
|
||
|
||
|
||
# ---------------------------------------------------------------- Anordnung
|
||
def test_layout_faellt_auf_die_eingebaute_zurueck(db, user):
|
||
antwort = dashboard.get_layout(db=db, user=user)
|
||
assert antwort.source == "builtin"
|
||
assert antwort.enforced is False
|
||
assert antwort.has_default is False
|
||
assert any(k["i"] == "expiry-all" for k in antwort.layout)
|
||
|
||
|
||
def test_eigene_anordnung_wird_gespeichert(db, user):
|
||
from app.schemas import DashboardLayoutIn
|
||
|
||
eigen = [{"i": "shopping", "x": 0, "y": 0, "w": 6, "h": 3}]
|
||
dashboard.put_layout(DashboardLayoutIn(layout=eigen), db=db, user=user)
|
||
|
||
antwort = dashboard.get_layout(db=db, user=user)
|
||
assert antwort.source == "user"
|
||
assert antwort.layout == eigen
|
||
|
||
|
||
def test_erzwungene_vorgabe_verhindert_eigene_anordnung(db, user):
|
||
from fastapi import HTTPException
|
||
|
||
from app.schemas import DashboardLayoutIn
|
||
|
||
vorgabe = [{"i": "status", "x": 0, "y": 0, "w": 12, "h": 1}]
|
||
dashboard.put_default_layout(DashboardLayoutIn(layout=vorgabe), db=db, admin=user)
|
||
dashboard.set_enforced(True, db=db, admin=user)
|
||
|
||
antwort = dashboard.get_layout(db=db, user=user)
|
||
assert antwort.source == "default"
|
||
assert antwort.enforced is True
|
||
assert antwort.layout == vorgabe
|
||
|
||
with pytest.raises(HTTPException) as fehler:
|
||
dashboard.put_layout(DashboardLayoutIn(layout=[]), db=db, user=user)
|
||
assert fehler.value.status_code == 403
|
||
|
||
|
||
def test_zuruecksetzen_stellt_die_vorgabe_wieder_her(db, user):
|
||
from app.schemas import DashboardLayoutIn
|
||
|
||
vorgabe = [{"i": "status", "x": 0, "y": 0, "w": 12, "h": 1}]
|
||
dashboard.put_default_layout(DashboardLayoutIn(layout=vorgabe), db=db, admin=user)
|
||
dashboard.put_layout(DashboardLayoutIn(layout=[{"i": "shopping", "x": 0, "y": 0, "w": 4, "h": 3}]),
|
||
db=db, user=user)
|
||
assert dashboard.get_layout(db=db, user=user).source == "user"
|
||
|
||
dashboard.reset_layout(db=db, user=user)
|
||
antwort = dashboard.get_layout(db=db, user=user)
|
||
assert antwort.source == "default"
|
||
assert antwort.layout == vorgabe
|