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
227 lines
8.5 KiB
Python
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)
|