"""Der Gruppen-Graph: Ober- und Untergruppen. Anders als Kategorien und Lagerorte bilden Gruppen KEINEN Baum: „Grillwurst" haengt unter „Wurst" UND unter „Grillgut" – mit einem einzelnen ``parent_id`` waere genau das nicht abbildbar. Die Struktur ist deshalb ein gerichteter azyklischer Graph. Daraus folgt die eine Regel, an der hier alles haengt: Auswertungen laufen ueber MENGEN, nicht ueber Baum-Walks. Eine ueber zwei Wege erreichbare Untergruppe wuerde sonst doppelt zaehlen und den Bestand ihrer Obergruppe verfaelschen. Jede Funktion schuetzt sich zusaetzlich mit einem ``gesehen``-Set gegen Ringe. Ueber die API kann keiner entstehen (Pruefung in routers/groups.py), eine eingespielte Sicherung kann aber einen mitbringen – und ein Ring wuerde jede Auswertung endlos laufen lassen. Gleiche Vorsichtsmassnahme wie in routers/categories.py. """ from __future__ import annotations from ..models import Group, Product def teilgraph(group: Group) -> list[Group]: """Die Gruppe samt ALLEN Untergruppen (transitiv), jede genau einmal.""" gesehen: set[int] = set() ergebnis: list[Group] = [] offen = [group] while offen: aktuell = offen.pop() if aktuell.id in gesehen: continue # ueber zwei Wege erreichbar ODER Ring in den Daten gesehen.add(aktuell.id) ergebnis.append(aktuell) offen.extend(aktuell.children) return ergebnis def nachfahren_ids(group: Group) -> set[int]: """IDs aller Untergruppen (transitiv, ohne die Gruppe selbst).""" return {g.id for g in teilgraph(group)} - {group.id} def vorfahren_ids(group: Group) -> set[int]: """IDs aller Obergruppen (transitiv, ohne die Gruppe selbst).""" gesehen: set[int] = set() offen = list(group.parents) while offen: aktuell = offen.pop() if aktuell.id in gesehen: continue gesehen.add(aktuell.id) offen.extend(aktuell.parents) return gesehen def produkte(group: Group) -> list[Product]: """Alle Artikel der Gruppe UND ihrer Untergruppen – jeder genau einmal. Ein Artikel haengt an genau einer Gruppe (``Product.group_id``), die Entdopplung greift also schon auf Gruppenebene. Der Artikel-Check bleibt trotzdem stehen: er kostet nichts und macht die Absicht sichtbar. """ gesehen: set[int] = set() ergebnis: list[Product] = [] for g in teilgraph(group): for p in g.products: if p.id not in gesehen: gesehen.add(p.id) ergebnis.append(p) return ergebnis