Modernes Design (Standard, Win95 als Legacy), Zeitraum abrechnen, Abrechnung erledigt, Linux-Build
- Design: „modern“ (hell, flach, wie Windows 11, normaler Windows-Rahmen) ist Standard; „win95“ bleibt unter Datei > Einstellungen wählbar (Einstellung „design“). - Export: Zeitraum von–bis (nach Aktivierungsdatum oder Abrechnungszeitraum des Anbieters), exportierte Positionen werden als abgerechnet gekennzeichnet (Betrag/Händler eingefroren) und per Fingerabdruck auch in späteren Dateien/Abrechnungen nicht noch einmal abgerechnet. - Datei > Abrechnung erledigt / wieder öffnen: erledigte Abrechnungen sind gegen Änderungen gesperrt. - DB-Schema v4 (Migration inkl. Fingerabdrücke für alte Positionen). - Linux-Build (python:3.12-bookworm) als zweiter Pipeline-Job mit Tests und Selbsttest; Release-Upload für beide Jobs über build/release_hochladen.sh. - Behoben: normaler Windows-Rahmen stürzte beim Start ab (Hauptrahmen.nativ fehlte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
97c73b82c7
commit
851fd6600a
17 files changed
+829
-80
No files matched your search
+69
-5
@@ -203,9 +203,7 @@ def home(tmp_path, monkeypatch):
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lauf_mit_daten(home):
|
||||
"""App mit einem Händler und einer Abrechnung (erfundene Daten)."""
|
||||
def _profil_mit_daten(home):
|
||||
from tests.test_kern import FREENET_KOPF, freenet_zeile
|
||||
csv = home / "freenet.csv"
|
||||
csv.write_bytes(("\r\n".join([
|
||||
@@ -219,6 +217,21 @@ def lauf_mit_daten(home):
|
||||
from provtool.importer import lies_datei
|
||||
db.importieren(db.abrechnung_neu("Oktober 2026"), lies_datei(csv))
|
||||
db.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lauf_mit_daten(home):
|
||||
"""App mit einem Händler und einer Abrechnung (erfundene Daten), klassisches Win95-Design."""
|
||||
_profil_mit_daten(home)
|
||||
lauf = Lauf(home, {"assistent_erledigt": True, "tipps_beim_start": False, "design": "win95"})
|
||||
yield lauf
|
||||
lauf.ende()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lauf_modern(home):
|
||||
"""Wie lauf_mit_daten, aber im modernen Design (Standard)."""
|
||||
_profil_mit_daten(home)
|
||||
lauf = Lauf(home, {"assistent_erledigt": True, "tipps_beim_start": False})
|
||||
yield lauf
|
||||
lauf.ende()
|
||||
@@ -324,8 +337,10 @@ def _dialog_oeffner():
|
||||
@pytest.mark.parametrize("name", ["haendler", "abrechnungen", "export", "einstellungen", "neu", "info", "tipp",
|
||||
"dateien", "hilfe", "assistent", "prozent", "neu_berechnen", "provisionsarten",
|
||||
"staffel"])
|
||||
def test_schliessen_waehrend_dialog_offen(lauf_mit_daten, name):
|
||||
lauf = lauf_mit_daten
|
||||
@pytest.mark.parametrize("design", ["win95", "modern"])
|
||||
def test_schliessen_waehrend_dialog_offen(home, name, design):
|
||||
_profil_mit_daten(home)
|
||||
lauf = Lauf(home, {"assistent_erledigt": True, "tipps_beim_start": False, "design": design})
|
||||
oeffnen = _dialog_oeffner()[name]
|
||||
lauf.ablauf([(IMMER, lambda: oeffnen(lauf.app), f"{name} öffnen"),
|
||||
(lambda: bool(lauf.dialoge()), lauf.schliessen_ueber_fenster, "Programm schließen")])
|
||||
@@ -1052,3 +1067,52 @@ def test_rand_druecken_startet_natives_ziehen(lauf_mit_daten, monkeypatch):
|
||||
r._resize_start(ev)
|
||||
assert gezogen == ["w", "se"]
|
||||
lauf.pruefen()
|
||||
|
||||
|
||||
def test_modern_ist_standard(lauf_modern):
|
||||
from provtool.gui import win95 as w
|
||||
lauf = lauf_modern
|
||||
assert w.DESIGN == "modern" and not lauf.app.rahmen.klassisch and lauf.app.rahmen.titelleiste is None
|
||||
lauf.ablauf([(IMMER, lambda: lauf.app.menueleiste._klick(lauf.app.menueleiste.knoepfe["Datei"]), "Menü"),
|
||||
(lambda: lauf.app.menueleiste.offener_knopf is not None,
|
||||
lambda: lauf.app.menueleiste.schliessen(), "zu")])
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
lauf.pruefen()
|
||||
|
||||
|
||||
def test_export_zeitraum_kennzeichnen_und_erledigt(lauf_modern, tmp_path, monkeypatch):
|
||||
"""Zeitraum abrechnen, exportierte Positionen nicht erneut exportieren, Abrechnung erledigt sperrt."""
|
||||
from provtool.gui import win95 as w
|
||||
from provtool.gui.dialogs import ExportDialog
|
||||
lauf = lauf_modern
|
||||
app = lauf.app
|
||||
app.settings["export_ordner"] = str(tmp_path / "x")
|
||||
meldungen = []
|
||||
monkeypatch.setattr(w, "info", lambda m, t, text: meldungen.append(text))
|
||||
monkeypatch.setattr(w, "frage", lambda *a, **k: True)
|
||||
|
||||
def ok(von="", bis=""):
|
||||
d = lauf.dialog(ExportDialog)
|
||||
d.v_von.set(von)
|
||||
d.v_bis.set(bis)
|
||||
d.v_auf.set(False)
|
||||
d._ok()
|
||||
|
||||
lauf.ablauf([
|
||||
(IMMER, app.exportieren, "Export Oktober 6."),
|
||||
(lambda: lauf.dialog(ExportDialog) is not None, lambda: ok("01.10.2026", "05.10.2026"), "nur bis 5.10."),
|
||||
(lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "Export Rest"),
|
||||
(lambda: lauf.dialog(ExportDialog) is not None, ok, "alles Offene"),
|
||||
(lambda: lauf.dialog(ExportDialog) is None, app.exportieren, "nochmal"),
|
||||
])
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
assert sum(1 for p in app.positionen if p["abgerechnet"]) == 2
|
||||
assert "1 Position(en) als abgerechnet" in meldungen[0] and "1 Position(en) als abgerechnet" in meldungen[1]
|
||||
assert "bereits abgerechnet" in meldungen[2], meldungen
|
||||
# erledigt: Änderungen gesperrt
|
||||
app.erledigt_umschalten()
|
||||
assert app.db.erledigt(app.aid)
|
||||
app.ptree.selection_set(app.ptree.get_children()[0])
|
||||
app.prozent()
|
||||
assert "erledigt" in meldungen[-1]
|
||||
lauf.pruefen()
|
||||
@@ -419,3 +419,99 @@ def test_migration_v2_haendler_ohne_airtime(tmp_path):
|
||||
d.standard_staffel_setzen([(1, 100), (5, None)])
|
||||
assert d.staffel_fuer(h.id) == [(1, 100), (5, None)]
|
||||
d.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Zeitraum / abgerechnet / erledigt
|
||||
|
||||
def test_zeitraum_lesen_und_bezugsdatum():
|
||||
from datetime import date
|
||||
from provtool import zeitraum as zr
|
||||
assert zr.datum_lesen("1.10.2026") == date(2026, 10, 1) and zr.datum_lesen("01.10.26") == date(2026, 10, 1)
|
||||
assert zr.datum_lesen("") is None
|
||||
with pytest.raises(ValueError):
|
||||
zr.datum_lesen("32.13.2026")
|
||||
assert zr.abrechnungsdatum("2026.10.08-2026.10.15") == date(2026, 10, 8)
|
||||
assert zr.abrechnungsdatum("VF2609") == date(2026, 9, 1) == zr.abrechnungsdatum("CY202609UM")
|
||||
assert zr.abrechnungsdatum("VF2609P6 Fil 0") == date(2026, 9, 1)
|
||||
p = {"zeitraum": "VF2609", "datum": "2026-08-30"}
|
||||
assert zr.bezugsdatum(p, zr.AKTIVIERUNG) == date(2026, 8, 30)
|
||||
assert zr.bezugsdatum(p, zr.ABRECHNUNG) == date(2026, 9, 1)
|
||||
okt = zr.Zeitraum(date(2026, 10, 1), date(2026, 10, 31))
|
||||
assert okt.enthaelt({"datum": "2026-10-31"}) and not okt.enthaelt({"datum": "2026-11-01"})
|
||||
assert not okt.enthaelt({"datum": None})
|
||||
assert zr.vormonat(date(2026, 3, 15)) == (date(2026, 2, 1), date(2026, 2, 28))
|
||||
assert zr.dieser_monat(date(2026, 12, 5)) == (date(2026, 12, 1), date(2026, 12, 31))
|
||||
|
||||
|
||||
def test_abgerechnet_einfrieren_und_doppelt_erkennen(db, freenet_csv):
|
||||
from provtool.db import AbrechnungErledigt
|
||||
hid = db.haendler_speichern(Haendler(0, "A"), [("freenet", "111")])
|
||||
a1 = db.abrechnung_neu("Okt")
|
||||
db.importieren(a1, lies_datei(freenet_csv))
|
||||
pos = [p for p in db.positionen(a1) if p["haendler_id"] == hid]
|
||||
assert len(pos) == 4 and all(p["fp"] for p in pos)
|
||||
erste = pos[:2]
|
||||
assert db.abrechnen([p["id"] for p in erste]) == 2
|
||||
# abgerechnet: Betrag und Händler eingefroren
|
||||
h = db.haendler(hid)
|
||||
h.standard_prozent = 50
|
||||
db.haendler_speichern(h, []) # Kennung weg, 50 %
|
||||
db.neu_berechnen(a1)
|
||||
nach = {p["id"]: p for p in db.positionen(a1)}
|
||||
for p in erste:
|
||||
assert nach[p["id"]]["haendler_id"] == hid and nach[p["id"]]["betrag"] == p["betrag"]
|
||||
with pytest.raises(AbrechnungErledigt):
|
||||
db.betrag_setzen(erste[0]["id"], 1)
|
||||
assert db.prozent_anwenden([erste[0]["id"]], 10) == 0
|
||||
|
||||
# dieselbe Datei in einer neuen Abrechnung: die beiden abgerechneten Positionen sind „doppelt“
|
||||
db.haendler_speichern(db.haendler(hid), [("freenet", "111")])
|
||||
a2 = db.abrechnung_neu("Nov")
|
||||
db.importieren(a2, lies_datei(freenet_csv))
|
||||
doppelt = [p for p in db.positionen(a2) if p["doppelt"]]
|
||||
assert len(doppelt) == 2 and all(p["doppelt"] == "Okt" for p in doppelt)
|
||||
assert db.abrechnen([p["id"] for p in doppelt]) == 0, "doppelte Positionen dürfen nicht erneut abgerechnet werden"
|
||||
|
||||
# Kennzeichnung aufheben -> nicht mehr doppelt
|
||||
assert db.abrechnung_aufheben([p["id"] for p in erste]) == 2
|
||||
assert not any(p["doppelt"] for p in db.positionen(a2))
|
||||
|
||||
|
||||
def test_abrechnung_erledigt_sperrt(db, freenet_csv):
|
||||
from provtool.db import AbrechnungErledigt
|
||||
hid = db.haendler_speichern(Haendler(0, "A"), [("freenet", "111")])
|
||||
aid = db.abrechnung_neu("Okt")
|
||||
db.importieren(aid, lies_datei(freenet_csv))
|
||||
assert db.erledigt_setzen(aid, True) == 4 # nur die 4 Händler-Positionen, nicht die unzugeordnete
|
||||
assert db.erledigt(aid)
|
||||
assert all(p["abgerechnet"] for p in db.positionen(aid) if p["haendler_id"] == hid)
|
||||
with pytest.raises(AbrechnungErledigt):
|
||||
db.importieren(aid, lies_datei(freenet_csv))
|
||||
with pytest.raises(AbrechnungErledigt):
|
||||
db.datei_entfernen(db.dateien(aid)[0]["id"])
|
||||
unz = next(p for p in db.positionen(aid) if p["haendler_id"] <= 0)
|
||||
db.haendler_zuweisen([unz["id"]], hid)
|
||||
assert next(p for p in db.positionen(aid) if p["id"] == unz["id"])["haendler_id"] <= 0, "erledigt: keine Änderung"
|
||||
assert db.abrechnungen()[0]["abgerechnet"] == 4
|
||||
# wieder öffnen, Positionen bleiben abgerechnet bzw. werden auf Wunsch freigegeben
|
||||
db.erledigt_setzen(aid, False)
|
||||
assert not db.erledigt(aid) and all(p["abgerechnet"] for p in db.positionen(aid) if p["haendler_id"] == hid)
|
||||
db.erledigt_setzen(aid, True)
|
||||
db.erledigt_setzen(aid, False, positionen_aufheben=True)
|
||||
assert not any(p["abgerechnet"] for p in db.positionen(aid))
|
||||
|
||||
|
||||
def test_migration_v3_fingerabdruck(tmp_path, freenet_csv):
|
||||
import sqlite3
|
||||
d = Datenbank(tmp_path / "x.db")
|
||||
aid = d.abrechnung_neu("Alt")
|
||||
d.importieren(aid, lies_datei(freenet_csv))
|
||||
d.close()
|
||||
con = sqlite3.connect(tmp_path / "x.db") # wie eine v3-Datenbank: ohne Fingerabdrücke
|
||||
con.execute("UPDATE position SET fp=NULL")
|
||||
con.commit()
|
||||
con.close()
|
||||
d = Datenbank(tmp_path / "x.db")
|
||||
assert all(p["fp"] for p in d.positionen(aid))
|
||||
assert len({p["fp"] for p in d.positionen(aid)}) == 5
|
||||
d.close()
|
||||
Reference in new issue
Block a user