# -*- coding: utf-8 -*- """Pruefstand fuer die Montage-Rechnung - der ganze Weg an echten Dateien. Bartl vermietet auch monatsweise an Montagearbeiter. Dieses Blatt schrieb er bisher von Hand in Word: Miete fuer Montagearbeiter inkl. 7 % MwSt. fuer den Monat August 2026 Netto 467,29 USt 32,71 Brutto 500,00 Im Rechnungstool ist das ein eigener Reiter. Geprueft wird deshalb nicht nur gerechnet, sondern der Weg, den er geht: Reiter waehlen, Kunde und Zeitraum eintragen, PDF erstellen. Danach liest das STEUERJOURNAL die Datei ein - die Miete ist eine Beherbergung, die Steuer kommt obendrauf (500 + 25 = 525) und muss genau so beim Amt ankommen. Die echte config.json wird nicht angefasst - es wird mit einer Kopie in einem Wegwerf-Ordner gearbeitet. """ import json import os import shutil import sys import tempfile import threading QUELLE = r"C:\claude\rechnungstool" JOURNAL_PROG = r"C:\claude\beherbergungssteuer" sys.path.insert(0, QUELLE) tmp = tempfile.mkdtemp(prefix="montage_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.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, indent=2) # Kein Erststart-Dialog im Pruefstand - er wuerde den Lauf haengen lassen. os.environ["CAMPINGHOF_TEST"] = "1" import app as A # noqa: E402 import bestand as BE # noqa: E402 from tkinter import messagebox # noqa: E402 from pruefhelfer import fehler, pruefe def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog, " "der nicht beantwortet wird") os._exit(2) _wecker = threading.Timer(150, _reissleine) _wecker.daemon = True _wecker.start() # Dialoge automatisch beantworten; jede Frage wird protokolliert. antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True} gefragt = [] def askyesno(titel, text, **kw): gefragt.append(titel) for schlue, wert in antworten.items(): if schlue in titel: return wert raise AssertionError(f"unangemeldeter Dialog: {titel!r}") warnungen = [] # Die Freigabe vor dem Beleg (§ 14 UStG) wird bewusst bejaht - geprueft wird hier # der Ablauf DANACH. Dass ein "Nein" keinen Beleg erzeugt, prueft pruef_freigabe.py. antworten["jetzt erstellen?"] = True # Der Erprobungshinweis wird hier bejaht - geprueft wird er in pruef_hinweis.py. antworten["Erprobungsfassung"] = True messagebox.askyesno = askyesno messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: warnungen.append(a[0] if a else "") messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "", a[1] if len(a) > 1 else "") f = A.RechnungsApp() f.update() f.cfg["output_dir"] = ordner # ---- 0. Der Reiter --------------------------------------------------------- reiter = [f.blaetter.tab(i, "text").strip() for i in range(f.blaetter.index("end"))] pruefe(reiter == ["Campingrechnung", "Montage"], f"zwei Reiter da ({reiter})") pruefe(not f.ist_montage(), "beim Start liegt die Campingrechnung oben") pruefe(f.datum_labels["von"].cget("text").startswith("Anreise"), "dort heisst das Feld Anreise") f.blaetter.select(1) f.update() pruefe(f.ist_montage(), "nach dem Wechsel liegt Montage oben") pruefe(f.datum_labels["von"].cget("text").startswith("Zeitraum von"), f"dort heisst dasselbe Feld Zeitraum von ({f.datum_labels['von'].cget('text')!r})") # ---- 1. Die Rechnung, wie er sie tippt ------------------------------------ f.k_anrede.set("Herr") f.k_vorname.set("Dominik") f.k_name.set("Schmidt") f.k_strasse.set("Musterweg 1") f.k_plzort.set("12345 Musterstadt") f.r_datum.set("12.05.2026") f.r_anreise.set("01.08.2026") f.r_abreise.set("31.08.2026") f.m_monate.set("1") f.update() pruefe(f.r_naechte.get() == "30", f"30 Naechte im Zeitraum ({f.r_naechte.get()})") text = f.m_text.cget("text") pruefe("Miete für Montagearbeiter" in text and "7 % MwSt." in text and "01.08.2026" in text and "31.08.2026" in text, f"die Zeile steht so da, wie sie aufs Blatt gehoert ({text!r})") f.pdf_erstellen() pdf = os.path.join(ordner, "Rechnung_2026-001_Schmidt_Montage.pdf") pruefe(os.path.exists(pdf), f"Montage-Rechnung geschrieben ({os.path.basename(pdf)})") # ---- 2. Was in den Kenndaten steht ---------------------------------------- meta = BE.kenndaten_lesen(pdf) or {} pruefe(meta.get("vorlage") == "montage", f"vorlage = montage ({meta.get('vorlage')!r})") pruefe(meta.get("art") == "rechnung", f"art bleibt rechnung - fuer das Journal eine normale Buchung ({meta.get('art')!r})") pruefe(float(meta.get("zwischensumme") or 0) == 500.0, f"Zwischensumme 500,00 ({meta.get('zwischensumme')})") pruefe(float(meta.get("steuer_satz") or 0) == 5.0, f"Beherbergungssteuer 5 % ({meta.get('steuer_satz')})") pruefe(float(meta.get("steuer_betrag") or 0) == 25.0, f"Steuer 25,00 - obendrauf, nicht drin ({meta.get('steuer_betrag')})") pruefe(float(meta.get("gesamt") or 0) == 525.0, f"Gesamt 525,00 ({meta.get('gesamt')})") pruefe(str(meta.get("naechte")) == "30", f"30 Naechte in den Kenndaten - die Meldung ans Amt ({meta.get('naechte')!r})") pruefe(meta.get("nachname") == "Schmidt", f"Nachname ({meta.get('nachname')!r})") bloecke = meta.get("ust_bloecke") or [] pruefe(len(bloecke) == 1 and int(bloecke[0]["satz"]) == 7, f"eine USt-Zeile mit 7 % ({bloecke})") pruefe(bloecke and abs(bloecke[0]["netto"] - 467.29) < 0.005 and abs(bloecke[0]["ust"] - 32.71) < 0.005, f"Netto 467,29 und USt 32,71 wie auf seinem Blatt ({bloecke})") pos = meta.get("positionen") or [] pruefe(len(pos) == 1 and float(pos[0]["anzahl"]) == 1.0, f"eine Position, ein Monat ({pos})") # ---- 3. Was auf dem Blatt steht ------------------------------------------- from pypdf import PdfReader # noqa: E402 seiten = [s.extract_text() or "" for s in PdfReader(pdf).pages] # Seit 05.09.2026 gibt es nur noch EIN Blatt. Ein zweites mit derselben Nummer, # aber anderem Gesamtbetrag ("fuer das Amt") sieht bei einer Pruefung aus, als # werde ueber zwei verschiedene Umsaetze abgerechnet. pruefe(len(seiten) == 1, f"genau EIN Blatt - kein zweites fuers Amt ({len(seiten)})") kunde_seite = seiten[0].replace("\n", " ") pruefe("Miete für Montagearbeiter" in kunde_seite, "die Miet-Zeile steht auf dem Blatt") pruefe("Zeitraum 01.08.2026" in kunde_seite, "mit dem Zeitraum") pruefe("Rechnungsbetrag Insgesamt" in kunde_seite, "und die Schlusszeile heisst wie bei ihm: Rechnungsbetrag Insgesamt") pruefe("Tage/Menge" not in kunde_seite and "Pos." not in kunde_seite, "keine Positionstabelle auf dem Montage-Blatt") pruefe("525,00" in kunde_seite and "500,00" in kunde_seite, "500,00 und 525,00 stehen drauf") pruefe("Beherbergungssteuer" in kunde_seite, "die Steuer ist auf der Kundenseite ausgewiesen") pruefe(kunde_seite.count("Rechnungsbetrag Insgesamt") == 1, "der Gesamtbetrag steht genau einmal in der Datei") # ---- 4. Das Steuerjournal liest die Datei --------------------------------- sys.path.append(JOURNAL_PROG) import db as BST # noqa: E402 journal = BST.Journal(os.path.join(tmp, "journal.sqlite3")) neu, akt, uebersprungen, kaputt = journal.scanne(ordner, 5.0, voll=True) pruefe(neu == 1 and not kaputt, f"das Journal liest sie ein ({neu} neu, Fehler: {kaputt})") buchungen = journal.buchungen(jahr=2026) pruefe(len(buchungen) == 1, f"eine Buchung im Journal ({len(buchungen)})") if buchungen: b = buchungen[0] pruefe(b.nachname == "Schmidt", f"auf Schmidt gebucht ({b.nachname!r})") pruefe(abs(b.gezahlt - 500.0) < 0.005, f"Gezahlt 500,00 ({b.gezahlt})") pruefe(abs(b.steuer - 25.0) < 0.005, f"Steuer 25,00 ({b.steuer})") pruefe(b.naechte == 30, f"30 Uebernachtungen ({b.naechte})") pruefe(b.datum.strftime("%d.%m.%Y") == "12.05.2026", f"Datum ({b.datum})") journal.schliessen() # ---- 5. Storno einer Montage-Rechnung ------------------------------------- offen = [d for d in BE.rechnungen_im_ordner(ordner) if d.get("art") != "storno"] pruefe(len(offen) == 1, "eine stornierbare Rechnung gefunden") f._storno_anlegen(offen[0], ordner) storno_pdf = f.storno_stand["pfad"] if f.storno_stand else "" pruefe(os.path.exists(storno_pdf), f"Storno geschrieben ({os.path.basename(storno_pdf)})") m2 = BE.kenndaten_lesen(storno_pdf) or {} pruefe(float(m2.get("zwischensumme") or 0) == -500.0, f"Storno-Zwischensumme -500,00 ({m2.get('zwischensumme')})") pruefe(float(m2.get("steuer_betrag") or 0) == -25.0, f"Beherbergungssteuer wird mitstorniert ({m2.get('steuer_betrag')})") pruefe(f.ist_montage(), "das Formular steht danach wieder im Montage-Reiter") pruefe(f.m_monate.get() in ("1", "1.0"), f"mit einem Monat ({f.m_monate.get()!r})") f._storno_verwerfen("Pruefstand") # ---- 6. Die Campingrechnung darf sich nicht veraendert haben -------------- f.blaetter.select(0) f.update() pruefe(not f.ist_montage(), "zurueck im Campingreiter") pruefe(f.datum_labels["von"].cget("text").startswith("Anreise"), "Feld heisst wieder Anreise") f.r_anreise.set("01.09.2026") f.r_abreise.set("05.09.2026") erste_pos = sorted(f.anzahl_vars)[0] f.anzahl_vars[erste_pos].set("4") f.update() f.pdf_erstellen() camping = [d for d in os.listdir(ordner) if d.endswith(".pdf") and "Montage" not in d and not d.startswith("Storno")] pruefe(len(camping) == 1, f"die Campingrechnung entsteht wie immer ({camping})") if camping: t = (PdfReader(os.path.join(ordner, camping[0])).pages[0].extract_text() or "").replace("\n", " ") pruefe("Tage/Menge" in t, "und hat weiter ihre Positionstabelle") # ---- 7. Datum: Tag bis 31, Monat bis 12 ----------------------------------- p = A.RechnungsApp._datum_ziffern_pruefen faelle = [("5", "05"), ("9", "09"), ("35", "3"), ("00", "0"), ("31", "31"), ("013", "0103"), ("1213", "121"), ("1200", "120"), ("3112", "3112")] for ein, soll in faelle: pruefe(p(ein) == soll, f"Tippen {ein!r} ergibt {p(ein)!r} (soll {soll!r})") f.r_datum.set("31.02.2026") # Tag und Monat je fuer sich moeglich - den Tag gibt es nicht kaputt = f._datumsfelder_pruefen() pruefe(len(kaputt) == 1 and "31.02.2026" in kaputt[0], f"31.02.2026 wird als Datum beanstandet ({kaputt})") vorher = len([d for d in os.listdir(ordner) if d.endswith(".pdf")]) warnungen.clear() f.pdf_erstellen() nachher = len([d for d in os.listdir(ordner) if d.endswith(".pdf")]) pruefe(nachher == vorher, "und es wird keine Rechnung mit diesem Datum geschrieben") pruefe(any("Datum" in w for w in warnungen), f"mit Meldung an den Benutzer ({warnungen})") f.destroy() if not fehler: shutil.rmtree(tmp, ignore_errors=True) else: print(f" (Wegwerf-Ordner bleibt: {tmp})") print("---") print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}") sys.exit(1 if fehler else 0)