Files
Mieterportal/server/import/import-masterdata.py
T

182 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Einmaliger Import der Mieter-Stammdaten aus der SharePoint-Export-CSV
(„Master Data V") in die PostgreSQL-Datenbank des Mieterportals.
Liest die CSV von stdin, schreibt SQL auf stdout:
python import-masterdata.py < \"Master Data V.csv\" | \\
docker exec -i mp-db psql -U mp -d mieterportal
ACHTUNG: Ersetzt die Demo-Daten (Häuser, Wohnungen, Mieter, Buchungen,
Meldungen, Unterhaltungen, Dokumente, Zähler, Kosten, Belege, Codes).
Verwaltungs-Konten, Ankündigungen und Sessions der Verwaltung bleiben.
IDs sind deterministisch (Slug aus Haus + WE) – das Skript ist damit
fehlerverträglich wiederholbar, solange die CSV unverändert bleibt.
Die CSV gehört NICHT ins Git (PII) – siehe .gitignore.
"""
import csv
import io
import re
import secrets
import sys
from datetime import date
HEUTE = f'{date.today().day}.{date.today().month}.{date.today().year}'
def q(wert):
"""SQL-Escaping für Textfelder."""
if wert is None:
return 'NULL'
return "'" + str(wert).replace("'", "''") + "'"
def slug(text):
s = str(text or '').strip().lower()
s = (s.replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue').replace('ß', 'ss'))
s = re.sub(r'[^a-z0-9]+', '-', s).strip('-')
return s or 'x'
def zahl(wert):
"""Deutsche Zahlenformate: '2.700' -> 2700, '71,4' -> 71.4, '' -> 0"""
s = str(wert or '').strip()
if not s:
return 0.0
s = s.replace('.', '').replace(',', '.')
try:
return float(s)
except ValueError:
return 0.0
def jahr(wert):
"""'1.905' -> 1905, leer/0 -> None"""
s = str(wert or '').strip()
if not s:
return None
s = s.replace('.', '')
try:
j = int(s)
except ValueError:
return None
return j if 1500 < j < 2100 else None
def etage(lage):
s = (lage or '').strip().lower()
if s.startswith('eg') or 'hinterhof' in s:
return 0
m = re.search(r'(\d+)\.\s*og', s)
if m:
return int(m.group(1))
if s.startswith('dg'):
return 4
return 0
def wohnungsnummer(we):
"""'WE01' -> '1', 'Garage 1' bleibt, 'Zimmer 2' bleibt."""
s = str(we or '').strip()
m = re.match(r'^WE0*(\d+)$', s, re.IGNORECASE)
if m:
return str(int(m.group(1)))
return s
def main():
eingabe = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8-sig')
zeilen = list(csv.DictReader(eingabe))
if not zeilen:
sys.exit('CSV ist leer oder konnte nicht gelesen werden.')
sql = []
sql.append('BEGIN;')
# buildings: NOT NULL lockern – die CSV liefert nicht alle Pflichtfelder
for spalte in ('baujahr', 'stockwerke', 'etagenhoehe', 'wohneinheiten', 'quadratmeter'):
sql.append(f'ALTER TABLE buildings ALTER COLUMN {spalte} DROP NOT NULL;')
# ── Demo-Daten entfernen (Reihenfolge wegen Fremdschlüsseln) ──
sql += [
'DELETE FROM threads;',
'DELETE FROM tickets;',
'DELETE FROM transactions;',
'DELETE FROM costs;',
'DELETE FROM belege;',
'DELETE FROM documents;',
'DELETE FROM meters;',
'DELETE FROM registration_codes;',
"DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE rolle = 'mieter');",
"DELETE FROM users WHERE rolle = 'mieter';",
'DELETE FROM units;',
'DELETE FROM buildings;',
]
# ── Häuser ──
haeuser = []
for z in zeilen:
t = z['Titel'].strip()
if t and t not in haeuser:
haeuser.append(t)
for t in haeuser:
zeilen_haus = [z for z in zeilen if z['Titel'].strip() == t]
we_zeilen = [z for z in zeilen_haus if z['WE'].strip()]
strasse = zeilen_haus[0]['Straße'].strip()
plz = zeilen_haus[0]['PLZ'].strip().replace('.', '')
stadt = zeilen_haus[0]['Stadt'].strip()
adresse = ', '.join(x for x in (strasse, f'{plz} {stadt}'.strip()) if x)
bj = jahr(zeilen_haus[0]['Baujahr'])
qm = sum(zahl(z['m²']) for z in we_zeilen) or None
wohnungen = len(we_zeilen)
sql.append(
'INSERT INTO buildings (name, adresse, baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter)'
f' VALUES ({q(t)}, {q(adresse)}, {bj if bj else "NULL"}, NULL, NULL, {wohnungen}, {qm if qm else "NULL"});',
)
# ── Wohneinheiten + Mieter ──
warnungen = []
for z in zeilen:
we = z['WE'].strip()
if not we:
continue
titel = z['Titel'].strip()
einheit_id = f"{slug(titel)}-{slug(we)}"
nummer = wohnungsnummer(we)
mieter = z['Name des Mieters'].strip()
kalt = zahl(z['Nettokaltmiete'])
warm = kalt + zahl(z['Nebenkosten'])
sql.append(
'INSERT INTO units (id, building, number, floor, size, cold_rent, warm_rent, tenant, status)'
f' VALUES ({q(einheit_id)}, {q(titel)}, {q(nummer)}, {etage(z["Lage im Objekt"])},'
f' {zahl(z["m²"])}, {kalt}, {warm}, {q(mieter) if mieter else "NULL"},'
f" '{('vermietet' if mieter else 'frei')}');",
)
if mieter:
nr = z['Nr. Mietpartei'].strip() or slug(we)
benutzername = f"{slug(titel)}-{nr}"
# Zufälliger unbekannter Hash -> Mieter kann sich erst nach
# Passwort-Reset durch die Verwaltung (Button in /benutzer) anmelden.
passwort_hash = secrets.token_hex(32)
sql.append(
'INSERT INTO users (id, benutzername, name, passwort_hash, rolle, email, einheit_id, aktiv, erstellt_am)'
f" VALUES ({q('b-' + einheit_id)}, {q(benutzername)}, {q(mieter)}, {q(passwort_hash)},"
f" 'mieter', '', {q(einheit_id)}, TRUE, {q(HEUTE)});",
)
# Zähler werden NICHT importiert: die CSV-Zählerspalten sind leer/'0'
if z['Stromzähler'].strip() not in ('', '0'):
warnungen.append(f"Stromzähler in {titel}/{we}: {z['Stromzähler'].strip()} (nicht importiert)")
sql.append('COMMIT;')
sys.stdout.write('\n'.join(sql) + '\n')
print(f'-- {len(haeuser)} Häuser, '
f"{sum(1 for z in zeilen if z['WE'].strip())} Wohneinheiten, "
f"{sum(1 for z in zeilen if z['Name des Mieters'].strip())} Mieter", file=sys.stderr)
for w in warnungen:
print('-- HINWEIS: ' + w, file=sys.stderr)
if __name__ == '__main__':
main()