From a837ddc4d4b4b5294983a814c2f8be9b9268fb26 Mon Sep 17 00:00:00 2001 From: Handy-Insel Date: Mon, 2 Nov 2026 09:31:36 +0100 Subject: [PATCH] =?UTF-8?q?Vodafone:=20Zuordnung=20=C3=BCber=20Kundennamen?= =?UTF-8?q?=20in=20GROSSBUCHSTABEN,=20feste=20H=C3=A4ndler-Zuweisung,=20nu?= =?UTF-8?q?r=20erste=20Seite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- README.md | 11 ++++-- provtool/db.py | 63 +++++++++++++++++++++++++++++--- provtool/gui/app.py | 27 ++++++++++++-- provtool/gui/dialogs.py | 64 ++++++++++++++++++++++++++++----- provtool/importer.py | 17 +++++---- tests/test_kern.py | 80 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 97a591b..f5f5b79 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,13 @@ fertige Excel-Abrechnung. Optik: Windows 95. 1. **Händler anlegen** (`Strg+H`): Name, Standard-Auszahlung in % und die *Kennungen*: - freenet → Spalte **Unterhaendler** (z. B. `11111102`) - - Vodafone → Spalte **Vo-Nr** (z. B. `80000002`, `EU000001`) + - Vodafone → Spalte **Vo-Nr** (z. B. `80000002`, `EU000001`) **oder** + „Vodafone (Name in GROSSBUCHSTABEN)“: alle Vodafone-Positionen, deren Kundenname komplett + großgeschrieben ist (so erfasst unser Unterhändler seine Kunden). Diese Regel kann nur ein + Händler haben und hat Vorrang vor der Vo-Nr. Ein Händler kann mehrere Kennungen haben, auch bei beiden Anbietern. + Einzelne Fehltreffer: Positionen markieren → Rechtsklick ▸ **Händler zuweisen…** + (feste Zuweisung, Vorrang vor allem anderen; zurück mit „Automatisch zuordnen“). 2. **Neue Abrechnung** (`Strg+N`), z. B. „Oktober 2026“. 3. **Import** (`Strg+I`): beliebig viele Dateien auf einmal, z. B. alle freenet-Wochenlisten eines Monats plus die Vodafone-Datei. Das Format wird an der Kopfzeile erkannt. @@ -49,8 +54,8 @@ Speicherort änderbar unter *Datei ▸ Einstellungen*, Datensicherung über *Dat | Dienst | TarifKey02 | | Aktionscode | Text | -Bei Vodafone wird nur das Blatt **All** gelesen (die Einzelblätter je Abrechnungslauf würden -sonst doppelt zählen). +Bei Vodafone wird nur die **erste Seite** („All“) gelesen – die weiteren Blätter je +Abrechnungslauf würden sonst doppelt zählen. ## Entwicklung diff --git a/provtool/db.py b/provtool/db.py index b57296d..7fddb3c 100644 --- a/provtool/db.py +++ b/provtool/db.py @@ -11,7 +11,7 @@ from pathlib import Path from provtool.importer import ImportErgebnis from provtool.money import apply_percent -SCHEMA_VERSION = 1 +SCHEMA_VERSION = 2 SCHEMA = """ CREATE TABLE IF NOT EXISTS meta (schluessel TEXT PRIMARY KEY, wert TEXT); @@ -83,6 +83,16 @@ CREATE INDEX IF NOT EXISTS ix_position_abr ON position(abrechnung_id); UNZUGEORDNET = 0 # Pseudo-Händler-ID für Positionen ohne bekannte Kennung +# Sonder-Kennung: Vodafone-Positionen, deren Kundenname komplett in GROSSBUCHSTABEN steht +# (so erfasst ein Unterhändler seine Kunden). Hat Vorrang vor der VO-Nr. +CAPS_ANBIETER = "vodafone_caps" +CAPS_WERT = "*" + + +def ist_grossgeschrieben(name: str | None) -> bool: + buchstaben = [c for c in (name or "") if c.isalpha() and c != "ß"] + return len(buchstaben) >= 2 and all(c.isupper() for c in buchstaben) + @dataclass class Haendler: @@ -107,9 +117,18 @@ class Datenbank: self.con.row_factory = sqlite3.Row self.con.execute("PRAGMA foreign_keys = ON") self.con.executescript(SCHEMA) + self._migrieren() self.con.execute("INSERT OR IGNORE INTO meta VALUES ('schema_version', ?)", (str(SCHEMA_VERSION),)) self.con.commit() + def _migrieren(self): + spalten = {r[1] for r in self.con.execute("PRAGMA table_info(position)")} + if "haendler_fest" not in spalten: + # NULL = automatisch über Kennungen, sonst fest zugewiesene Händler-ID (0 = bewusst keiner) + self.con.execute("ALTER TABLE position ADD COLUMN haendler_fest INTEGER") + self.con.execute("UPDATE meta SET wert=? WHERE schluessel='schema_version'", (str(SCHEMA_VERSION),)) + self.con.commit() + def close(self): self.con.close() @@ -163,6 +182,9 @@ class Datenbank: besitzer = self.con.execute("SELECT h.name FROM kennung k JOIN haendler h ON h.id=k.haendler_id " "WHERE k.anbieter=? AND k.wert=?", (anbieter, wert)).fetchone() if besitzer: + if anbieter == CAPS_ANBIETER: + raise ValueError("Die Regel „Vodafone-Kundenname in GROSSBUCHSTABEN“ ist bereits dem " + f"Händler „{besitzer[0]}“ zugeordnet – sie kann nur einen Händler haben.") raise ValueError(f"Die Kennung {wert} ({anbieter}) ist bereits dem Händler „{besitzer[0]}“ zugeordnet.") self.con.execute("INSERT INTO kennung (haendler_id, anbieter, wert) VALUES (?,?,?)", (hid, anbieter, wert)) return hid @@ -179,6 +201,20 @@ class Datenbank: return {(r[0], r[1]): r[2] for r in self.con.execute( "SELECT k.anbieter, k.wert, k.haendler_id FROM kennung k JOIN haendler h ON h.id=k.haendler_id WHERE h.aktiv=1")} + def zuordner(self): + """Liefert eine Funktion (anbieter, kennung, name, haendler_fest) -> Händler-ID.""" + kmap = self.kennung_map() + caps = kmap.get((CAPS_ANBIETER, CAPS_WERT)) + aktive = {h.id for h in self.haendler_liste(nur_aktive=True)} + + def finde(anbieter: str, kennung: str, name: str | None = None, fest: int | None = None) -> int: + if fest is not None: + return fest if (fest == UNZUGEORDNET or fest in aktive) else UNZUGEORDNET + if caps and anbieter == "vodafone" and ist_grossgeschrieben(name): + return caps + return kmap.get((anbieter, kennung), UNZUGEORDNET) + return finde + # ------------------------------------------------------------------ Regeln def regeln(self, hid: int) -> dict[str, float]: return {r[0]: r[1] for r in self.con.execute( @@ -241,7 +277,7 @@ class Datenbank: self.con.execute("DELETE FROM abrechnung_datei WHERE id=?", (datei_id,)) def importieren(self, aid: int, erg: ImportErgebnis) -> int: - kmap = self.kennung_map() + finde = self.zuordner() start = self.con.execute("SELECT coalesce(max(reihenfolge), 0) FROM position WHERE abrechnung_id=?", (aid,)).fetchone()[0] with self.con: @@ -250,7 +286,7 @@ class Datenbank: (aid, erg.datei, erg.sha256, erg.format, _now(), len(erg.positionen))) did = cur.lastrowid for i, p in enumerate(erg.positionen, start=start + 1): - hid = kmap.get((p.anbieter, p.kennung), UNZUGEORDNET) + hid = finde(p.anbieter, p.kennung, p.name) proz = self.prozent_fuer(hid, p.provisionsart) self.con.execute( "INSERT INTO position (abrechnung_id, datei_id, reihenfolge, anbieter, zeitraum, provisionsart, kennung, " @@ -265,14 +301,31 @@ class Datenbank: def positionen(self, aid: int) -> list[dict]: """Alle Positionen mit aktueller Händler-Zuordnung (über die Kennungen).""" - kmap = self.kennung_map() + finde = self.zuordner() out = [] for r in self.con.execute("SELECT * FROM position WHERE abrechnung_id=? ORDER BY reihenfolge", (aid,)): d = dict(r) - d["haendler_id"] = kmap.get((d["anbieter"], d["kennung"]), UNZUGEORDNET) + d["haendler_id"] = finde(d["anbieter"], d["kennung"], d["name"], d["haendler_fest"]) out.append(d) return out + def haendler_zuweisen(self, pids: list[int], hid: int | None) -> None: + """Weist Positionen fest einem Händler zu (hid=None: wieder automatisch zuordnen). + Nicht manuell geänderte Beträge werden nach den Regeln des neuen Händlers berechnet.""" + with self.con: + for pid in pids: + self.con.execute("UPDATE position SET haendler_fest=? WHERE id=?", (hid, pid)) + finde = self.zuordner() + with self.con: + for pid in pids: + r = self.con.execute("SELECT * FROM position WHERE id=?", (pid,)).fetchone() + if r is None or r["manuell"]: + continue + neu_hid = finde(r["anbieter"], r["kennung"], r["name"], r["haendler_fest"]) + proz = self.prozent_fuer(neu_hid, r["provisionsart"]) + self.con.execute("UPDATE position SET betrag=?, prozent=? WHERE id=?", + (apply_percent(r["betrag_orig"], proz), proz, pid)) + def betrag_setzen(self, pid: int, cent: int) -> None: with self.con: self.con.execute("UPDATE position SET betrag=?, manuell=1, prozent=NULL WHERE id=?", (cent, pid)) diff --git a/provtool/gui/app.py b/provtool/gui/app.py index e2298da..f5f29d6 100644 --- a/provtool/gui/app.py +++ b/provtool/gui/app.py @@ -116,6 +116,8 @@ class App: h = tk.Menu(m, tearoff=0) h.add_command(label="Händler verwalten…", accelerator="Strg+H", command=self.haendler) h.add_command(label="Kennung des Eintrags zuordnen…", command=self.kennung_zuordnen_auswahl) + h.add_command(label="Auswahl fest einem Händler zuweisen…", command=self.haendler_zuweisen) + h.add_command(label="Auswahl automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True)) m.add_cascade(label="Händler", menu=h, underline=0) hi = tk.Menu(m, tearoff=0) @@ -328,7 +330,8 @@ class App: kunde = " ".join(x for x in (p["vorname"], p["name"]) if x) or "(ohne Name)" t.insert(giid, "end", iid=f"p{p['id']}", text=kunde, values=(_datum_de(p["datum"]), p["rufnummer"], p["tarif"], _zeitraum_kurz(p["zeitraum"]), - p["kennung"], fmt(p["betrag_orig"]), + p["kennung"] + (" (fest)" if p["haendler_fest"] is not None else ""), + fmt(p["betrag_orig"]), "manuell" if p["manuell"] else fmt_percent(p["prozent"]), fmt(p["betrag"])), tags=tuple(tags)) name = {ALLE: "Alle Positionen", UNZUGEORDNET: "Nicht zugeordnet"}.get( @@ -641,11 +644,28 @@ class App: m.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen) m.add_separator() m.add_command(label="Auszahlung 0 € (nicht auszahlen)", command=lambda: self._null()) + m.add_separator() + m.add_command(label="Händler zuweisen…", command=self.haendler_zuweisen) + m.add_command(label="Automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True)) if self.ansicht == UNZUGEORDNET or self.ansicht == ALLE: - m.add_separator() m.add_command(label="Kennung einem Händler zuordnen…", command=self.kennung_zuordnen_auswahl) m.tk_popup(e.x_root, e.y_root) + def haendler_zuweisen(self, automatisch: bool = False): + sel, pids, _ = self._auswahl_info() + if not pids: + w.info(self.root, "Händler zuweisen", "Bitte zuerst Positionen oder eine Gruppe auswählen.") + return + if automatisch: + hid = None + else: + hid = dlg.HaendlerWaehlen(self.root, self.db, len(pids)).zeigen() + if hid is None: + return + self.db.haendler_zuweisen(pids, hid) + self.db.touch(self.aid) + self._aktualisieren() + def _null(self): sel, pids, _ = self._auswahl_info() self.db.prozent_anwenden(pids, 0.0) @@ -746,7 +766,8 @@ class App: def hilfe(self): w.info(self.root, "Kurzanleitung", "1. Händler anlegen (Strg+H): Name, Standard-Auszahlung in % und die Kennungen –\n" - " bei freenet die Unterhändlernummer, bei Vodafone die VO-Nr.\n\n" + " bei freenet die Unterhändlernummer, bei Vodafone die VO-Nr. oder die Regel\n" + " „Name in GROSSBUCHSTABEN“. Einzelne Positionen: Rechtsklick ▸ Händler zuweisen.\n\n" "2. Neue Abrechnung (Strg+N), z. B. „Oktober 2026“.\n\n" "3. Dateien importieren (Strg+I) – mehrere CSV/Excel auf einmal möglich,\n" " auch alle Wochenlisten eines Monats.\n\n" diff --git a/provtool/gui/dialogs.py b/provtool/gui/dialogs.py index a57a172..60dadfe 100644 --- a/provtool/gui/dialogs.py +++ b/provtool/gui/dialogs.py @@ -5,9 +5,14 @@ import tkinter as tk from tkinter import filedialog, ttk from provtool import APP_NAME, APP_ORG, __version__ -from provtool.db import Datenbank, Haendler +from provtool.db import CAPS_ANBIETER, CAPS_WERT, Datenbank, Haendler from provtool.gui import win95 as w from provtool.importer import ANBIETER + +# Auswahl im Händler-Dialog: Schlüssel → Anzeige +KENNUNG_ARTEN = {"freenet": "freenet (Unterhändler-Nr.)", "vodafone": "Vodafone (VO-Nr.)", + CAPS_ANBIETER: "Vodafone (Name in GROSSBUCHSTABEN)"} +CAPS_TEXT = "Kunden-Nachname komplett in GROSSBUCHSTABEN" from provtool.money import fmt, fmt_percent, parse_percent @@ -68,15 +73,18 @@ class HaendlerDialog(w.Dialog): self.kliste.heading("anbieter", text="Anbieter") self.kliste.heading("wert", text="Unterhändler-Nr. / VO-Nr.") self.kliste.column("anbieter", width=90) - self.kliste.column("wert", width=200) + self.kliste.column("wert", width=260) r2.pack(fill="both", expand=True) kf = tk.Frame(g2, bg=w.FACE) kf.pack(fill="x", pady=(4, 0)) - self.v_kanb = tk.StringVar(value="freenet") - cb = ttk.Combobox(kf, textvariable=self.v_kanb, values=list(ANBIETER.values()), width=10, state="readonly") + self.v_kanb = tk.StringVar(value=KENNUNG_ARTEN["freenet"]) + cb = ttk.Combobox(kf, textvariable=self.v_kanb, values=list(KENNUNG_ARTEN.values()), width=34, + state="readonly") cb.pack(side="left") + cb.bind("<>", lambda e: self._art_gewaehlt()) self.v_kwert = tk.StringVar() - ke = tk.Entry(kf, textvariable=self.v_kwert, width=18) + ke = tk.Entry(kf, textvariable=self.v_kwert, width=14) + self._ke = ke ke.pack(side="left", padx=4) ke.bind("", lambda e: self._kennung_dazu()) w.knopf(kf, "Hinzufügen", self._kennung_dazu).pack(side="left") @@ -111,7 +119,12 @@ class HaendlerDialog(w.Dialog): # ---- intern @staticmethod def _anbieter_key(anzeige: str) -> str: - return next((k for k, v in ANBIETER.items() if v == anzeige), anzeige) + return next((k for k, v in KENNUNG_ARTEN.items() if v == anzeige), anzeige) + + def _art_gewaehlt(self): + caps = self._anbieter_key(self.v_kanb.get()) == CAPS_ANBIETER + self.v_kwert.set("") + self._ke.configure(state="disabled" if caps else "normal") def _liste_laden(self, auswahl_id=None): self.liste.delete(*self.liste.get_children()) @@ -147,7 +160,10 @@ class HaendlerDialog(w.Dialog): def _kennungen_zeigen(self): self.kliste.delete(*self.kliste.get_children()) for i, (a, v) in enumerate(self._kennungen): - self.kliste.insert("", "end", iid=str(i), values=(ANBIETER.get(a, a), v)) + if a == CAPS_ANBIETER: + self.kliste.insert("", "end", iid=str(i), values=("Vodafone", CAPS_TEXT)) + else: + self.kliste.insert("", "end", iid=str(i), values=(ANBIETER.get(a, a), v)) def _regeln_zeigen(self): self.rliste.delete(*self.rliste.get_children()) @@ -168,10 +184,10 @@ class HaendlerDialog(w.Dialog): self._regeln_zeigen() def _kennung_dazu(self): - wert = self.v_kwert.get().strip() + anb = self._anbieter_key(self.v_kanb.get()) + wert = CAPS_WERT if anb == CAPS_ANBIETER else self.v_kwert.get().strip() if not wert: return - anb = self._anbieter_key(self.v_kanb.get()) if (anb, wert) not in self._kennungen: self._kennungen.append((anb, wert)) self.v_kwert.set("") @@ -486,6 +502,36 @@ class KennungZuordnen(w.Dialog): self.destroy() +class HaendlerWaehlen(w.Dialog): + """Feste Zuweisung ausgewählter Positionen zu einem Händler.""" + + KEINER = "(keinem Händler – nicht auszahlen)" + + def __init__(self, master, db: Datenbank, anzahl: int): + super().__init__(master, "Händler zuweisen") + self._hmap = {h.name: h.id for h in db.haendler_liste(nur_aktive=True)} + tk.Label(self, text=f"{anzahl} Position(en) fest zuweisen an:", anchor="w").pack(fill="x", padx=12, + pady=(12, 4)) + self.cb = ttk.Combobox(self, values=list(self._hmap) + [self.KEINER], state="readonly", width=40) + self.cb.pack(padx=12, pady=4) + tk.Label(self, text="Die feste Zuweisung hat Vorrang vor Kennungen und der GROSSBUCHSTABEN-Regel.\n" + "Rückgängig: Rechtsklick ▸ „Automatisch zuordnen“.", justify="left", + fg=w.SHADOW).pack(fill="x", padx=12) + self.bind("", lambda e: self._ok()) + self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=12) + + def _ok(self): + name = self.cb.get() + if not name: + w.warnung(self, "Händler zuweisen", "Bitte einen Händler auswählen.") + return + self.result = 0 if name == self.KEINER else self._hmap[name] + self.destroy() + + def zeigen(self, fokus=None): + return super().zeigen(self.cb) + + # =========================================================================== Export class ExportDialog(w.Dialog): diff --git a/provtool/importer.py b/provtool/importer.py index 69ba519..bbe69bf 100644 --- a/provtool/importer.py +++ b/provtool/importer.py @@ -288,13 +288,16 @@ def lies_datei(path: str | Path) -> ImportErgebnis: ) warnungen: list[str] = [] - # Vodafone/Euronics: Das Blatt "All" enthält alle Abrechnungsläufe. Sind zusätzlich - # Einzelblätter befüllt, würden Positionen doppelt gezählt – daher nur "All" verwenden. - alle = [g for g in gefunden if g[0].lower() == "all"] - if alle and len(gefunden) > 1: - uebrig = [g[0] for g in gefunden if g is not alle[0]] - warnungen.append(f"Nur Blatt „All“ übernommen, Einzelblätter ignoriert: {', '.join(uebrig)}") - gefunden = alle + # Vodafone/Euronics: Maßgeblich ist nur die erste Seite ("All") – sie enthält alle + # Abrechnungsläufe. Weitere Blätter würden Positionen doppelt zählen. + if any(f.key == VodafoneFormat.key for _, f, _ in gefunden): + erstes = blaetter[0][0] + uebrig = [g[0] for g in gefunden if g[0] != erstes] + gefunden = [g for g in gefunden if g[0] == erstes] + if uebrig: + warnungen.append(f"Nur die erste Seite „{erstes}“ übernommen, ignoriert: {', '.join(uebrig)}") + if not gefunden: + raise ImportFehler(f"Auf der ersten Seite von „{path.name}“ wurde keine Vodafone-Abrechnung gefunden.") positionen = [p for _, _, ps in gefunden for p in ps] formate = sorted({f.key for _, f, _ in gefunden}) diff --git a/tests/test_kern.py b/tests/test_kern.py index 05f71ab..5f33f72 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -185,3 +185,83 @@ def test_datei_doppelt_erkannt_und_entfernbar(db, freenet_csv): did = db.dateien(aid)[0]["id"] db.datei_entfernen(did) assert db.positionen(aid) == [] + + +# --------------------------------------------------------------------------- Vodafone: GROSSBUCHSTABEN-Regel + +def test_grossbuchstaben_erkennung(): + from provtool.db import ist_grossgeschrieben + assert ist_grossgeschrieben("HUBERMEIER") + assert ist_grossgeschrieben("ÖSTERLEHNER ") + assert ist_grossgeschrieben("GROß") # ß gibt es nicht als Großbuchstabe + assert not ist_grossgeschrieben("Musterfrau") + assert not ist_grossgeschrieben("Max & Moritz Test") + assert not ist_grossgeschrieben("") + assert not ist_grossgeschrieben("X") + + +@pytest.fixture +def vodafone_caps_xlsx(tmp_path): + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "All" + kopf = ["Anschaltdatum", "Kartennummer", "IMEI", "Rufnummer", "Kunde", "Artikelbezeichnung", "Tarif", + "TarifKey02", "Prämie in EUR", "Abrechnungslauf", "Vo-Nr", "Lieferantennr", "Text"] + for c, h in enumerate(kopf, 1): + ws.cell(6, c, h) + for r, (kunde, betrag) in enumerate([("HUBERMEIER ", 295), ("Musterfrau", 370), ("BEISPIELER", 235)], 7): + for c, v in enumerate((datetime(2026, 9, 1), "1", "", "4917", kunde, "Vergütung aus VVL otelo", "Tarif", + "otelo Allnet Flat", betrag, "VF2609", "80000001", "700000", "10000"), 1): + ws.cell(r, c, v) + # zweite Seite mit Daten – darf NICHT eingelesen werden + ws2 = wb.create_sheet("VF2609") + for c, h in enumerate(kopf, 1): + ws2.cell(5, c, h) + for c, v in enumerate((datetime(2026, 9, 1), "1", "", "4917", "DOPPELT", "Vergütung aus VVL otelo", "T", + "x", 999, "VF2609", "80000001", "700000", ""), 1): + ws2.cell(6, c, v) + p = tmp_path / "vf_caps.xlsx" + wb.save(p) + return p + + +def test_vodafone_nur_erste_seite(vodafone_caps_xlsx): + erg = lies_datei(vodafone_caps_xlsx) + assert [p.name for p in erg.positionen] == ["HUBERMEIER", "Musterfrau", "BEISPIELER"] + assert erg.warnungen + + +def test_caps_regel_und_feste_zuweisung(db, vodafone_caps_xlsx): + from provtool.db import CAPS_ANBIETER, CAPS_WERT + laden = db.haendler_speichern(Haendler(0, "Eigener Laden"), [("vodafone", "80000001")]) + uh = db.haendler_speichern(Haendler(0, "Unterhändler Caps", standard_prozent=90), [(CAPS_ANBIETER, CAPS_WERT)]) + with pytest.raises(ValueError): + db.haendler_speichern(Haendler(0, "Noch einer"), [(CAPS_ANBIETER, CAPS_WERT)]) + aid = db.abrechnung_neu("September") + db.importieren(aid, lies_datei(vodafone_caps_xlsx)) + pos = {p["name"]: p for p in db.positionen(aid)} + assert pos["HUBERMEIER"]["haendler_id"] == uh and pos["BEISPIELER"]["haendler_id"] == uh + assert pos["Musterfrau"]["haendler_id"] == laden + assert pos["HUBERMEIER"]["betrag"] == 26550 # 90 % von 295 € + + # Fehltreffer: BEISPIELER gehört eigentlich zum Laden → fest zuweisen + db.haendler_zuweisen([pos["BEISPIELER"]["id"]], laden) + mayr = next(p for p in db.positionen(aid) if p["name"] == "BEISPIELER") + assert mayr["haendler_id"] == laden and mayr["betrag"] == 23500 + # zurück auf automatisch + db.haendler_zuweisen([mayr["id"]], None) + mayr = next(p for p in db.positionen(aid) if p["name"] == "BEISPIELER") + assert mayr["haendler_id"] == uh and mayr["betrag"] == 21150 + + +def test_migration_alter_datenbank(tmp_path): + import sqlite3 + from provtool.db import SCHEMA + pfad = tmp_path / "alt.db" + con = sqlite3.connect(pfad) + con.executescript(SCHEMA) # Schema v1 ohne Spalte haendler_fest + con.close() + d = Datenbank(pfad) + spalten = {r[1] for r in d.con.execute("PRAGMA table_info(position)")} + assert "haendler_fest" in spalten + d.close()