Eigene Kennungen ausblenden, Win95-Menüs, verschiebbares Hauptfenster bei offenem Dialog u. a.

Neu:
- Eigene Kennungen (eigene Filiale) ausblenden: nicht angezeigt, nicht mitgerechnet, nicht
  exportiert; per Rechtsklick, Händler-Menü oder Import-Bericht; Händler-Zuordnung hat Vorrang
- Menüs im Win95-Stil: wechseln beim Überfahren der Menüleiste, Tastatur, Kontextmenü gleich
- Dialoge sperren nur den Inhalt: Hauptfenster bleibt verschiebbar/maximierbar; Dialoge haben
  eigenen Taskleisten-Eintrag; Klick auf gesperrten Inhalt holt den Dialog nach vorn (blinkt)
- Trennlinie zwischen Händler- und Positionsliste sichtbar und ziehbar, Position wird gemerkt
- Strg+Rücktaste / Strg+Entf löschen ganze Wörter in allen Eingabefeldern

Behoben:
- Gedrückte Knöpfe zeigten keine Beschriftung (activeForeground gehört in Tk zur Klasse Background)
- Tipp des Tages: Text lief aus dem sichtbaren Bereich
- Größenänderung: breiterer Griffbereich mit Ecken, Zeiger wird zuverlässig zurückgesetzt,
  flüssiger; Verschieben über Windows selbst (inkl. Andocken), Minimieren über Windows
