diff --git a/libs/CSVConfig.py b/libs/CSVConfig.py new file mode 100644 index 0000000..7b9307e --- /dev/null +++ b/libs/CSVConfig.py @@ -0,0 +1,190 @@ +""" +Liest eine Gaesteliste im CSV-Format ein — der Web-Ersatz fuer bestellung.xml +(Meilenstein 1 der Roadmap, Adapter analog zu XMLConfig 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 in bestellung.xml). + VIPs laufen nicht durch die GA-Platzierung. + - Anzahl: optionale Reservierungsanzahl — expandiert die Zeile in so + viele identische Buchungen (wie in bestellung.xml, + 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 XMLConfig.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 XMLConfig) + - GN: Gruppen-Container (ohne VIP-Gruppe, wie XMLConfig) + - VIPGruppe: Gruppe(0) mit allen fest gesetzten Personen + - VipHash: { PersonId: Tischnummer } fuer VIPsSetzen() + - GruppenNamen: { GruppenId: Anzeigename } (neu gegenueber XMLConfig) + """ + # 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