Files
Plugin_SN_Verfahrensgebiet/functions/eigentuemer_korrektur.py
T
DanielandClaude Opus 5.5 e547cb90c5 Anzeige für den Eigentümer und Korrekturen im Tab „Eigentümer“ (#56)
„Daten anzeigen“ in der Trefferliste zeigt Name, Anschrift und E-Mail der
gewählten Einträge in einem eigenen Fenster: auf einem zweiten Bildschirm
im Vollbild, auf dem QGIS-Bildschirm als normales Fenster. Die Sitzung
wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten,
ohne das Fenster zu schließen. Konten, IDs und Kennzeichen erscheinen dort
nicht, alle Werte werden maskiert.

„Korrigieren …“ ändert angezeigte Daten. Die Eigentümerdatei bleibt
unverändert; jede Änderung wird mit Zeitpunkt, Konto, Personen-ID, altem
und neuem Wert an <Eigentümerdatei>_korrekturen.csv angehängt und sofort
angezeigt. Ohne beschreibbaren Ordner wird nach dem Speicherort gefragt.

In test_eigentuemer_tab.py sind die beim letzten Mal zusammengezogenen
with-Zeilen wieder umbrochen; die Trefferliste bekommt jetzt den
Anzeige-Rückruf, der Test dazu ist angepasst.

Dritter und letzter Schritt von #56.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:27:15 +02:00

129 lines
4.8 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.
"""sn_verfahrensgebiet/functions/eigentuemer_korrektur.py – Korrekturen beim Datenabgleich (#56).
Der Bearbeiter korrigiert im Plugin Name, Anschrift oder E-Mail eines
angezeigten Eigentümers. Die Eigentümerdatei (Serienbriefexport) bleibt
unverändert; jede Änderung wird als eigene Zeile an eine Korrektur-CSV
angehängt, standardmäßig ``<Eigentümerdatei>_korrekturen.csv`` im selben Ordner.
Spalten der Korrektur-CSV (UTF-8 mit BOM, Komma, wie der Export):
``Zeitpunkt, EIG_KONTO, PER_ID, Feld, Alter Wert, Neuer Wert``. ``Feld`` ist
der Spaltenname aus dem Export. Gehören mehrere Konten oder Personen-IDs zu
einem Eintrag, stehen sie durch Leerzeichen getrennt in einer Zelle.
**Datenschutz:** Die Korrektur-CSV enthält Personendaten. Sie liegt neben
dem Export und ist genauso zu schützen wie dieser. Das Modul schreibt
nichts auf die Konsole und zeigt keine Dialoge.
"""
from __future__ import annotations
import csv
import os
import re
from dataclasses import replace
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from urllib.parse import unquote, urlparse
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, SPALTEN
#: Korrigierbare Felder: (Schlüssel, Beschriftung). Der Spaltenname kommt aus SPALTEN.
FELDER: List[Tuple[str, str]] = [
("anrede", "Anrede"),
("titel", "Titel"),
("vorname", "Vorname"),
("nachname", "Name"),
("strasse", "Straße"),
("plz", "PLZ"),
("wohnort", "Wohnort"),
("ortsteil", "Ortsteil"),
("email", "E-Mail"),
]
KOPFZEILE = ["Zeitpunkt", "EIG_KONTO", "PER_ID", "Feld", "Alter Wert", "Neuer Wert"]
ENDUNG_KORREKTUR = "_korrekturen.csv"
Aenderung = Tuple[str, str, str] # (Schlüssel, alter Wert, neuer Wert)
def feldwerte(eigentuemer: Eigentuemer) -> Dict[str, str]:
"""Die korrigierbaren Werte eines Eintrags; mehrere E-Mails durch ``, `` getrennt."""
werte = {schluessel: getattr(eigentuemer, schluessel, "") for schluessel, _ in FELDER if schluessel != "email"}
werte["email"] = ", ".join(eigentuemer.emails)
return werte
def ermittle_aenderungen(eigentuemer: Eigentuemer, neue_werte: Dict[str, str]) -> List[Aenderung]:
"""Vergleicht die Eingaben mit dem Eintrag; Leerraum am Rand und doppelte Leerzeichen zählen nicht."""
alt = feldwerte(eigentuemer)
aenderungen: List[Aenderung] = []
for schluessel, _ in FELDER:
if schluessel not in neue_werte:
continue
neu = " ".join(str(neue_werte[schluessel] or "").split())
if neu != alt[schluessel]:
aenderungen.append((schluessel, alt[schluessel], neu))
return aenderungen
def wende_an(eigentuemer: Eigentuemer, aenderungen: List[Aenderung]) -> Eigentuemer:
"""Liefert den Eintrag mit den neuen Werten (für die Anzeige)."""
felder = {}
for schluessel, _alt, neu in aenderungen:
if schluessel == "email":
felder["emails"] = tuple(t.strip() for t in neu.split(",") if t.strip())
else:
felder[schluessel] = neu
return replace(eigentuemer, **felder)
def korrektur_pfad(layer_quelle: str) -> Optional[str]:
"""Vorgeschlagene Korrektur-CSV neben der Eigentümerdatei, oder ``None``.
Versteht die Quellangaben des Textdatei-Providers
(``file:///D:/ordner/export.csv?type=csv&…``) und des OGR-Providers
(``D:/ordner/export.csv|layername=export``).
"""
quelle = (layer_quelle or "").strip()
if not quelle:
return None
if quelle.lower().startswith("file:"):
pfad = unquote(urlparse(quelle).path)
if re.match(r"^/[A-Za-z]:", pfad):
pfad = pfad[1:]
else:
pfad = quelle.split("|", 1)[0]
stamm, endung = os.path.splitext(pfad)
if endung.lower() != ".csv":
return None
return stamm + ENDUNG_KORREKTUR
def schreibe_korrekturen(
pfad: str,
eigentuemer: Eigentuemer,
aenderungen: List[Aenderung],
zeitpunkt: Optional[datetime] = None,
) -> int:
"""Hängt die Änderungen an die Korrektur-CSV an und gibt die Zahl der Zeilen zurück.
Legt die Datei mit Kopfzeile an, wenn sie fehlt oder leer ist.
Raises
------
OSError
Die Datei lässt sich nicht schreiben.
"""
if not aenderungen:
return 0
stempel = (zeitpunkt or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
konten = " ".join(eigentuemer.konten)
per_ids = " ".join(eigentuemer.per_ids)
neu_anlegen = not os.path.exists(pfad) or os.path.getsize(pfad) == 0
with open(pfad, "a", encoding="utf-8-sig" if neu_anlegen else "utf-8", newline="") as datei:
schreiber = csv.writer(datei, delimiter=",", lineterminator="\r\n")
if neu_anlegen:
schreiber.writerow(KOPFZEILE)
for schluessel, alt, neu in aenderungen:
schreiber.writerow([stempel, konten, per_ids, SPALTEN[schluessel], alt, neu])
return len(aenderungen)