"""Aufbereitung der gruppierten Listenansicht: Markt → Warengruppe → Artikel. Liegt bewusst außerhalb der Router, weil drei Stellen dieselbe Gliederung brauchen: die App, der öffentliche Link und der Ausdruck. Läge die Logik im Router, würden die drei mit der Zeit auseinanderlaufen. """ from sqlalchemy import select from sqlalchemy.orm import Session from app.models import Category, ListItem, Market, ShoppingList from app.schemas_shopping import ItemOut, ListView, ViewCategory, ViewMarket NO_MARKET = "Ohne Markt" NO_CATEGORY = "Ohne Warengruppe" def creator_label(item: ListItem) -> str | None: """Anzeigename, sonst der lokale Teil der Adresse. Die vollständige E-Mail-Adresse anderer Mitglieder wird nie ausgeliefert.""" if item.creator is None: return None return item.creator.display_name or item.creator.email.split("@")[0] def item_out(item: ListItem, *, anonymous: bool = False) -> ItemOut: """`anonymous=True` für öffentliche Links: Dort ist der Empfängerkreis unbestimmt, deshalb darf nicht erkennbar sein, wer was eingetragen hat.""" return ItemOut( id=item.id, article_id=item.article_id, article_name=item.article.name, created_by=None if anonymous else item.created_by, created_by_name=None if anonymous else creator_label(item), market_id=item.market_id, category_id=item.category_id, count=item.count, pack_size=item.pack_size, pack_unit=item.pack_unit, variant=item.variant, note=item.note, status=item.status, price_cents=item.price_cents, total_cents=(item.price_cents * item.count) if item.price_cents else None, row_rev=item.row_rev, updated_at=item.updated_at, ) def build_view( db: Session, lst: ShoppingList, *, include_bought: bool = True, include_deferred: bool = True, anonymous: bool = False, ) -> ListView: """Märkte und Warengruppen ordnen sich nach `sort_order`, dann nach Namen - so lässt sich die Reihenfolge an den tatsächlichen Weg durch den Laden anpassen. Artikel innerhalb einer Gruppe alphabetisch. Einträge ohne Zuordnung landen in Sammelgruppen am Ende, damit nichts unsichtbar wird. """ wanted = {"open"} if include_bought: wanted.add("bought") if include_deferred: wanted.add("deferred") items = db.scalars( select(ListItem).where( ListItem.list_id == lst.id, ListItem.deleted_at.is_(None), ListItem.status.in_(wanted), ) ).all() markets = { m.id: m for m in db.scalars( select(Market).where(Market.list_id == lst.id, Market.deleted_at.is_(None)) ).all() } categories = { c.id: c for c in db.scalars( select(Category).where( Category.list_id == lst.id, Category.deleted_at.is_(None) ) ).all() } buckets: dict[str | None, dict[str | None, list[ListItem]]] = {} for item in items: mid = item.market_id if item.market_id in markets else None cid = item.category_id if item.category_id in categories else None buckets.setdefault(mid, {}).setdefault(cid, []).append(item) def market_key(mid: str | None) -> tuple[int, int, str]: if mid is None: return (1, 0, "") # "Ohne Markt" ganz nach hinten m = markets[mid] return (0, m.sort_order, m.name.casefold()) def category_key(cid: str | None) -> tuple[int, int, str]: if cid is None: return (1, 0, "") c = categories[cid] return (0, c.sort_order, c.name.casefold()) out_markets: list[ViewMarket] = [] grand_total = 0 for mid in sorted(buckets, key=market_key): out_categories: list[ViewCategory] = [] market_total = 0 open_count = 0 for cid in sorted(buckets[mid], key=category_key): group = sorted(buckets[mid][cid], key=lambda i: i.article.name.casefold()) for item in group: if item.status == "open": open_count += 1 if item.price_cents: # Preis gilt je Gebinde, multipliziert wird mit der # Stückzahl - nicht mit der Packungsgröße. Vorher # ergaben 500 ml zu 2,99 EUR eine Summe von 1495 EUR. market_total += item.price_cents * item.count out_categories.append( ViewCategory( category_id=cid, category_name=categories[cid].name if cid else NO_CATEGORY, items=[item_out(i, anonymous=anonymous) for i in group], ) ) grand_total += market_total out_markets.append( ViewMarket( market_id=mid, market_name=markets[mid].name if mid else NO_MARKET, categories=out_categories, open_count=open_count, total_cents=market_total, ) ) return ListView( list_id=lst.id, list_name=lst.name, rev=lst.rev, markets=out_markets, grand_total_cents=grand_total, )