rechnungstool/pruef_vorschau.py
TheMockTv 2b09d15cd0 Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle
Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe
Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer
sich pruefbar - nach jeder liefen beide Pruefketten gruen.

* Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks.
  Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung
  des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt
  hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine
  Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird
  ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm()
  begrenzt; am unteren Fensterrand lief sie vorher hinaus.
  Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse.

* %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt
  fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren
  die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn
  eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG).

* pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py.
  Die zwei Ausreisser sind mitgezogen: pruef_bilder.py schrieb ein eigenes
  Format, pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende
  nicht sagen, WAS fehlschlug.

* dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden
  app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen
  denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es
  eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31)
  und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen.

* pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft
  ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt
  == 1, und der Dateiname wird mitgeprueft.
  Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden
  Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die
  Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF.

* .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen.
  Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen
  Unterschied, den es im Repository gar nicht gibt.

* Sechs stumme "except OSError" sagen jetzt, warum sie schweigen duerfen, und
  einer meldet statt zu schweigen: schlaegt die Uebernahme einer alten
  config.json fehl, faengt das Programm ohne Firmendaten, Katalog und Zaehler
  neu an - und der Erfolgsfall schrieb eine Zeile, der Fehlerfall nicht. Die
  24 stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade
  zerstoert wurde, und eine Meldung waere Rauschen.

* Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=),
  kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von
  theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes".

39 Pruefstaende, 0 rot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 18:39:03 +02:00

115 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: der Vorschau-Knopf.
Die Vorschau soll zeigen, wie das Blatt aussehen WIRD - und dabei nichts
anfassen. Drei Zusicherungen, die alle drei still brechen koennten:
1. Sie verbraucht KEINE Rechnungsnummer und traegt nichts ins Nummernbuch.
Sonst fehlt in der Buchhaltung eine Nummer, fuer die es kein Blatt gibt.
2. Sie legt nichts im Rechnungsordner ab - dort liegt die Buchhaltung.
3. Auf jeder Seite steht "VORSCHAU - keine Rechnung". Ein Blatt ohne diesen
Hinweis koennte ausgedruckt beim Gast landen und als Rechnung gelten.
"""
import json
import os
import shutil
import sys
import tempfile
import threading as _th
QUELLE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, QUELLE)
tmp = tempfile.mkdtemp(prefix="vorschau_pruef_")
ordner = os.path.join(tmp, "rechnungen")
os.makedirs(ordner)
import config # noqa: E402
shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"))
config.BASE_DIR = tmp
config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False)
os.environ["CAMPINGHOF_TEST"] = "1"
import app as A # noqa: E402
from tkinter import messagebox # noqa: E402
from pruefhelfer import fehler, pruefe
def _reissleine():
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
os._exit(2)
_wecker = _th.Timer(120, _reissleine)
_wecker.daemon = True
_wecker.start()
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
# Geprueft wird er eigens in pruef_hinweis.py.
messagebox.askyesno = lambda *a, **k: True
# Im Pruflauf oeffnet das Programm nichts mehr (sonst stehen nach einem Lauf
# dreissig PDF-Fenster offen). Geprueft wird der Pfad, den die Vorschau
# zurueckgibt - und dass wirklich NICHTS aufgemacht wurde.
geoeffnet = []
A.os.startfile = lambda p, *a, **k: geoeffnet.append(p)
f = A.RechnungsApp()
f.update()
f.cfg["output_dir"] = ordner
erste = sorted(f.anzahl_vars)[0]
f.k_vorname.set("Anna")
f.k_name.set("Beispiel")
f.k_strasse.set("Musterweg 1")
f.k_plzort.set("12345 Musterstadt")
f.anzahl_vars[erste].set("3")
f.update()
nummer_vorher = f.r_nummer.get()
zaehler_vorher = f.cfg.get("rechnung_zaehler")
pfad = f.vorschau_zeigen()
pruefe(bool(pfad), f"die Vorschau nennt ihr Blatt ({os.path.basename(str(pfad))})")
pruefe(not geoeffnet, f"im Pruflauf wird KEIN Betrachter aufgemacht ({geoeffnet})")
pruefe(os.path.exists(pfad), "Vorschau-PDF wurde geschrieben")
pruefe(os.path.abspath(ordner) not in os.path.abspath(pfad),
"Vorschau liegt NICHT im Rechnungsordner")
pruefe(not os.listdir(ordner), f"Rechnungsordner bleibt leer ({os.listdir(ordner)})")
pruefe(f.r_nummer.get() == nummer_vorher,
f"Rechnungsnummer unveraendert ({nummer_vorher} -> {f.r_nummer.get()})")
pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher, "Zaehler unveraendert")
from pypdf import PdfReader # noqa: E402
seiten = [s.extract_text() or "" for s in PdfReader(pfad).pages]
pruefe(all("VORSCHAU" in t for t in seiten),
f"'VORSCHAU' steht auf jeder Seite ({len(seiten)} Seiten)")
pruefe(all("keine Rechnung" in t for t in seiten), "'keine Rechnung' steht auf jeder Seite")
# Und die echte Rechnung danach traegt die Marke NICHT.
f.pdf_erstellen()
echte = [os.path.join(ordner, n) for n in os.listdir(ordner) if n.lower().endswith(".pdf")]
pruefe(len(echte) == 1, f"danach entsteht genau eine echte Rechnung ({len(echte)})")
if echte:
text = "".join((s.extract_text() or "") for s in PdfReader(echte[0]).pages)
pruefe("VORSCHAU" not in text, "die echte Rechnung traegt KEINE Vorschau-Marke")
f.destroy()
print()
if fehler:
print(f"FEHLGESCHLAGEN: {len(fehler)}")
for x in fehler:
print(" -", x)
sys.exit(1)
print("ALLES GRUEN")