- Zeichen, die MS Sans Serif nicht hat (…, ▸, →, •), durch Win95-übliche ersetzt
- Menü/Tooltip/Dialog: Größe+Position zusammen setzen (Tk ignoriert sonst die Position)
- Suche reagiert erst nach kurzer Tipp-Pause (flüssigeres Tippen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-11-02 12:58:18 +01:00
1 parent 2b2d5e8580
commit dcb1d0378c
15 files changed
+1217 -187

No files matched your search

+279 -21
View File
@@ -61,6 +61,23 @@ class Lauf:
# eine evtl. geöffnete Fehlermeldung schließen, damit der Test nicht hängt
self.root.after(100, self._meldungen_zu)
self.root.report_callback_exception = melden
self._warten_bis_sichtbar()
def _warten_bis_sichtbar(self, sekunden: float = 8.0):
"""Wie ein Mensch erst bedienen, wenn das Hauptfenster zu sehen ist: Unter Windows wird es beim
Start für den Taskleisten-Eintrag kurz aus- und wieder eingeblendet (unter Wine langsamer)."""
ende = time.time() + sekunden
stabil = 0
while time.time() < ende:
try:
self.root.update()
sichtbar = self.root.winfo_viewable() and self.app.inhalt.winfo_height() > 50
except tk.TclError:
return
stabil = stabil + 1 if sichtbar else 0
if stabil >= 10: # ~0,1 s durchgehend sichtbar
return
time.sleep(0.01)
# ---------------------------------------------------------------- Zustand
def lebt_sonst(self, text: str) -> str:
@@ -155,6 +172,10 @@ class Lauf:
time.sleep(0.01)
self.uebrig = [s[2] for s in offen]
def diagnose(self) -> str:
"""Für Fehlermeldungen: welche Schritte nicht liefen + alle gesammelten Fehler."""
return f"nicht ausgeführt: {self.uebrig}\n" + "\n".join(self.fehler)
def schliessen_ueber_fenster(self):
"""Wie Klick auf × / Taskleiste ▸ Fenster schließen (WM_DELETE_WINDOW)."""
cmd = self.root.tk.call("wm", "protocol", self.root._w, "WM_DELETE_WINDOW")
@@ -233,7 +254,7 @@ def test_alt_f4_im_assistent_fragt_und_schliesst_nur_assistent(home):
(lambda: lauf.dialog(Einrichtungsassistent) is None, lambda: None, "Assistent ist zu"),
])
assert lauf.lebt, lauf.lebt_sonst("Programm darf nicht beendet werden")
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
assert lauf.app.settings.get("assistent_erledigt") is True
lauf.pruefen()
lauf.ende()
@@ -342,7 +363,6 @@ def test_alt_f4_bevor_die_sperre_steht(lauf_mit_daten, monkeypatch):
Zeichnen sind noch nicht fertig. Alt+F4 muss trotzdem nur den Dialog schließen."""
from provtool.gui import win95 as w
from provtool.gui.dialogs import InfoDialog
monkeypatch.setattr(w.Dialog, "_sperre_setzen", lambda self, versuch=0: None) # keine Sperre
monkeypatch.setattr(w.Dialog, "winfo_ismapped", lambda self: 0) # „noch nicht gezeichnet“
lauf = lauf_mit_daten
lauf.ablauf([
@@ -353,7 +373,7 @@ def test_alt_f4_bevor_die_sperre_steht(lauf_mit_daten, monkeypatch):
(lambda: lauf.dialog(InfoDialog) is None, lambda: None, "Dialog geschlossen"),
], sekunden=8)
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 darf bei offenem Dialog nicht das ganze Programm beenden")
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
lauf.pruefen()
@@ -367,7 +387,7 @@ def test_alt_f4_im_rundgang_beendet_nur_den_rundgang(lauf_mit_daten):
(lambda: rg().blase is None and not rg().rahmen, lambda: None, "Rundgang samt Rahmen weg"),
], sekunden=8)
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 im Rundgang darf nicht das Programm beenden")
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
lauf.pruefen()
@@ -396,7 +416,7 @@ def test_betrag_dialog_wenn_zelle_unsichtbar(lauf_mit_daten):
(lambda: bool(app.ptree.get_children()), oeffnen, "Betrag ändern bei unsichtbarer Zelle"),
(lambda: eingabe() is not None, tippen, "Betrag im Dialog eingeben"),
])
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
assert any(p["betrag"] == 777 and p["manuell"] for p in app.positionen)
del app.ptree.bbox
lauf.pruefen()
@@ -533,7 +553,7 @@ def test_windows_popup_kommt_nach_alt_tab_wieder_nach_vorn(lauf_mit_daten):
fremd.geometry("400x300+50+50")
fremd.update()
m.update(besitzer=win32.besitzer_von(d), haupt=win32.hwnd(lauf.root),
haupt_bedienbar=win32.ist_bedienbar(lauf.root))
haupt_bedienbar=win32.ist_bedienbar(lauf.root), taskleiste=win32.hat_taskleisteneintrag(d))
win32.nach_vorn(fremd) # „woanders hingeklickt“
lauf.root.update()
m["woanders"] = win32.stapel(d, lauf.root, fremd)
@@ -548,34 +568,38 @@ def test_windows_popup_kommt_nach_alt_tab_wieder_nach_vorn(lauf_mit_daten):
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, messen), "messen"),
(lambda: "zurueck" in m and lauf.dialog(InfoDialog) is None, lambda: None, "geschlossen"),
], sekunden=10)
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
assert m["besitzer"] == m["haupt"], "Popup gehört nicht dem Hauptfenster"
assert m["haupt_bedienbar"] is False, "Hauptfenster muss gesperrt sein, solange das Popup offen ist"
assert m["haupt_bedienbar"] is True, "Hauptfenster soll verschiebbar bleiben (nur der Inhalt ist gesperrt)"
assert m["taskleiste"] is True, "Popup braucht einen eigenen Eintrag in der Taskleiste"
assert m["woanders"][0] == 2, f"Testaufbau: das fremde Fenster sollte oben liegen ({m['woanders']})"
assert m["zurueck"][:2] == [0, 1], f"Popup liegt nach dem Zurückholen nicht über dem Hauptfenster: {m['zurueck']}"
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
lauf.pruefen()
@nur_windows
def test_windows_verschachtelte_rueckfrage(lauf_mit_daten):
"""Händler-Dialog → Rückfrage: Rückfrage gehört dem Händler-Dialog; beim Schließen der Reihe nach frei."""
from provtool.gui import win32
def test_verschachtelte_rueckfrage_sperrt_der_reihe_nach(lauf_mit_daten):
"""Händler-Dialog -> Rückfrage: gesperrt sind Hauptfenster-Inhalt und Händler-Dialog (nicht dessen
Titelleiste); nach der Rückfrage ist der Händler-Dialog wieder frei, das Hauptfenster noch nicht."""
from provtool.gui import modal, win32
from provtool.gui import win95 as w
from provtool.gui.dialogs import HaendlerDialog
lauf = lauf_mit_daten
m = {}
rueckfrage = lambda: next((d for d in lauf.dialoge() if d.title() == "Test-Rückfrage"), None) # noqa: E731
gesperrt = lambda wdg: modal.TAG in wdg.bindtags() # noqa: E731
def messen_innen():
h, r = lauf.dialog(HaendlerDialog), rueckfrage()
m.update(r_besitzer=win32.besitzer_von(r), h=win32.hwnd(h), h_an=win32.ist_bedienbar(h),
haupt_an=win32.ist_bedienbar(lauf.root))
m.update(h_inhalt=gesperrt(h.liste), h_titel=gesperrt(h.titelleiste) if h.titelleiste else False,
haupt_inhalt=gesperrt(lauf.app.ptree), haupt_titel=gesperrt(lauf.app.rahmen.titelleiste)
if lauf.app.rahmen.titelleiste else False,
r_besitzer=win32.besitzer_von(r), h=win32.hwnd(h))
r.destroy()
def messen_aussen():
h = lauf.dialog(HaendlerDialog)
m.update(h_an_danach=win32.ist_bedienbar(h), haupt_an_danach=win32.ist_bedienbar(lauf.root))
m.update(h_inhalt_danach=gesperrt(h.liste), haupt_inhalt_danach=gesperrt(lauf.app.ptree))
h.abbrechen()
lauf.ablauf([
@@ -586,11 +610,13 @@ def test_windows_verschachtelte_rueckfrage(lauf_mit_daten):
(lambda: "h" in m and rueckfrage() is None, messen_aussen, "außen messen"),
(lambda: lauf.dialog(HaendlerDialog) is None, lambda: None, "alles zu"),
], sekunden=12)
assert not lauf.uebrig, lauf.uebrig
assert m["r_besitzer"] == m["h"], "Rückfrage gehört nicht dem Händler-Dialog"
assert m["h_an"] is False and m["haupt_an"] is False
assert m["h_an_danach"] is True and m["haupt_an_danach"] is False
assert win32.ist_bedienbar(lauf.root)
assert not lauf.uebrig, lauf.diagnose()
assert m["h_inhalt"] and m["haupt_inhalt"], "Inhalte müssen gesperrt sein, solange die Rückfrage offen ist"
assert not m["h_titel"] and not m["haupt_titel"], "Titelleisten (verschieben) dürfen nicht gesperrt sein"
assert not m["h_inhalt_danach"] and m["haupt_inhalt_danach"]
assert not gesperrt(lauf.app.ptree), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
if sys.platform.startswith("win"):
assert m["r_besitzer"] == m["h"], "Rückfrage gehört nicht dem Händler-Dialog"
lauf.pruefen()
@@ -612,8 +638,240 @@ def test_windows_rundgang_sperrt_und_gibt_frei(lauf_mit_daten):
messen_und_weiter, f"Schritt {i + 1}"))
schritte.append((IMMER, lambda: rg().beenden(), "Rundgang beenden"))
lauf.ablauf(schritte, sekunden=12)
assert not lauf.uebrig, lauf.uebrig
assert not lauf.uebrig, lauf.diagnose()
assert all(m["gesperrt"]), "Hauptfenster während des Rundgangs nicht gesperrt"
assert all(m["besitzer"]), "Sprechblase gehört nicht dem Hauptfenster"
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Rundgang nicht freigegeben"
lauf.pruefen()
# =========================================================================== Rückmeldungen vom 2.11. (Teil 2)
def test_gedrueckte_knoepfe_behalten_ihre_schrift(lauf_mit_daten):
"""Gemeldet: Im gedrückten Zustand war die Beschriftung weg (activeforeground = Grau)."""
from provtool.gui import win95 as w
lauf = lauf_mit_daten
for b in (w.knopf(lauf.root, "Test"), tk.Button(lauf.root, text="roh")):
assert b.cget("activeforeground") != b.cget("activebackground"), "Schrift gedrückter Knöpfe unsichtbar"
b.destroy()
lauf.pruefen()
def test_tipp_text_bleibt_im_sichtbaren_bereich(lauf_mit_daten):
from provtool.gui.assistent import TIPPS, TippDesTages
lauf = lauf_mit_daten
m = {}
def messen():
d = lauf.dialog(TippDesTages)
rahmen = d.text.master
for t in TIPPS: # jeden Tipp einmal anzeigen
d.text.configure(text=t)
d.update_idletasks()
m.setdefault("breiten", []).append((d.text.winfo_reqwidth(), rahmen.winfo_width()))
m.setdefault("hoehen", []).append((d.text.winfo_reqheight(), rahmen.winfo_height()))
d.abbrechen()
lauf.ablauf([
(IMMER, lauf.app.tipp_des_tages, "Tipp öffnen"),
(lambda: lauf.dialog(TippDesTages) is not None, lambda: lauf.root.after(100, messen), "messen"),
(lambda: "breiten" in m and lauf.dialog(TippDesTages) is None, lambda: None, "zu"),
])
assert not lauf.uebrig, lauf.diagnose()
for text_b, rahmen_b in m["breiten"]:
assert text_b <= rahmen_b, f"Tipp-Text ({text_b}px) breiter als sein Bereich ({rahmen_b}px)"
for text_h, rahmen_h in m["hoehen"]:
assert text_h <= rahmen_h, f"Tipp-Text ({text_h}px) höher als sein Bereich ({rahmen_h}px)"
lauf.pruefen()
def test_strg_ruecktaste_loescht_ganzes_wort(lauf_mit_daten):
from provtool.gui import win95 as w
lauf = lauf_mit_daten
e = tk.Entry(lauf.root)
e.insert(0, "Mobilfunk Schmid GmbH")
ev = type("E", (), {"widget": e})()
e.icursor("end")
w._wort_weg(ev, -1)
assert e.get() == "Mobilfunk Schmid "
w._wort_weg(ev, -1)
assert e.get() == "Mobilfunk "
e.icursor(0)
w._wort_weg(ev, 1)
assert e.get() == ""
# Bindung ist für alle Eingabefelder aktiv
for klasse in ("Entry", "TEntry", "TCombobox"):
assert lauf.root.bind_class(klasse, "<Control-BackSpace>"), klasse
assert lauf.root.bind_class(klasse, "<Control-Delete>"), klasse
e.destroy()
lauf.pruefen()
def test_menue_wechselt_beim_ueberfahren(lauf_mit_daten):
"""Gemeldet: Bei offenem Menü wechselte es nicht mit, wenn man zum nächsten Eintrag fuhr."""
lauf = lauf_mit_daten
leiste = lauf.app.menueleiste
datei, haendler = leiste.knoepfe["Datei"], leiste.knoepfe["Händler"]
m = {}
def ueberfahren():
popup = leiste.menues[datei]
ev = type("E", (), {"x_root": haendler.winfo_rootx() + 3, "y_root": haendler.winfo_rooty() + 3})()
popup._bewegung(ev)
def pruefen_und_esc():
m["offen"] = leiste.offener_knopf
m["popup_offen"] = leiste.menues[haendler].offen
leiste.menues[haendler].fenster.event_generate("<Escape>")
leiste.menues[haendler].schliessen()
lauf.ablauf([
(IMMER, lambda: leiste._klick(datei), "Datei öffnen"),
(lambda: leiste.menues[datei].offen, ueberfahren, "zu „Händler“ fahren"),
(lambda: leiste.menues[haendler].offen, pruefen_und_esc, "prüfen + schließen"),
(lambda: leiste.offener_knopf is None, lambda: None, "alles zu"),
])
assert not lauf.uebrig, lauf.diagnose()
assert m["offen"] is haendler and m["popup_offen"]
assert not leiste.menues[datei].offen
lauf.pruefen()
def test_menue_geht_unter_seinem_menuepunkt_auf(lauf_mit_daten):
"""Unter Windows landete das Menü zunächst oben links in der Bildschirmecke."""
lauf = lauf_mit_daten
leiste = lauf.app.menueleiste
m = {}
for name in ("Datei", "Händler"):
knopf = leiste.knoepfe[name]
def messen(knopf=knopf, name=name):
f = leiste.menues[knopf].fenster
m[name] = (f.winfo_rootx() - knopf.winfo_rootx(),
f.winfo_rooty() - (knopf.winfo_rooty() + knopf.winfo_height()))
leiste.schliessen()
lauf.ablauf([(IMMER, lambda knopf=knopf: leiste._klick(knopf), f"{name} öffnen"),
(lambda knopf=knopf: leiste.menues[knopf].offen, lambda: lauf.root.after(100, messen), "messen"),
(lambda name=name: name in m, lambda: None, "fertig")])
assert not lauf.uebrig, lauf.diagnose()
for name, (dx, dy) in m.items():
assert abs(dx) <= 3 and abs(dy) <= 3, f"Menü „{name}“ nicht unter seinem Menüpunkt (Abstand {dx}, {dy})"
lauf.pruefen()
def test_menue_befehl_wird_ausgefuehrt(lauf_mit_daten):
from provtool.gui.assistent import TippDesTages
lauf = lauf_mit_daten
leiste = lauf.app.menueleiste
hilfe = leiste.knoepfe["?"]
def tipp_waehlen():
popup = leiste.menues[hilfe]
i = next(i for i, (e, _, _) in enumerate(popup.zeilen) if e["label"].startswith("Tipp des Tages"))
popup._ausfuehren(i)
lauf.ablauf([
(IMMER, lambda: leiste._klick(hilfe), "? öffnen"),
(lambda: leiste.menues[hilfe].offen, tipp_waehlen, "Tipp des Tages wählen"),
(lambda: lauf.dialog(TippDesTages) is not None, lambda: lauf.dialog(TippDesTages).abbrechen(), "zu"),
])
assert not lauf.uebrig, lauf.diagnose()
assert not leiste.menues[hilfe].offen
lauf.pruefen()
def test_hauptfenster_bei_offenem_dialog_verschiebbar_inhalt_gesperrt(lauf_mit_daten, monkeypatch):
"""Gemeldet: Bei offenem Dialog ließ sich das Hauptfenster nicht verschieben."""
from provtool.gui import modal, rahmen
from provtool.gui.dialogs import InfoDialog
monkeypatch.setattr(rahmen, "NATIV_VERSCHIEBEN", False) # Tk-Weg prüfen (nativ macht Windows selbst)
lauf = lauf_mit_daten
app = lauf.app
if not app.rahmen.klassisch:
pytest.skip("nur im Win95-Rahmen")
m = {"neu_aufgerufen": False}
monkeypatch.setattr(type(app), "neu", lambda self: m.__setitem__("neu_aufgerufen", True))
titel = app.rahmen.titelleiste.titel_lbl
def pruefen():
x0 = lauf.root.winfo_x()
rx, ry = titel.winfo_rootx() + 30, titel.winfo_rooty() + 5
titel.event_generate("<ButtonPress-1>", x=30, y=5, rootx=rx, rooty=ry)
titel.event_generate("<B1-Motion>", x=70, y=5, rootx=rx + 40, rooty=ry)
titel.event_generate("<ButtonRelease-1>", x=70, y=5, rootx=rx + 40, rooty=ry)
lauf.root.update()
m["verschoben"] = lauf.root.winfo_x() - x0
knopf = app.tb["Neu"] # Klick auf den gesperrten Inhalt
knopf.event_generate("<ButtonPress-1>", x=5, y=5)
knopf.event_generate("<ButtonRelease-1>", x=5, y=5)
m["gesperrt"] = modal.TAG in app.ptree.bindtags()
lauf.dialog(InfoDialog).abbrechen()
lauf.ablauf([
(IMMER, lambda: InfoDialog(lauf.root).zeigen(), "Dialog öffnen"),
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(100, pruefen), "prüfen"),
(lambda: "verschoben" in m and lauf.dialog(InfoDialog) is None, lambda: None, "zu"),
])
assert not lauf.uebrig, f"{lauf.uebrig} {m}\n" + "\n".join(lauf.fehler)
assert m["verschoben"] == 40, f"Hauptfenster ließ sich nicht verschieben ({m['verschoben']} px)"
assert m["gesperrt"] and not m["neu_aufgerufen"], "Inhalt des Hauptfensters war nicht gesperrt"
assert modal.TAG not in app.ptree.bindtags(), "Sperre nach dem Schließen nicht aufgehoben"
lauf.pruefen()
def test_rand_zeiger_wird_zurueckgesetzt(lauf_mit_daten):
"""Gemeldet: Der Größenänderungs-Zeiger blieb manchmal stehen."""
lauf = lauf_mit_daten
r = lauf.app.rahmen
if not r.klassisch:
pytest.skip("nur im Win95-Rahmen")
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2})()
r._rand_cursor(ev)
assert r.aussen.cget("cursor"), "am Rand muss der Größenänderungs-Zeiger erscheinen"
r.inhalt.event_generate("<Enter>")
lauf.root.update()
assert r.aussen.cget("cursor") == "", "Zeiger wurde beim Betreten des Inhalts nicht zurückgesetzt"
# Ecken: in der Nähe des Ecks diagonal
assert r._welcher_rand(2, 10) == "nw" and r._welcher_rand(r.aussen.winfo_width() - 3, 10) == "ne"
lauf.pruefen()
def test_eigene_kennungen_ausblenden_in_der_oberflaeche(lauf_mit_daten):
from provtool.db import EIGEN
lauf = lauf_mit_daten
app = lauf.app
alle_vorher = int(app.htree.item("-1", "values")[0])
app.db.eigene_setzen("freenet", "999")
app._aktualisieren()
assert app.htree.exists(str(EIGEN)), "Eintrag „(Eigene – ausgeblendet)“ fehlt"
assert int(app.htree.item("-1", "values")[0]) == alle_vorher - 1, "eigene Positionen zählen noch mit"
assert all(p["haendler_id"] != EIGEN for p in app._gefiltert())
lauf.pruefen()
@nur_windows
def test_windows_minimieren_mit_offenem_dialog(lauf_mit_daten):
"""Minimieren bei offenem Dialog darf den Dialog nicht zerstören; nach dem Wiederherstellen ist er da."""
from provtool.gui import win32
from provtool.gui.dialogs import InfoDialog
lauf = lauf_mit_daten
m = {}
def minimieren():
lauf.app.rahmen.minimieren()
lauf.root.update()
m["minimiert"] = win32.ist_minimiert(lauf.root)
win32.user32().ShowWindow(win32.hwnd(lauf.root), 9) # SW_RESTORE (wie Klick in der Taskleiste)
lauf.root.update()
m["dialog_da"] = lauf.dialog(InfoDialog) is not None
lauf.dialog(InfoDialog).abbrechen()
lauf.ablauf([
(IMMER, lambda: InfoDialog(lauf.root).zeigen(), "Dialog öffnen"),
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, minimieren), "minimieren"),
(lambda: "dialog_da" in m, lambda: None, "fertig"),
], sekunden=12)
assert not lauf.uebrig, lauf.diagnose()
assert m["minimiert"], "Hauptfenster wurde nicht minimiert"
assert m["dialog_da"], "Dialog nach dem Minimieren verschwunden"
lauf.pruefen()
+16
View File
@@ -265,3 +265,19 @@ def test_migration_alter_datenbank(tmp_path):
spalten = {r[1] for r in d.con.execute("PRAGMA table_info(position)")}
assert "haendler_fest" in spalten
d.close()
def test_eigene_kennungen_ausblenden(db, freenet_csv):
from provtool.db import EIGEN
hid = db.haendler_speichern(Haendler(0, "Händler A"), [("freenet", "111")])
aid = db.abrechnung_neu("X")
db.importieren(aid, lies_datei(freenet_csv))
assert {p["haendler_id"] for p in db.positionen(aid)} == {hid, UNZUGEORDNET}
db.eigene_setzen("freenet", "999")
ids = {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}
assert ids["999"] == EIGEN and ids["111"] == hid
# Händler-Kennung hat Vorrang vor „eigen“
db.eigene_setzen("freenet", "111")
assert {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}["111"] == hid
db.eigene_setzen("freenet", "999", False)
assert {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}["999"] == UNZUGEORDNET