Steuerrechnungstool: neuer Name und Erprobungshinweis wie im Rechnungstool

NAME
Das Programm heisst jetzt Steuerrechnungstool - wie sein Repository
(https://git.pcore.de/TheMockTv/Steuerrechnungstool). Umgestellt: Fenstertitel,
Kopfzeile, "laeuft bereits"-Meldung, Startdatei, Spec-Datei (EXE-Name), Logzeile.

NICHT umbenannt, mit Absicht:
  * "Beherbergungssteuer" als NAME DER STEUER - das ist ein Rechtsbegriff und
    steht weiter so in Berichten, Anmeldung und Oberflaeche.
  * LOCALAPPDATA\Beherbergungssteuer in ablage.py - dort liegt der Zeiger auf
    den Datenordner. Wer den umbenennt, kappt bestehenden Installationen den
    Weg zu ihren Daten.
  * Der lokale Ordner C:\claude\beherbergungssteuer - steht in Pruefstaenden
    und Spec-Dateien fest, eigener Schritt.

HINWEIS
hinweis.py aus dem Rechnungstool uebernommen (liegt in beiden Programmen
gleich). Der Hinweis kommt beim Start, Ablehnen beendet das Programm ohne
etwas zu vermerken, die Kenntnisnahme wird mit Benutzer, Zeit und Fassung in
der Einstellungstabelle des Journals gespeichert. Neu: Menue Hilfe -> Info mit
dem Text und der Signaturzeile.

PRUEFSTAENDE
Vier Prueflaeufe hatten gar keinen Dialogabfang - bisher fragte das Programm
beim Start nichts. Jetzt bleiben sie sonst vor einem echten Fenster stehen.
pruef_doppelte und pruef_storno_verrechnet haben einen bekommen, der bei
UNBEKANNTEN Fragen abbricht statt still "ja" zu sagen. pruef_storno_journal und
pruef_wache melden Hinweis und Freigabe ausdruecklich an. pruef_wache treibt
ausserdem die Ereignisschleife an (update), sonst kaeme after_idle nie dran.

pruef_wache: die Erwartung "im gemeinsamen Ordner liegt genau EINE Datei" ist
auf die richtige Menge gebracht - Nummernbuch UND Schluessel der Mini-API, der
seit dem 05.09.2026 dorthin gehoert. Alles darueber hinaus faellt weiter auf.

⚠️ pruef_wache bleibt ROT an einer Stelle, die NICHT von diesen Arbeiten kommt
(vor dem Umbau gemessen, war schon rot): die Statuszeile soll nach einem Push
"Rechnungstool meldet" zeigen, wird aber vom Ergebnis des Einlesens
ueberschrieben. Dokumentiert in AUFGABE_BEWEISKETTE_PRUEFBAR.md.

5 von 6 Pruefstaenden gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
TheMockTv 2026-09-06 16:24:43 +02:00
parent f5f196151e
commit 40f53c85a0
7 changed files with 265 additions and 13 deletions

98
app.py
View file

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
"""
Beherbergungssteuer – Steuerjournal für den Campinghof Bartl.
Steuerrechnungstool – Steuerjournal für den Campinghof Bartl.
Ersetzt die Excel-Mappe: Die Rechnungs-PDFs aus dem Rechnungstool sind die
Quelle, das Programm liest sie ein, führt das Journal (SQLite) und schreibt
auf Knopfdruck die Excel-Mappe fürs Amt.
Start: Doppelklick auf "Beherbergungssteuer starten.bat" oder `python app.py`
Start: Doppelklick auf "Steuerrechnungstool starten.bat" oder `python app.py`
"""
import os
@ -25,6 +25,7 @@ from tkinter import ttk, filedialog, messagebox
# zieht alte Dateien um; das darf nicht zweimal gleichzeitig laufen.
# Nur beim echten Start: Pruefstaende importieren app als Modul und sollen
# nicht an der Sperre des laufenden Programms scheitern.
import hinweis
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen,
fenster_nach_vorn, laufende_pid_lesen)
@ -37,7 +38,7 @@ if __name__ == "__main__":
_wurzel.withdraw()
_wurzel.attributes("-topmost", True)
messagebox.showerror(
"Beherbergungssteuer läuft bereits",
"Steuerrechnungstool läuft bereits",
"Das Steuerjournal ist schon geöffnet, das Fenster ließ sich aber "
"nicht nach vorn holen – bitte über die Taskleiste zurückwechseln.")
_wurzel.destroy()
@ -78,7 +79,7 @@ log = logging.getLogger("bst.app")
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Beherbergungssteuer – Campinghof Bartl")
self.title("Steuerrechnungstool – Campinghof Bartl")
self.geometry("1000x680")
self.minsize(820, 520)
@ -94,26 +95,71 @@ class App(tk.Tk):
self.suche = tk.StringVar()
self.suche.trace_add("write", lambda *_: self.aktualisieren())
os.makedirs(BERICHTE, exist_ok=True)
self._menue()
self._kopfleiste()
self._blaetter()
self._statusleiste()
self.bind("<F5>", lambda _e: self.einlesen())
self.bind("<Control-f>", lambda _e: self.suchfeld.focus_set())
self.protocol("WM_DELETE_WINDOW", self._beenden)
self.aktualisieren()
# NICHTS laeuft an, bevor der Erprobungshinweis bestaetigt ist - sonst
# stuenden beim Ablehnen Timer in der Warteschlange, die auf ein
# Fenster zugreifen, das es nicht mehr gibt.
self._briefkasten_stand = "?" # "?" = noch nicht nachgesehen
self._push_kasten = queue.Queue() # Zettel aus dem API-Faden
self._nachricht_stand = 0 # bis hierher sind Meldungen gelesen
# after_idle statt after(0): laeuft, sobald Tk nichts mehr zu zeichnen hat -
# also nachdem das Hauptfenster steht. Bei after(0) haengt der modale Dialog
# an einem noch ungezeichneten Fenster und wirkt wie eingefroren; eine feste
# Wartezeit dagegen erreicht ein Pruefstand nie, der keine Schleife dreht.
self.after_idle(self._hinweis_pruefen)
# ------------------------------------------------- Erprobungshinweis
def _hinweis_ablage(self):
"""hinweis.py erwartet etwas mit .get() und []= - hier ist das die
Einstellungstabelle des Journals.
"""
journal = self.journal
class Ablage:
def get(self, schluessel, standard=""):
return journal.hole(schluessel, standard)
def __setitem__(self, schluessel, wert):
journal.setze(schluessel, str(wert))
return Ablage()
def _hinweis_pruefen(self):
"""Hinweis auf die Erprobungsfassung. Ablehnen beendet das Programm."""
self.update_idletasks() # Fenster fertig zeichnen lassen
# Nach vorn holen, sonst liegt der Hinweis beim Start hinter dem
# Hauptfenster und wirkt wie ein Programm, das haengen geblieben ist.
self.lift()
self.focus_force()
ablage = self._hinweis_ablage()
if not hinweis.angenommen(ablage):
log.info("Erprobungshinweis: Kenntnisnahme noch offen (%s)", hinweis.FASSUNG)
if not messagebox.askyesno(hinweis.TITEL,
hinweis.text("steuerrechnungstool"),
parent=self, default="no"):
log.info("Erprobungshinweis abgelehnt - Programm wird beendet")
self.destroy()
return
zeile = hinweis.vermerken(ablage)
log.info("Erprobungshinweis: %s", zeile)
self._start_fortsetzen()
def _start_fortsetzen(self):
"""Der uebrige Start - laeuft erst nach der Kenntnisnahme an."""
if self.pdf_ordner and os.path.isdir(self.pdf_ordner):
self.after(200, lambda: self.einlesen(still=True))
self.after(400, self._frage_gemeinsam_falls_noetig)
# Beide Programme laufen nebeneinander: schreibt das Rechnungstool eine
# Rechnung, meldet es das hier herein - siehe _wache_jetzt().
self._briefkasten_stand = "?" # "?" = noch nicht nachgesehen
self._push_kasten = queue.Queue() # Zettel aus dem API-Faden
self.after(250, self._push_abholen)
self._nachricht_stand = 0 # bis hierher sind Meldungen gelesen
self.after(600, self._wache)
# Beim Zurueckklicken sofort nachsehen statt zwei Sekunden zu warten.
self.bind("<FocusIn>", lambda _e: self._wache_jetzt())
@ -143,8 +189,38 @@ class App(tk.Tk):
daten.add_command(label="Gemeinsamer Ordner (Nummernbuch)…",
command=self.dlg_gemeinsam)
leiste.add_cascade(label="Daten", menu=daten)
hilfe = tk.Menu(leiste, tearoff=0)
hilfe.add_command(label="Info", command=self._info)
leiste.add_cascade(label="Hilfe", menu=hilfe)
self.config(menu=leiste)
def _info(self):
"""Erprobungshinweis und Signatur - dauerhaft nachlesbar.
Ohne diese Seite waere die Kenntnisnahme ein Klick, den spaeter
niemand mehr nachlesen kann.
"""
d = tk.Toplevel(self)
d.title("Info")
d.transient(self)
d.resizable(False, False)
rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text="Steuerrechnungstool", font=("Segoe UI", 11, "bold")
).pack(anchor="w")
ttk.Label(rahmen, justify="left", wraplength=460,
text=hinweis.text("steuerrechnungstool")).pack(anchor="w", pady=(10, 0))
zeile = hinweis.signatur(self._hinweis_ablage())
if zeile:
ttk.Label(rahmen, justify="left", wraplength=460, text=zeile
).pack(anchor="w", pady=(12, 0))
ttk.Button(rahmen, text="OK", width=10, command=d.destroy
).pack(anchor="e", pady=(16, 0))
d.bind("<Escape>", lambda _e: d.destroy())
d.bind("<Return>", lambda _e: d.destroy())
d.grab_set()
def _kopfleiste(self):
leiste = ttk.Frame(self, padding=(10, 8))
leiste.pack(fill="x")
@ -737,7 +813,7 @@ class App(tk.Tk):
if __name__ == "__main__":
log.info("Beherbergungssteuer startet (Python %s)", sys.version.split()[0])
log.info("Steuerrechnungstool startet (Python %s)", sys.version.split()[0])
# Die Sperre selbst greift schon ganz oben, vor dem Datenordner.
eigene_pid_merken()
App().mainloop()