rechnungstool/pruef_bilder.py
TheMockTv ca9ac212cf Amt-Blatt raus, Bedienung ueber die API, Erststart fragt nach zwei Wegen
Das zweite Blatt "fuer das Amt" trug DIESELBE Rechnungsnummer, aber einen
anderen Gesamtbetrag. Mehrere Blaetter zum selben Umsatz mit verschiedenen
Endbetraegen sehen aus, als werde ueber verschiedene Umsaetze abgerechnet -
bei einer Pruefung ein Problem. Ersatzlos entfallen; was die Stadt braucht,
rechnet das Steuerjournal aus den Kenndaten der PDF. Mit dem Blatt fielen
_AmtMarke, gesamtrechnung_betont und der doppelte Renderlauf weg.

Die API kann das Fenster jetzt BEDIENEN - mit Schluessel wie bei einem SSH-Key:
32 Byte Zufall in api_schluessel.txt im gemeinsamen Ordner, den nur das
Steuerjournal und der Betreiber lesen. Ohne gueltigen Schluessel kein Zutritt
(compare_digest). ping und schau_nach bleiben schluessellos, sonst kann ein
aelteres Journal nicht mehr anklopfen. Was einen BELEG schreibt, geht nur im
Testlauf - eine Fernbedienung, die echte Rechnungen ausstellt, hat in einer
Buchhaltung nichts zu suchen. api.py liegt wie immer in beiden Programmen.

Der Erststart fragte nach "der zuletzt vergebenen Nummer" - das holt niemanden
ab, der bei 0 anfaengt. Jetzt zwei Wege: bei 0 anfangen ODER die eigene
Zaehlung weiterfuehren. Unter das schon Vergebene kommt keiner der beiden.

pruef_nummernsperre bediente den Dialog nur halb: nach einer abgelehnten Zahl
bleibt er offen (so soll er), niemand drueckte "Spaeter" - das Programm wartete
in wait_window() ewig. Das war schon vor diesem Umbau so.

Neu: pruef_api.py. Alle 14 Pruefstaende gruen.

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

144 lines
5.7 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
fehler = []
def pruefe(bedingung, text):
print(("OK | " if bedingung else "FEHL | ") + text)
if not bedingung:
fehler.append(text)
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")