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_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * 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. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die 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
131 lines
5.4 KiB
Python
131 lines
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: was macht das Steuerjournal mit Storno und Berichtigung?
|
|
|
|
Seit dem 02.09.2026 kann das Rechnungstool drei Sorten Blatt schreiben:
|
|
|
|
Rechnung - die Uebernachtung, mit Beherbergungssteuer
|
|
Storno - hebt sie auf, alle Betraege NEGATIV
|
|
Berichtigung - reicht nur die Kundenangaben nach; traegt die Nummer DER
|
|
RECHNUNG und lauter Nullen
|
|
|
|
Alle drei liegen im selben Ordner, den dieses Programm einliest. Der
|
|
gefaehrliche Fall ist das Berichtigungsblatt: unter dem alten Schluessel
|
|
(jahr, rechnungsnummer) hat es die echte Buchung ueberschrieben - im
|
|
Amtsbericht fehlte die Uebernachtung dann, ohne ein Wort.
|
|
|
|
Geprueft wird an ECHTEN PDFs, die der Renderer des Rechnungstools schreibt.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
HIER = r"C:\claude\beherbergungssteuer"
|
|
TOOL = r"C:\claude\rechnungstool"
|
|
sys.path.insert(0, HIER)
|
|
|
|
|
|
# --- Reissleine: ein Pruefstand darf nie haengen ---
|
|
import threading as _th
|
|
|
|
|
|
def _reissleine():
|
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = _th.Timer(120, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
tmp = tempfile.mkdtemp(prefix="bst_pruef_")
|
|
ordner = os.path.join(tmp, "rechnungen")
|
|
os.makedirs(ordner)
|
|
|
|
# ---- PDFs mit dem echten Renderer des Rechnungstools schreiben --------------
|
|
sys.path.insert(0, TOOL)
|
|
import config as tool_config # noqa: E402
|
|
shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json"))
|
|
tool_config.CONFIG_PATH = os.path.join(tmp, "config.json")
|
|
import berechnung as tb # noqa: E402
|
|
import pdf_renderer # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
cfg_tool = tool_config.load_config()
|
|
|
|
|
|
def schreibe(datei, nummer, name, datum, tage=3, **extra):
|
|
r = tb.berechne(cfg_tool["katalog"], {1: {"anzahl": tage, "personen": 0}}, cfg_tool)
|
|
kopf = {"rechnungsnummer": nummer, "datum": datum, "anreise": "", "abreise": "",
|
|
"naechte": str(tage), "nachname": name, "vorname": "Anna",
|
|
"kunde_zeilen": ["", f"Anna {name}", "", "Musterweg 1", "12345 Musterstadt"]}
|
|
kopf.update(extra)
|
|
pdf_renderer.render_rechnung(os.path.join(ordner, datei), cfg_tool, r, kopf)
|
|
return r
|
|
|
|
|
|
rechnung = schreibe("Rechnung_2026-014_Meier.pdf", "2026-014", "Meier", "12.08.2026", 3)
|
|
steuer_soll = round(sum(a.betrag for a in rechnung.aufschlaege), 2)
|
|
# Berichtigungsblatt zu genau dieser Rechnung - traegt DEREN Nummer
|
|
schreibe("Berichtigte Rechnung_2026-08-20_2026-014.pdf", "2026-014", "Meier", "12.08.2026", 3,
|
|
art="berichtigung", berichtigt_zu="2026-014", berichtigt_am="20.08.2026",
|
|
vorgang="2026-014")
|
|
# und der Altbestand: dieselbe Nummer zweimal, verschiedene Gaeste
|
|
schreibe("Rechnung_2026-020_Erst.pdf", "2026-020", "Erst", "01.08.2026", 2)
|
|
schreibe("Rechnung_2026-020_Zweit.pdf", "2026-020", "Zweit", "05.08.2026", 4)
|
|
|
|
# ---- jetzt das Steuerjournal -----------------------------------------------
|
|
sys.path.remove(TOOL)
|
|
for name in ("config", "berechnung"):
|
|
sys.modules.pop(name, None)
|
|
import db as db_mod # noqa: E402
|
|
|
|
journal = db_mod.Journal(os.path.join(tmp, "journal.sqlite3"))
|
|
neu, akt, uebersprungen, kaputt = journal.scanne(ordner, 5.0, voll=True)
|
|
# Direkt aus der Tabelle lesen - so sieht man auch, was WIRKLICH gespeichert
|
|
# wurde, nicht nur, was das Modell daraus macht.
|
|
buchungen = [dict(r) for r in journal.con.execute(
|
|
"SELECT * FROM buchungen WHERE jahr=2026 ORDER BY rechnungsnummer, id")]
|
|
|
|
print("--- was im Journal steht ---")
|
|
for b in buchungen:
|
|
print(f" {b['rechnungsnummer']:>6} {b['datum']} {b['nachname']:10} "
|
|
f"{b['naechte']} Nächte {b['gezahlt']:8.2f} {os.path.basename(b['pdf_pfad'])}")
|
|
|
|
nummern = [b["rechnungsnummer"] for b in buchungen]
|
|
|
|
# 1. Das Berichtigungsblatt darf die Rechnung NICHT ueberschrieben haben
|
|
zu_014 = [b for b in buchungen if b["rechnungsnummer"] in ("014", "2026-014")]
|
|
pruefe(len(zu_014) == 1, f"zu 2026-014 steht genau EINE Buchung im Journal ({len(zu_014)})")
|
|
pruefe(zu_014 and zu_014[0]["naechte"] == 3,
|
|
f"und sie hat die 3 Nächte der Rechnung, nicht 0 ({zu_014[0]['naechte'] if zu_014 else '-'})")
|
|
pruefe(zu_014 and zu_014[0]["gezahlt"] > 0,
|
|
f"und einen Betrag > 0 ({zu_014[0]['gezahlt'] if zu_014 else '-'})")
|
|
pruefe(zu_014 and "Berichtigte" not in os.path.basename(zu_014[0]["pdf_pfad"]),
|
|
"die Buchung stammt aus der RECHNUNG, nicht aus dem Berichtigungsblatt")
|
|
|
|
# 2. Zwei Rechnungen mit derselben Nummer muessen BEIDE dastehen
|
|
zu_020 = [b for b in buchungen if b["rechnungsnummer"] in ("020", "2026-020")]
|
|
pruefe(len(zu_020) == 2,
|
|
f"beide Rechnungen mit 2026-020 stehen im Journal ({len(zu_020)})")
|
|
pruefe({b["nachname"] for b in zu_020} == {"Erst", "Zweit"},
|
|
f"mit beiden Gästen ({[b['nachname'] for b in zu_020]})")
|
|
|
|
# 3. Die Beherbergungssteuer der Rechnung ist vollstaendig da
|
|
summe = round(sum(b["gezahlt"] * b["satz"] / 100 for b in buchungen), 2)
|
|
pruefe(summe > 0, f"Steuer über alle Buchungen: {summe:.2f} € (Rechnung allein {steuer_soll:.2f} €)")
|
|
|
|
journal.schliessen()
|
|
if not fehler:
|
|
import gc as _gc
|
|
import sqlite3 as _sq
|
|
for _obj in list(_gc.get_objects()):
|
|
if isinstance(_obj, _sq.Connection):
|
|
try:
|
|
_obj.close()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
print("---")
|
|
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
|
|
sys.exit(1 if fehler else 0)
|