rechnungstool/pruef_nummernbuch.py
TheMockTv 5ebb02b267 Oberflaeche im Pcore-Stil: Tuerkis, Kacheln, Glas-Knoepfe, Roboto
Seine Ansage: "schau dir unsern fivem server an und unsere webseiten, dann
weisst du wie mein design ist" - also die Palette aus _ui_www und der
Adminpage: Tuerkis #06C6A4 auf gestuften dunklen Flaechen (#0d0e10 Grund,
#15171b Kachel), Raender #25282e, Text #e8e8ea / #8a8e96, Rot #e85a5a.

Das PROGRAMM traegt diese Handschrift, die RECHNUNG die Farbe der jeweiligen
Firma - das Blatt gehoert dem Kunden, nicht uns.

- Roboto wird mitgeliefert und nur fuer den Prozess geladen
  (AddFontResourceEx mit FR_PRIVATE): keine Installation, keine Adminrechte,
  nach dem Schliessen ist der Rechner wie vorher. Fehlt sie, bleibt es bei
  Segoe UI. Die Schriftdateien gehen mit in die exe (spec).
- Abschnitte sind Kacheln mit Ueberschrift statt grauer Rahmen.
- Knoepfe im Glas-Stil wie in seinen Oberflaechen: getoente Flaeche, Rand in
  Akzent-Transparenz. Tk kann kein backdrop-filter - die Toenung wird
  ausgerechnet (theme.mischen), das Ergebnis ist auf einfarbigem Grund
  identisch. "PDF erstellen" fuehrt, "Storno" ist rot.
- Zebrastreifen in der Leistungsliste: bei 17 Zeilen verrutscht man sonst.
- Kein expand am offenen Reiter mehr - dadurch sprang beim Umschalten die
  ganze Leiste ("das taben macht den aktiven kleiner").
- Das Fenster fuer den gemeinsamen Ordner stand als einziges noch in Weiss da:
  es kam nie durch _style_dialog.

Dazu ein Fund, der nichts mit dem Design zu tun hat: die Pruefstaende erbten
"nummer_eingerichtet" aus der config DIESES Rechners. Sobald hier jemand die
Nummer einmal einrichtet, liefen sie in einen anderen Zweig und wurden rot -
ohne dass sich am Programm etwas geaendert haette. Sie legen den Zustand jetzt
selbst fest. Alle 14 gruen.

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

