Dashboard zaehlt Einzelstuecke mit; Baum-Dropdown scrollbar
Auswertungen (by_category, expiry_split, stats) iterierten nur Lots. Gegenstaende mit Menge-je-Lagerort liegen als Lots und tauchten auf – Einzelstuecke liegen aber als Items und fehlten dadurch komplett (Dashboard zeigte z.B. nur die Powerbank, nicht Kamera/Kabel). Neuer Helfer _bestand_beitraege deckt beide Speicherformen ab: jedes Item zaehlt als ein Stueck ohne MHD. Regressionstest ergaenzt; volle Suite gruen (120). CategorySelect: Scrollen IM aufklappbaren Menue schloss es, weil jeder Scroll als Seiten-Scroll gewertet wurde. Jetzt schliesst nur ein Scroll ausserhalb des Menues; die lange Liste laesst sich scrollen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ from ..models import (
|
||||
Category,
|
||||
DashboardLayout,
|
||||
Group,
|
||||
Item,
|
||||
Lot,
|
||||
Movement,
|
||||
MovementType,
|
||||
@@ -300,6 +301,35 @@ def _lots_mit_produkt(db: Session) -> list[tuple[Lot, Product]]:
|
||||
)
|
||||
|
||||
|
||||
def _bestand_beitraege(
|
||||
db: Session, heute: date, warnfrist: int
|
||||
) -> list[tuple[Product, float, str]]:
|
||||
"""Alle Bestands-Beiträge als (Produkt, Artikeleinheiten, Ablaufzustand).
|
||||
|
||||
Deckt beide Speicherformen ab, damit die Auswertungen vollständig sind:
|
||||
- Chargen (Lebensmittel und Mengen-Gegenstände) je Lot mit ihrem MHD-Zustand.
|
||||
- Einzelstücke: jedes Item zählt als ein Stück ohne MHD ("no_date"). Diese
|
||||
liegen als Items und nicht als Lots vor – ohne sie fehlten Einzelstück-
|
||||
Produkte (z.B. eine Kamera) in jeder Kategorie-/Ablauf-Auswertung.
|
||||
"""
|
||||
beitraege: list[tuple[Product, float, str]] = []
|
||||
for lot, product in _lots_mit_produkt(db):
|
||||
# Einzelstücke werden über ihre Items gezählt (siehe unten), nie über Lots.
|
||||
if product.individual:
|
||||
continue
|
||||
faktor, _label = article_unit(product)
|
||||
menge = lot.quantity / (faktor or 1.0)
|
||||
beitraege.append((product, menge, _zustand(lot.best_before, heute, warnfrist)))
|
||||
|
||||
for product in db.query(Product).filter(Product.individual.is_(True)).all():
|
||||
anzahl = db.query(Item).filter(Item.product_id == product.id).count()
|
||||
if anzahl <= 0:
|
||||
continue
|
||||
faktor, _label = article_unit(product)
|
||||
beitraege.append((product, anzahl / (faktor or 1.0), "no_date"))
|
||||
return beitraege
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DashboardStats)
|
||||
def stats(
|
||||
db: Session = Depends(get_db), _: User = Depends(get_current_user)
|
||||
@@ -310,11 +340,9 @@ def stats(
|
||||
einheiten = 0.0
|
||||
bestand_produkte: set[int] = set()
|
||||
bald = abgelaufen = 0
|
||||
for lot, product in _lots_mit_produkt(db):
|
||||
faktor, _label = article_unit(product)
|
||||
einheiten += lot.quantity / faktor if faktor else 0.0
|
||||
for product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
|
||||
einheiten += menge
|
||||
bestand_produkte.add(product.id)
|
||||
zustand = _zustand(lot.best_before, heute, warnfrist)
|
||||
if zustand == "expired":
|
||||
abgelaufen += 1
|
||||
elif zustand == "soon":
|
||||
@@ -351,9 +379,8 @@ def expiry_split(
|
||||
heute = date.today()
|
||||
warnfrist = get_expiry_warning_days(db)
|
||||
summen = {"ok": 0.0, "soon": 0.0, "expired": 0.0, "no_date": 0.0}
|
||||
for lot, product in _lots_mit_produkt(db):
|
||||
faktor, _label = article_unit(product)
|
||||
summen[_zustand(lot.best_before, heute, warnfrist)] += lot.quantity / (faktor or 1.0)
|
||||
for _product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
|
||||
summen[zustand] += menge
|
||||
return ExpirySplit(**{k: round(v, 3) for k, v in summen.items()})
|
||||
|
||||
|
||||
@@ -368,12 +395,10 @@ def by_category(
|
||||
|
||||
leer = lambda: {"article_units": 0.0, "ok": 0.0, "soon": 0.0, "expired": 0.0, "no_date": 0.0}
|
||||
eimer: dict[int | None, dict] = defaultdict(leer)
|
||||
for lot, product in _lots_mit_produkt(db):
|
||||
faktor, _label = article_unit(product)
|
||||
menge = lot.quantity / (faktor or 1.0)
|
||||
for product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
|
||||
topf = eimer[product.category_id]
|
||||
topf["article_units"] += menge
|
||||
topf[_zustand(lot.best_before, heute, warnfrist)] += menge
|
||||
topf[zustand] += menge
|
||||
|
||||
ergebnis = [
|
||||
CategoryShare(
|
||||
|
||||
@@ -2,9 +2,10 @@ from datetime import date, timedelta # noqa: F401 (date fuer die Zustands-Test
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import BaseUnit, Category, Product, Role, User
|
||||
from app.models import BaseUnit, Category, Item, Product, Role, User
|
||||
from app.routers import dashboard
|
||||
from app.services.conversion import article_unit, article_units
|
||||
from app.services.items import generate_uid
|
||||
from app.services.stock import check_in, check_out
|
||||
|
||||
|
||||
@@ -89,6 +90,31 @@ def test_by_category_bündelt_und_sortiert(db, user):
|
||||
assert anteile[1].article_units == 2
|
||||
|
||||
|
||||
def test_einzelstuecke_zaehlen_in_auswertungen(db, user):
|
||||
"""Regression: Einzelstueck-Produkte liegen als Items, nicht als Lots. Sie
|
||||
muessen trotzdem in Kategorie-, Ablauf- und Bestandsauswertung erscheinen –
|
||||
sonst zeigte das Dashboard nur die Mengen-Gegenstaende (Bug: nur Powerbank)."""
|
||||
kamera_kat = Category(name="Kamera", tracking="object")
|
||||
db.add(kamera_kat)
|
||||
db.commit()
|
||||
kamera = Product(name="Alpha 7 IV", category_id=kamera_kat.id, individual=True)
|
||||
db.add(kamera)
|
||||
db.commit()
|
||||
for _ in range(2):
|
||||
db.add(Item(uid=generate_uid(db), product_id=kamera.id))
|
||||
db.commit()
|
||||
|
||||
anteil = next(a for a in dashboard.by_category(db=db, _=user) if a.name == "Kamera")
|
||||
assert anteil.article_units == 2
|
||||
assert anteil.no_date == 2 # Einzelstuecke haben kein MHD
|
||||
|
||||
assert dashboard.expiry_split(db=db, _=user).no_date == 2
|
||||
|
||||
st = dashboard.stats(db=db, _=user)
|
||||
assert st.article_units == 2
|
||||
assert st.products_in_stock == 1
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- Verlauf
|
||||
def test_schrittweite_haengt_am_zeitraum():
|
||||
"""Kurze Zeitraeume werden fein abgetastet, lange grob."""
|
||||
|
||||
Reference in New Issue
Block a user