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
137 lines
5.5 KiB
Python
137 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: Kopf- und Fussbild auf der Rechnung.
|
|
|
|
Drei Dinge muessen stimmen, sonst faellt es erst beim Drucken auf:
|
|
|
|
1. Ein gewaehltes Bild wird KOPIERT (Ordner "bilder"), nicht nur verlinkt -
|
|
sonst ist das Logo weg, sobald der Nutzer seinen Desktop aufraeumt.
|
|
2. Das Fussbild steht auf der Kundenrechnung, aber NICHT auf dem Amt-Blatt:
|
|
ein Briefpapier-Band traegt Bank und Steuernummer, und die haben auf dem
|
|
Blatt fuers Amt nichts zu suchen.
|
|
3. Der Haken "Angaben stehen im Bild" laesst die gezeichneten Texte weg -
|
|
und zwar nur dort, wo er gilt.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, QUELLE)
|
|
|
|
import config # noqa: E402
|
|
|
|
tmp = tempfile.mkdtemp(prefix="bilder_pruef_")
|
|
config.BASE_DIR = tmp
|
|
config.CONFIG_PATH = os.path.join(tmp, "config.json")
|
|
shutil.copy(os.path.join(QUELLE, "config.json"), config.CONFIG_PATH)
|
|
|
|
import bilder # noqa: E402
|
|
import pdf_renderer # noqa: E402
|
|
from berechnung import berechne # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
|
|
def testbild(pfad, farbe):
|
|
"""Kleines PNG erzeugen - der Pruefstand braucht keine echten Kundenbilder."""
|
|
from PIL import Image
|
|
Image.new("RGB", (600, 120), farbe).save(pfad)
|
|
return pfad
|
|
|
|
|
|
# ---- 1. Uebernehmen: kopieren, nicht verlinken -----------------------------
|
|
fremd = os.path.join(tmp, "vom_desktop.png")
|
|
testbild(fremd, (10, 90, 60))
|
|
gespeichert = bilder.uebernehmen(fremd)
|
|
pruefe(gespeichert == "bilder/vom_desktop.png", f"Pfad wird kurz gemerkt: {gespeichert}")
|
|
pruefe(os.path.exists(os.path.join(tmp, "bilder", "vom_desktop.png")), "Bild liegt im Ordner bilder/")
|
|
|
|
# Das Original loeschen - die Rechnung muss trotzdem ein Bild haben.
|
|
os.remove(fremd)
|
|
pruefe(bool(bilder.aufloesen(gespeichert)), "Bild bleibt da, wenn das Original geloescht wird")
|
|
|
|
# Dasselbe Bild zweimal waehlen -> kein zweites Exemplar.
|
|
shutil.copy(os.path.join(tmp, "bilder", "vom_desktop.png"), fremd)
|
|
pruefe(bilder.uebernehmen(fremd) == "bilder/vom_desktop.png", "gleiches Bild zweimal -> kein Duplikat")
|
|
# Anderer Inhalt, gleicher Name -> das alte Bild wird NICHT ueberschrieben.
|
|
testbild(fremd, (200, 30, 30))
|
|
zweit = bilder.uebernehmen(fremd)
|
|
pruefe(zweit == "bilder/vom_desktop-2.png", f"gleicher Name, anderes Bild -> {zweit}")
|
|
|
|
# ---- 2. Rendern: Kunde vs. Amt --------------------------------------------
|
|
cfg = config.load_config()
|
|
cfg["firma"]["logo"] = bilder.uebernehmen(testbild(os.path.join(tmp, "kopf.png"), (20, 60, 120)))
|
|
cfg["firma"]["fussbild"] = bilder.uebernehmen(testbild(os.path.join(tmp, "fuss.png"), (120, 60, 20)))
|
|
cfg["firma"]["bank"] = "Bank Testbank"
|
|
|
|
mengen = {1: {"anzahl": 3, "personen": 0}}
|
|
r = berechne(cfg["katalog"], mengen, cfg)
|
|
kopf = {"art": "rechnung", "rechnungsnummer": "2026-999", "datum": "05.09.2026",
|
|
"kunde_zeilen": ["Herr", "Max Muster", "Musterweg 1", "04416 Markkleeberg"],
|
|
"anreise": "01.09.2026", "abreise": "04.09.2026", "naechte": 3}
|
|
pfad = os.path.join(tmp, "probe.pdf")
|
|
pdf_renderer.render_rechnung(pfad, cfg, r, kopf)
|
|
pruefe(os.path.exists(pfad), "PDF mit beiden Bildern entsteht")
|
|
|
|
|
|
def seiten_bilder_und_text(pdf):
|
|
"""Je Seite: (Anzahl eingebetteter Bilder, Text)."""
|
|
from pypdf import PdfReader
|
|
raus = []
|
|
for seite in PdfReader(pdf).pages:
|
|
xo = seite.get("/Resources", {}).get("/XObject", {})
|
|
try:
|
|
xo = xo.get_object()
|
|
except AttributeError:
|
|
pass
|
|
anzahl = sum(1 for k in xo if xo[k].get_object().get("/Subtype") == "/Image")
|
|
raus.append((anzahl, seite.extract_text() or ""))
|
|
return raus
|
|
|
|
|
|
seiten = seiten_bilder_und_text(pfad)
|
|
pruefe(len(seiten) == 1, f"genau EIN Blatt ({len(seiten)} Seiten)")
|
|
kunde_bilder, kunde_text = seiten[0]
|
|
pruefe(kunde_bilder == 2, f"das Blatt traegt Kopf- UND Fussbild ({kunde_bilder})")
|
|
pruefe("Testbank" in kunde_text, "Bankverbindung steht auf der Rechnung")
|
|
pruefe("Steuernummer" in kunde_text,
|
|
"die Steuernummer steht auf der Rechnung (§ 14 Abs. 4 Nr. 2 UStG)")
|
|
|
|
# ---- 3. Die Firmenfarbe faerbt das Blatt ----------------------------------
|
|
# Jede Firma faerbt ihre Rechnung selbst. Geprueft wird, dass die gewaehlte
|
|
# Farbe wirklich im PDF landet - und dass eine kaputte Eingabe das Schreiben
|
|
# NICHT verhindert, sondern still auf die Vorgabe zurueckfaellt.
|
|
cfg["design"] = {"akzent": "#8a1f1f"}
|
|
pfad2 = os.path.join(tmp, "probe_rot.pdf")
|
|
pdf_renderer.render_rechnung(pfad2, cfg, r, kopf)
|
|
pruefe(pdf_renderer.ACCENT.hexval().endswith("8a1f1f"),
|
|
f"gewaehlte Farbe steht im Blatt: {pdf_renderer.ACCENT.hexval()}")
|
|
pruefe(pdf_renderer.GREY_BG.red > 0.9,
|
|
"helle Flaeche wird aus der Farbe gemischt (bleibt hell)")
|
|
|
|
cfg["design"] = {"akzent": "gruen bitte"}
|
|
pfad3 = os.path.join(tmp, "probe_kaputt.pdf")
|
|
pdf_renderer.render_rechnung(pfad3, cfg, r, kopf)
|
|
pruefe(os.path.exists(pfad3), "kaputte Farbe verhindert die Rechnung NICHT")
|
|
pruefe(pdf_renderer.ACCENT.hexval().endswith("1f5c3d"), "kaputte Farbe faellt auf die Vorgabe zurueck")
|
|
|
|
cfg["design"] = {"akzent": pdf_renderer.AKZENT_VORGABE}
|
|
|
|
# Ohne Bilder muss das Blatt aussehen wie immer.
|
|
cfg["firma"]["logo"] = ""
|
|
cfg["firma"]["fussbild"] = ""
|
|
pfad4 = os.path.join(tmp, "probe_ohne_bild.pdf")
|
|
pdf_renderer.render_rechnung(pfad4, cfg, r, kopf)
|
|
seiten3 = seiten_bilder_und_text(pfad4)
|
|
pruefe(seiten3[0][0] == 0, "ohne Bilder: keine Bilder im PDF")
|
|
pruefe(cfg["firma"]["name"] in seiten3[0][1], "ohne Bild steht der Firmenname gezeichnet oben")
|
|
|
|
print()
|
|
print(f"Ordner: {tmp}")
|
|
if fehler:
|
|
print(f"FEHLGESCHLAGEN: {len(fehler)}")
|
|
for f in fehler:
|
|
print(" -", f)
|
|
sys.exit(1)
|
|
print("ALLES GRUEN")
|