PErsonendaten auch als .csv Einlesen
This commit is contained in:
@@ -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 <Tisch> in bestellung.xml).
|
||||||
|
VIPs laufen nicht durch die GA-Platzierung.
|
||||||
|
- Anzahl: optionale Reservierungsanzahl — expandiert die Zeile in so
|
||||||
|
viele identische Buchungen (wie <Anzahl> 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
|
||||||
Reference in New Issue
Block a user