190 lines
7 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: das gemeinsame Nummernbuch von Rechnungstool und Steuerjournal.
Der Fall, um den es geht (sein Satz vom 03.09.2026):
"er hat manuelle eingetragen die das andere programm die rechnungs nummer
nicht kennt - mit der sqlite beheben wir das"
Im Steuerjournal stehen Buchungen aus der alten Excel-Mappe, zu denen es keine
PDF gibt. Das Rechnungstool sucht die naechste freie Nummer bisher nur in
seinem eigenen Ordner - und vergibt so eine Nummer zum zweiten Mal
(§ 14 Abs. 4 Nr. 4 UStG: jede Nummer nur einmal).
Geprueft wird mit BEIDEN echten Programmen auf EINEM gemeinsamen Ordner.
"""
import json
import os
import shutil
import sys
import tempfile
import threading
TOOL = r"C:\claude\rechnungstool"
JOURNAL = r"C:\claude\beherbergungssteuer"
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)")
os._exit(2)
_wecker = threading.Timer(150, _reissleine)
_wecker.daemon = True
_wecker.start()
os.environ["CAMPINGHOF_TEST"] = "1" # kein Erststart-Dialog
tmp = tempfile.mkdtemp(prefix="buch_pruef_")
gemeinsam_ordner = os.path.join(tmp, "install") # hier liegt das Buch
rechnungen = os.path.join(tmp, "rechnungen")
journal_daten = os.path.join(tmp, "journal_daten")
for d in (gemeinsam_ordner, rechnungen, journal_daten):
os.makedirs(d)
# ---- Erst das Steuerjournal (eigener Datenordner) --------------------------
sys.path.insert(0, JOURNAL)
import ablage # noqa: E402
ablage.daten_ordner = lambda: journal_daten
import app as JA # noqa: E402 - das Journal
from modell import Buchung # noqa: E402
import gemeinsam # noqa: E402
del sys.modules["app"]
sys.path.remove(JOURNAL)
from datetime import date # noqa: E402
from tkinter import messagebox # noqa: E402
messagebox.showinfo = lambda *a, **k: None
messagebox.showwarning = lambda *a, **k: None
journal = JA.App()
journal.withdraw()
# Altbestand wie aus der Excel-Mappe: Nummern OHNE PDF.
for nr, name in (("001", "Alt Eins"), ("002", "Alt Zwei"), ("003", "Alt Drei")):
journal.journal.speichern(Buchung(date(2026, 5, 1), nr, 2026, name, 2, 50.0,
quelle="xlsx"))
journal.jahr.set(2026)
journal.aktualisieren()
journal.journal.setze("gemeinsam_ordner", gemeinsam_ordner)
journal._buch_oeffnen()
journal._buch_fuellen()
buch = gemeinsam.Nummernbuch(gemeinsam_ordner)
pruefe(os.path.exists(gemeinsam.pfad_zur_datei(gemeinsam_ordner)),
"das Journal legt das Nummernbuch im gemeinsamen Ordner an")
nummern = buch.nummern()
pruefe(nummern == {"2026-001", "2026-002", "2026-003"},
f"die drei Altbestands-Nummern stehen drin ({sorted(nummern)})")
eintrag = buch.eintrag("2026-002")
pruefe(eintrag and eintrag["herkunft"] == "xlsx" and eintrag["quelle"] == "journal",
f"mit Herkunft und Quelle ({eintrag and (eintrag['quelle'], eintrag['herkunft'])})")
# ---- Jetzt das Rechnungstool auf denselben Ordner --------------------------
sys.path.insert(0, TOOL)
import config # noqa: E402
shutil.copy(os.path.join(TOOL, "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"] = rechnungen
roh["gemeinsam_ordner"] = gemeinsam_ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
import app as A # noqa: E402 - das Rechnungstool
import bestand as BE # noqa: E402
antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True,
"Storno wirklich": 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
tool = A.RechnungsApp()
tool.cfg["output_dir"] = rechnungen
tool._buch_oeffnen()
pruefe(tool.buch is not None, "das Rechnungstool findet dasselbe Buch")
tool._neue_nummer_vorschlagen()
pruefe(tool.r_nummer.get() == "2026-004",
f"es schlaegt 2026-004 vor und ueberspringt den Altbestand ({tool.r_nummer.get()})")
# ---- Eine Rechnung schreiben: sie muss im Buch landen ---------------------
tool.k_vorname.set("Anna")
tool.k_name.set("Neu")
tool.k_strasse.set("Musterweg 1")
tool.k_plzort.set("12345 Musterstadt")
tool.r_anreise.set("01.09.2026")
tool.r_abreise.set("03.09.2026")
tool.anzahl_vars[sorted(tool.anzahl_vars)[0]].set("2")
tool.update()
tool.pdf_erstellen()
pruefe(os.path.exists(os.path.join(rechnungen, "Rechnung_2026-004_Neu.pdf")),
"die Rechnung 2026-004 ist geschrieben")
e4 = buch.eintrag("2026-004")
pruefe(e4 is not None, "und steht im gemeinsamen Buch")
pruefe(e4 and e4["quelle"] == "rechnungstool" and e4["nachname"] == "Neu",
f"mit Quelle rechnungstool und Namen ({e4 and (e4['quelle'], e4['nachname'])})")
pruefe(e4 and abs(float(e4["zwischensumme"]) - 26.0) < 0.005,
f"und dem Betrag ({e4 and e4['zwischensumme']})")
# ---- Storno: eigene Nummer ins Buch, beim Verwerfen wieder raus -----------
offen = [m for m in BE.rechnungen_im_ordner(rechnungen) if m.get("art") != "storno"]
tool._storno_anlegen(offen[0], rechnungen)
storno_nr = tool.storno_stand["nummer"] if tool.storno_stand else ""
pruefe(buch.belegt(storno_nr), f"der Storno {storno_nr} steht im Buch")
e_storno = buch.eintrag(storno_nr)
pruefe(e_storno and e_storno["storno_zu"] == "2026-004",
f"und sagt, welche Rechnung er aufhebt ({e_storno and e_storno['storno_zu']})")
tool._storno_verwerfen("Pruefstand")
pruefe(not buch.belegt(storno_nr),
f"beim Verwerfen ist die Nummer wieder frei ({storno_nr})")
# ---- Das Journal sieht die neue Rechnung im Buch --------------------------
journal._buch_oeffnen()
pruefe("2026-004" in journal.buch.nummern(),
"das Journal sieht die Nummer des Rechnungstools im Buch")
# ---- Faellt das Buch aus, laeuft trotzdem alles weiter --------------------
tool.cfg["gemeinsam_ordner"] = os.path.join(tmp, "gibtsnicht")
tool._buch_oeffnen()
pruefe(tool.buch is None, "ein nicht erreichbarer Ordner schaltet das Buch ab")
pruefe(tool._belegte_nummern() == set(), "die belegten Nummern sind dann leer")
tool._neue_nummer_vorschlagen()
pruefe(bool(tool.r_nummer.get()), f"und es wird trotzdem eine Nummer vorgeschlagen "
f"({tool.r_nummer.get()})")
tool.destroy()
journal.journal.schliessen()
journal.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)