CSVConfig.HandleGaesteliste liefert zusaetzlich FixierteGruppen (Name -> Tisch/Anzahl), damit fest platzierte Gaeste in der Gaesteliste sichtbar (grau hinterlegt) erscheinen. Durchgaengig "VIP" -> "fixiert" umbenannt. Backend: Strafprofile werden gecacht geladen, neuer Endpoint /strafprofile und Query-Parameter strafen an /berechnen, damit sich Profile (default vs. locker) in der GUI vergleichen lassen. SVG-Ausgabe nutzt das Farbschema aus farben.cfg (Rueckfall auf Palette). Frontend: tische.ini und CSV werden bei Auswahl sofort geladen, Auswahl der fixierten Gruppen und des Optimierungsprofils in der Oberflaeche. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
202 lines
6.7 KiB
Python
202 lines
6.7 KiB
Python
"""
|
||
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 = {}
|
||
# fest platzierte ("fixierte") Gruppen fuer die Anzeige: Name ->
|
||
# {'tisch': Tischnummer, 'anzahl': Personenzahl}. VIPs laufen nicht
|
||
# durch die GA-Platzierung, sollen in der Gaesteliste aber sichtbar
|
||
# (grau hinterlegt) erscheinen.
|
||
FixierteGruppen = {}
|
||
|
||
# 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:
|
||
# fixiert: fester Tisch, keine GA-Platzierung
|
||
VipHash[ PID ] = TischNr
|
||
VIPGruppe.Person_dazu( P )
|
||
Name = GruppenName if GruppenName else 'Fixiert Tisch %d' % TischNr
|
||
Eintrag = FixierteGruppen.setdefault(
|
||
Name, { 'tisch': TischNr, 'anzahl': 0 } )
|
||
Eintrag['anzahl'] = Eintrag['anzahl'] + 1
|
||
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, FixierteGruppen ]
|
||
|
||
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, FixierteGruppen ] = \
|
||
C.HandleGaesteliste( Beispiel )
|
||
print(PN)
|
||
print(GN)
|
||
print(VIPGruppe)
|
||
print(VipHash)
|
||
print(GruppenNamen)
|
||
print(FixierteGruppen)
|
||
|
||
else:
|
||
pass
|