rechnungstool/pruef_proforma.py
TheMockTv 94c2c7881a Mini-API zwischen beiden Programmen, Proforma-Rechnung
MINI-API (seine Ansage: "die muessen ja sehen das die beiden auf sind"):
Jedes Programm macht beim Start einen winzigen Server auf 127.0.0.1 mit einem
Port, den das Betriebssystem aussucht, und traegt Port und PID in die
gemeinsame SQLite ein - die ist das Telefonbuch. Zwei Nachrichten reichen:
"ping" wird mit "hallo, bin noch da" beantwortet, "schau_nach" ist der Push.

Der Push traegt KEINE Daten. Er sagt nur "schau in die sqlite" - dort steht die
Meldung ohnehin. Eine Wahrheit, und wenn der Anruf nicht durchgeht, findet das
andere Programm die Meldung beim naechsten Nachsehen.

Gebunden wird nur auf Loopback: von aussen ist nichts erreichbar, Windows fragt
bei 127.0.0.1 nicht nach der Firewall, und die API fuehrt keine Befehle aus.
Kein Netzwerk-Server mit MySQL - dafuer braeuchte er Infrastruktur, die es
nicht gibt; auf verschiedenen Rechnern waere ohnehin ein anderer Bau noetig.

PROFORMA-RECHNUNG: ein Knopf, gewaehlt wird im Popup - die fruehere
Sicherheitsfrage IST diese Auswahl geworden (eine Frage statt zwei).

Seine Bedingung: "musst nur auf passen das der rechner dann die rechnungs
nummer nicht aendert". Die Proforma laeuft in einer EIGENEN Reihe (2026-P001);
Nummernfeld, Rechnungszaehler, Nummernbuch und die Meldung ans Journal bleiben
unberuehrt. Genau das prueft pruef_proforma.py zuerst.

An den Quellen geprueft, nicht an Ratgeberseiten:
* § 14 Abs. 1 S. 1 UStG / UStAE 14.1 - Rechnung ist jedes Dokument, mit dem
  abgerechnet wird, "gleichgueltig, wie dieses Dokument im Geschaeftsverkehr
  bezeichnet wird". Das Wort "Proforma" allein schuetzt also nicht.
* § 14c Abs. 2 UStG - wer Steuer gesondert ausweist, ohne berechtigt zu sein,
  schuldet den Betrag; Korrektur nur mit Antrag und Zustimmung des Finanzamts.
* § 14 Abs. 4 Nr. 4 UStG - die fortlaufende, einmalig vergebene Nummer gilt
  fuer Rechnungen.

Deshalb: Ueberschrift "Proforma-Rechnung", der Satz "keine Rechnung im Sinne
des § 14 UStG ... nicht zu bezahlen", "Proforma an:" statt "Rechnung an:", die
Nummer ausdruecklich als KEINE Rechnungsnummer, KEIN Steuersatz und KEIN
Steuerbetrag (auch die Beherbergungssteuer nicht - sonst waere das Blatt halb
ausgewiesen, halb nicht), dafuer "Voraussichtlicher Gesamtbetrag" mit dem
Hinweis, dass alles enthalten ist. Kein Blatt fuers Amt, kein Zahlungstext.

TESTLAUF: _testwahl sagt, was im Auswahlfenster geklickt worden waere;
pruef_proforma setzt _dialog_zeigen und drueckt die echten Knoepfe.

Pruefstaende: pruef_proforma.py (neu, mit dem echten Popup), pruef_montage,
pruef_storno, pruef_kette, pruef_kernregeln, pruef_nummernbuch, pruef_nummern,
pruef_dialoge - alle gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
2026-09-03 21:22:48 +02:00

