""" Liest eine Gaesteliste im CSV-Format ein — der Web-Ersatz fuer bestellung.json (Meilenstein 1 der Roadmap, Adapter analog zu JSONConfig in Strukturdaten.py). CSV-Format (Kopfzeile noetig, Spaltenreihenfolge egal, Trennzeichen ; oder ,): Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl - Vorname, Nachname: Pflicht - Titel: optional (z.B. "Dr.") - Gruppe: Gruppenname — Personen mit demselben Gruppennamen gehoeren zusammen und sollen moeglichst zusammen sitzen. Leer = Einzelperson (bildet eine eigene Ein-Personen-Gruppe). - Tisch: optionale Tischnummer — die Person ist VIP und wird fest an diesen Tisch gesetzt (wie "tisch" in bestellung.json). VIPs laufen nicht durch die GA-Platzierung. - Anzahl: optionale Reservierungsanzahl — expandiert die Zeile in so viele identische Buchungen (wie "anzahl" in bestellung.json, z.B. fuer anonyme Sammelbuchungen). Leer = 1. Zusaetzlich werden die Spalten "MagNicht" und "MagGern" akzeptiert, aber noch ignoriert (reserviert fuer Personenpaar-Regeln, siehe Roadmap-Konzeptentscheidung Nr. 10) — so entsteht spaeter kein Formatbruch. """ __author__ = "Michael Stangl" __copyright__ = "Copyright (c) 2026 Michael Stangl" __license__ = "Python" import csv import io from Strukturdaten import Person, Personen, Gruppe, Gruppen class CSVConfig: """aus CSV Text/Datei alles einlesen (Rueckgabe wie JSONConfig.laden, plus Gruppennamen fuer die Anzeige im Web-Frontend)""" PflichtSpalten = ['vorname', 'nachname'] BekannteSpalten = ['vorname', 'nachname', 'titel', 'gruppe', 'tisch', 'anzahl', 'magnicht', 'maggern'] def HandleGaesteliste(self, Text): """parst den CSV-Text und baut Personen/Gruppen/VIPs auf Rueckgabe: [ PN, GN, VIPGruppe, VipHash, GruppenNamen ] - PN: Personen-Container (ohne VIPs, wie JSONConfig) - GN: Gruppen-Container (ohne VIP-Gruppe, wie JSONConfig) - VIPGruppe: Gruppe(0) mit allen fest gesetzten Personen - VipHash: { PersonId: Tischnummer } fuer VIPsSetzen() - GruppenNamen: { GruppenId: Anzeigename } """ # Trennzeichen automatisch bestimmen: die Kopfzeile entscheidet Kopfzeile = Text.lstrip('').splitlines()[0] if Text.strip() else '' Trenner = ';' if Kopfzeile.count(';') >= Kopfzeile.count(',') else ',' Leser = csv.DictReader( io.StringIO(Text.lstrip('')), delimiter=Trenner ) if Leser.fieldnames is None: raise ValueError("CSV ist leer — Kopfzeile mit mindestens " "Vorname/Nachname erwartet") # Spaltennamen normalisieren (Gross/Klein egal, Leerzeichen weg) SpaltenMap = {} for Feld in Leser.fieldnames: if Feld is None: continue SpaltenMap[Feld] = Feld.strip().lower() for Pflicht in self.PflichtSpalten: if Pflicht not in SpaltenMap.values(): raise ValueError("CSV-Kopfzeile: Pflichtspalte '%s' fehlt " "(gefunden: %s)" % (Pflicht, Leser.fieldnames)) def Wert(Zeile, Name): for Original, Normiert in SpaltenMap.items(): if Normiert == Name: Inhalt = Zeile.get(Original) return (Inhalt or '').strip() return '' PN = Personen() GN = Gruppen() VIPGruppe = Gruppe( 0 ) VipHash = {} GruppenNamen = {} # Gruppen entstehen beim ersten Auftreten ihres Namens; # Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe GruppeVonName = {} PID = 0 GID = 0 ZeilenNr = 1 for Zeile in Leser: ZeilenNr = ZeilenNr + 1 Vorname = Wert(Zeile, 'vorname') Nachname = Wert(Zeile, 'nachname') if Vorname == '' and Nachname == '': continue # Leerzeile if Vorname == '' or Nachname == '': raise ValueError("Zeile %d: Vorname und Nachname sind Pflicht" % ZeilenNr) Titel = Wert(Zeile, 'titel') GruppenName = Wert(Zeile, 'gruppe') TischFeld = Wert(Zeile, 'tisch') AnzahlFeld = Wert(Zeile, 'anzahl') try: AnzahlReservierungen = int(AnzahlFeld) if AnzahlFeld else 1 except ValueError: raise ValueError("Zeile %d: Anzahl '%s' ist keine Zahl" % (ZeilenNr, AnzahlFeld)) if AnzahlReservierungen < 1: raise ValueError("Zeile %d: Anzahl muss >= 1 sein" % ZeilenNr) TischNr = None if TischFeld != '': try: TischNr = int(TischFeld) except ValueError: raise ValueError("Zeile %d: Tisch '%s' ist keine " "Tischnummer" % (ZeilenNr, TischFeld)) for i in range(AnzahlReservierungen): PID = PID + 1 P = Person( PID, Vorname, Nachname, Titel ) if TischNr is not None: # VIP: fester Tisch, keine GA-Platzierung VipHash[ PID ] = TischNr VIPGruppe.Person_dazu( P ) else: PN.PersonAdd( P ) if GruppenName == '': # Einzelperson: eigene Ein-Personen-Gruppe, # komplett befuellen und dann registrieren GID = GID + 1 G = Gruppe( GID ) G.Person_dazu( P ) GruppenNamen[ GID ] = '%s %s' % (Vorname, Nachname) GN.Gruppe_dazu( G ) else: if GruppenName not in GruppeVonName: GID = GID + 1 G = Gruppe( GID ) GruppeVonName[ GruppenName ] = G GruppenNamen[ GID ] = GruppenName else: G = GruppeVonName[ GruppenName ] G.Person_dazu( P ) # benannte Gruppen erst jetzt registrieren, wenn sie komplett sind # (Gruppen.Gruppe_dazu uebernimmt Mitglieder und zaehlt NLeute) for GruppenName, G in GruppeVonName.items(): if G.Anzahl() > 0: GN.Gruppe_dazu( G ) return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ] def laden(self, FileName): """lese alle Eingangsdaten aus einer CSV-Datei ein""" # utf-8-sig schluckt das BOM, das Excel beim CSV-Export voranstellt fsock = open( FileName, 'r', encoding='utf-8-sig' ) Text = fsock.read() fsock.close() return self.HandleGaesteliste( Text ) if __name__ == '__main__': Beispiel = """Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl Britta;Beispiel;;Brautpaar;1; Bernd;Beispiel;Dr.;Brautpaar;1; Anna;Huber;;Familie Huber;; Berta;Huber;;Familie Huber;; Carl;Huber;;Familie Huber;; Gast;Kollege;;Kollegen;;4 Einzel;Gast;;;; """ print("\n-- Parse Beispiel-CSV:") C = CSVConfig() [ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = C.HandleGaesteliste( Beispiel ) print(PN) print(GN) print(VIPGruppe) print(VipHash) print(GruppenNamen) else: pass