227 lines
8.5 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: die Proforma-Rechnung.
Sein Auftrag: ein Knopf, im Popup waehlt man Rechnung oder Proforma - *"musst
nur auf passen das der rechner dann die rechnungs nummer nicht aendert, sonst
muessen wir wieder die nummern aenderbar machen und dann macht er wieder
fehler"*.
Genau das ist hier die Hauptpruefung: die Proforma laeuft in einer EIGENEN
Reihe (2026-P001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt.
Dazu die Rechtslage (nachgeschlagen am 03.09.2026):
* keine Nummer aus dem regulaeren Kreis - die fortlaufende Nummerierung nach
§ 14 Abs. 4 Nr. 4 UStG gilt nur fuer echte Rechnungen
* kein Steuersatz und kein Steuerbetrag auf dem Blatt (§ 14c UStG: wer Steuer
ausweist, schuldet sie)
* deutlich als "Proforma-Rechnung" kennzeichnen, sonst kann das Finanzamt das
Blatt als echte Rechnung werten
Gefahren wird das ECHTE Popup - die Knoepfe werden angeklickt, nicht umgangen.
"""
import json
import os
import shutil
import sys
import tempfile
import threading
QUELLE = r"C:\claude\rechnungstool"
JOURNAL = r"C:\claude\beherbergungssteuer"
sys.path.insert(0, QUELLE)
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kodierung = sys.stdout.encoding or "utf-8"
print(zeile.encode(kodierung, "replace").decode(kodierung))
if not bedingung:
fehler.append(text)
def _reissleine():
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
os._exit(2)
_wecker = threading.Timer(150, _reissleine)
_wecker.daemon = True
_wecker.start()
os.environ["CAMPINGHOF_TEST"] = "1"
tmp = tempfile.mkdtemp(prefix="proforma_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
roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
import app as A # noqa: E402
import bestand as BE # noqa: E402
from tkinter import messagebox # noqa: E402
antworten = {"Fertig": False, "Hausnummer": True}
def askyesno(titel, text, **kw):
for schlue, wert in antworten.items():
if schlue in titel:
return wert
raise AssertionError(f"unangemeldeter Dialog: {titel!r}")
messagebox.askyesno = askyesno
messagebox.showinfo = lambda *a, **k: None
warnungen = []
messagebox.showwarning = lambda *a, **k: warnungen.append(a[0] if a else "")
messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "")
f = A.RechnungsApp()
f.cfg["output_dir"] = ordner
# Dieser Pruefstand will das ECHTE Popup sehen und seine Knoepfe druecken.
f._dialog_zeigen = True
def knoepfe_von(fenster):
"""Alle Knopf-Widgets eines Fensters, egal wie tief."""
aus = []
def gehe(w):
for k in w.winfo_children():
if k.winfo_class() in ("TButton", "Button"):
aus.append(k)
gehe(k)
gehe(fenster)
return aus
def klicke_im_popup(teil):
"""Wartet auf das Popup und drueckt den Knopf, dessen Text `teil` enthaelt."""
def versuch(rest=40):
tops = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"]
if tops:
for k in knoepfe_von(tops[-1]):
if teil.lower() in str(k.cget("text")).lower():
k.invoke()
return
if rest:
f.after(100, lambda: versuch(rest - 1))
f.after(200, versuch)
def kunde(vorname, nachname):
f.k_vorname.set(vorname)
f.k_name.set(nachname)
f.k_strasse.set("Musterweg 1")
f.k_plzort.set("12345 Musterstadt")
f.r_anreise.set("01.08.2026")
f.r_abreise.set("04.08.2026")
f.anzahl_vars[sorted(f.anzahl_vars)[0]].set("3")
f.update()
# ---- 1. Der eine Knopf und das Popup --------------------------------------
knopf_texte = [str(k.cget("text")) for k in knoepfe_von(f)]
pruefe("PDF erstellen" in knopf_texte, f"unten steht EIN Knopf zum Erstellen ({knopf_texte})")
pruefe(not any("Proforma" in t for t in knopf_texte),
"und kein zweiter Knopf fuer die Proforma - gewaehlt wird im Popup")
kunde("Anna", "Vorab")
nummer_vorher = f.r_nummer.get()
zaehler_vorher = f.cfg.get("rechnung_zaehler")
pruefe(nummer_vorher == "2026-001", f"vorgeschlagen ist 2026-001 ({nummer_vorher})")
# ---- 2. Abbrechen schreibt nichts ----------------------------------------
klicke_im_popup("Abbrechen")
f.pdf_erstellen()
pruefe(not os.listdir(ordner), f"Abbrechen schreibt nichts ({os.listdir(ordner)})")
# ---- 3. Proforma waehlen -------------------------------------------------
klicke_im_popup("Proforma")
f.pdf_erstellen()
dateien = sorted(os.listdir(ordner))
pruefe(dateien == ["Proforma_2026-P001_Vorab.pdf"], f"die Proforma liegt da ({dateien})")
# ---- 4. DIE Hauptsache: die Rechnungsnummer ist unberuehrt --------------
pruefe(f.r_nummer.get() == nummer_vorher,
f"das Nummernfeld steht unveraendert auf {nummer_vorher} ({f.r_nummer.get()})")
pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher,
f"der Rechnungszaehler ist unveraendert ({f.cfg.get('rechnung_zaehler')})")
pruefe(f.cfg.get("proforma_zaehler") == 1,
f"nur der Proforma-Zaehler ist weitergelaufen ({f.cfg.get('proforma_zaehler')})")
# ---- 5. Was auf dem Blatt steht -----------------------------------------
from pypdf import PdfReader # noqa: E402
pfad = os.path.join(ordner, "Proforma_2026-P001_Vorab.pdf")
seiten = PdfReader(pfad).pages
pruefe(len(seiten) == 1, f"eine Seite - kein Blatt fuers Amt ({len(seiten)})")
text = (seiten[0].extract_text() or "").replace("\n", " ")
pruefe("Proforma-Rechnung" in text, "es steht deutlich Proforma-Rechnung drauf")
pruefe("keine Rechnung im Sinne des § 14 UStG" in text,
"mit dem Satz, dass es keine Rechnung nach § 14 UStG ist")
pruefe("keine Rechnungsnummer" in text, "und dass die Nummer keine Rechnungsnummer ist")
pruefe("2026-P001" in text and "2026-001" not in text,
"nur die Proforma-Nummer steht drauf, keine Rechnungsnummer")
pruefe("Mehrwertsteuer" not in text and "Nettobetrag" not in text,
"kein Steuerbetrag und kein Nettoausweis (§ 14c UStG)")
pruefe("Beherbergungssteuer" not in text and "Zwischensumme" not in text,
"auch die Beherbergungssteuer steht nicht drauf - gar keine Steuer wird ausgewiesen")
pruefe("alle Steuern und Abgaben enthalten" in text,
"dafuer der Hinweis, dass alles im Betrag enthalten ist")
pruefe("Voraussichtlicher Gesamtbetrag" in text,
"der Betrag heisst voraussichtlich, nicht Rechnungsbetrag")
meta = BE.kenndaten_lesen(pfad) or {}
pruefe(meta.get("art") == "proforma", f"art = proforma ({meta.get('art')})")
pruefe(float(meta.get("zwischensumme") or 0) == 0.0 and float(meta.get("gesamt") or 0) == 0.0,
f"die Kenndaten tragen Nullen ({meta.get('zwischensumme')}, {meta.get('gesamt')})")
# ---- 6. Danach die echte Rechnung: sie bekommt 2026-001 ----------------
antworten["Storno"] = True
klicke_im_popup("Rechnung 2026-001 erstellen")
f.pdf_erstellen()
pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-001_Vorab.pdf")),
f"die Rechnung 2026-001 ist geschrieben ({sorted(os.listdir(ordner))})")
pruefe(f.cfg.get("rechnung_zaehler") == 1,
f"jetzt erst laeuft der Rechnungszaehler weiter ({f.cfg.get('rechnung_zaehler')})")
# ---- 7. Die zweite Proforma zaehlt in ihrer Reihe weiter ---------------
f.reset_formular()
kunde("Bernd", "Zweitvorab")
klicke_im_popup("Proforma")
f.pdf_erstellen()
pruefe(os.path.exists(os.path.join(ordner, "Proforma_2026-P002_Zweitvorab.pdf")),
f"die zweite Proforma heisst P002 ({sorted(os.listdir(ordner))})")
f.destroy()
# ---- 8. Das Steuerjournal bucht die Proforma NICHT --------------------
sys.path.append(JOURNAL)
import db as BST # noqa: E402
journal = BST.Journal(os.path.join(tmp, "journal.sqlite3"))
journal.scanne(ordner, 5.0, voll=True)
buchungen = journal.buchungen(jahr=2026)
pruefe(len(buchungen) == 1, f"nur die echte Rechnung ist gebucht ({len(buchungen)})")
pruefe(buchungen and buchungen[0].voll_nummer == "2026-001",
f"und zwar 2026-001 ({[b.voll_nummer for b in buchungen]})")
pruefe(not any("P0" in b.voll_nummer for b in buchungen),
"keine Proforma-Nummer im Journal")
journal.schliessen()
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)