diff --git a/Steuerrechnungstool starten.bat b/Steuerrechnungstool starten.bat new file mode 100644 index 0000000..39ff4c0 --- /dev/null +++ b/Steuerrechnungstool starten.bat @@ -0,0 +1,3 @@ +@echo off +cd /d "%~dp0" +start "" pythonw app.py diff --git a/app.py b/app.py index 97b1df1..5d22828 100644 --- a/app.py +++ b/app.py @@ -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("", lambda _e: self.einlesen()) self.bind("", 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("", 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("", lambda _e: d.destroy()) + d.bind("", 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() diff --git a/hinweis.py b/hinweis.py new file mode 100644 index 0000000..170b3ee --- /dev/null +++ b/hinweis.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""Der Hinweis auf die Erprobungsfassung - und die Kenntnisnahme des Nutzers. + +WARUM ES DAS GIBT +----------------- +Beide Programme schreiben Belege, die steuerlich zaehlen. Solange sie erprobt +werden, muss der Betreiber wissen, woran er ist: dass es eine Erprobungsfassung +ist, dass fuer falsch ausgestellte Belege nicht gehaftet wird, und dass er jeden +Beleg vor dem Herausgeben SELBST ansehen muss - dafuer gibt es die Vorschau. + +Sein Wortlaut (06.09.2026): + + "bei jedem oeffnen, solange wir die testen, ein popup, dass dies eine + experimentelle software ist und ich keine haftung fuer irgendwelche falsch + ausgestellten uebernehme. der user muss selbst pruefen ueber den vorschau + button auf beiden tools, ob das alles seine richtigkeit hat. wenn nicht, + auf dem link eine issue erstellen. und das alles auch noch mal in die info + mit signatur: user hat dies gelesen und zur kenntnis genommen. wenn er nein + drueckt oder ablehnt, schliesst sich die software und geht beim naechsten + start wieder mit dem selben auf, bis er auf ja drueckt." + +WIE ES WIRKT +------------ +* Beim Start kommt der Hinweis, BEVOR gearbeitet werden kann. +* "Nein" beendet das Programm. Es wird nichts geschrieben, nichts gespeichert. +* "Ja" wird mit Benutzername, Zeitpunkt und Fassung in der Konfiguration + vermerkt - das ist die Signatur. Danach kommt der Hinweis nicht mehr. +* Aendert sich der TEXT (also FASSUNG), wird erneut gefragt. Eine Zustimmung + gilt fuer das, was dagestanden hat, nicht fuer alles Kuenftige. +* Unter "Info" steht derselbe Text dauerhaft, mit der Signaturzeile darunter. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber - sonst steht in zwei Programmen zweierlei. +""" + +import datetime +import getpass + +# Fassung des HINWEISTEXTES, nicht des Programms. Wird der Text geaendert, +# gehoert dieses Datum mitgeaendert - dann wird die Kenntnisnahme neu eingeholt. +FASSUNG = "2026-09-06" + +# Wo Fehler gemeldet werden. Je Programm eine eigene Adresse. +# +# Das frueher "Beherbergungssteuer" genannte Programm heisst seit dem +# 06.09.2026 STEUERRECHNUNGSTOOL - so wie sein Repository. Der lokale Ordner +# heisst noch beherbergungssteuer: der Pfad steht in mehreren Pruefstaenden und +# in den Spec-Dateien fest, das ist ein eigener Schritt. +ISSUES = { + "rechnungstool": "https://git.pcore.de/TheMockTv/rechnungstool", + "steuerrechnungstool": "https://git.pcore.de/TheMockTv/Steuerrechnungstool", +} + +TITEL = "Erprobungsfassung – bitte lesen" + + +def text(programm): + """Der Hinweistext. `programm` ist ein Schluessel aus ISSUES.""" + url = ISSUES.get(programm, "") + return ( + "Diese Software ist eine ERPROBUNGSFASSUNG.\n\n" + "Sie wird noch getestet. Für Belege, die falsch ausgestellt werden, " + "wird keine Haftung übernommen.\n\n" + "Prüfen Sie jeden Beleg selbst, bevor Sie ihn herausgeben oder " + "verbuchen. Nutzen Sie dafür die Vorschau – in diesem Programm und im " + "jeweils anderen. Stimmt etwas nicht, melden Sie es bitte als Fehler " + "unter:\n\n" + f" {url}\n\n" + "Mit „Ja\" bestätigen Sie, dass Sie diesen Hinweis gelesen und zur " + "Kenntnis genommen haben.\n" + "Mit „Nein\" wird das Programm beendet; der Hinweis erscheint beim " + "nächsten Start erneut." + ) + + +def _stand(cfg): + return (str(cfg.get("hinweis_fassung", "") or ""), + str(cfg.get("hinweis_zeit", "") or ""), + str(cfg.get("hinweis_benutzer", "") or "")) + + +def angenommen(cfg): + """Hat der Nutzer GENAU DIESE Fassung schon zur Kenntnis genommen?""" + fassung, zeit, benutzer = _stand(cfg) + return fassung == FASSUNG and bool(zeit) and bool(benutzer) + + +def vermerken(cfg): + """Die Kenntnisnahme festhalten. Gibt die Signaturzeile zurueck.""" + cfg["hinweis_fassung"] = FASSUNG + cfg["hinweis_zeit"] = datetime.datetime.now().isoformat(timespec="seconds") + try: + cfg["hinweis_benutzer"] = getpass.getuser() + except Exception: # noqa: BLE001 - ohne Benutzernamen ist es kein Grund zu scheitern + cfg["hinweis_benutzer"] = "unbekannt" + return signatur(cfg) + + +def signatur(cfg): + """Die Zeile fuer den Info-Dialog - oder '' , wenn nichts vorliegt.""" + fassung, zeit, benutzer = _stand(cfg) + if not (fassung and zeit and benutzer): + return "" + try: + wann = datetime.datetime.fromisoformat(zeit).strftime("%d.%m.%Y um %H:%M") + except ValueError: + wann = zeit + return (f"Gelesen und zur Kenntnis genommen von {benutzer} " + f"am {wann} (Fassung {fassung}).") diff --git a/pruef_doppelte.py b/pruef_doppelte.py index 610f684..f31ee8e 100644 --- a/pruef_doppelte.py +++ b/pruef_doppelte.py @@ -51,6 +51,28 @@ import ablage # noqa: E402 ablage.daten_ordner = lambda: tmp import app as app_mod # noqa: E402 (liest ablage.daten_ordner beim Import) +from tkinter import messagebox # noqa: E402 + +# Kein echtes Fenster im Pruefstand: sonst steht der Lauf vor einem Dialog, den +# niemand anklickt. Der Erprobungshinweis wird hier bejaht - geprueft wird er +# eigens in pruef_hinweis.py des Rechnungstools. Eine UNBEKANNTE Frage bricht +# ab, statt still 'ja' zu sagen: sonst uebergeht der Lauf genau die Faelle, +# wegen denen es die Pruefung gibt. +_antworten = {"Erprobungsfassung": True, "jetzt erstellen?": True, + "Fertig": False, "Hausnummer": True} + + +def _askyesno(titel, text="", **kw): + for schluessel, wert in _antworten.items(): + if schluessel in str(titel): + return wert + raise AssertionError(f"unangemeldeter Dialog: {titel!r}") + + +messagebox.askyesno = _askyesno +messagebox.showinfo = lambda *a, **k: None +messagebox.showwarning = lambda *a, **k: None + from modell import Buchung, doppelte_nummern, summiere # noqa: E402 pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})") diff --git a/pruef_storno_journal.py b/pruef_storno_journal.py index 989b2a8..e0a58a8 100644 --- a/pruef_storno_journal.py +++ b/pruef_storno_journal.py @@ -63,9 +63,13 @@ import app as A # noqa: E402 import bestand as BE # noqa: E402 from tkinter import messagebox # noqa: E402 +# Erprobungshinweis und Freigabe vor dem Beleg werden hier bejaht - geprueft +# werden sie eigens in pruef_hinweis.py und pruef_freigabe.py des Rechnungstools. antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True, "Storno wirklich": True, "Berichtigte Rechnung erstellen": True, "Berichtigung": True, "Storno erstellen": True} +antworten["Erprobungsfassung"] = True +antworten["jetzt erstellen?"] = True def askyesno(titel, text, **kw): diff --git a/pruef_storno_verrechnet.py b/pruef_storno_verrechnet.py index 394cc40..88394d6 100644 --- a/pruef_storno_verrechnet.py +++ b/pruef_storno_verrechnet.py @@ -61,6 +61,28 @@ import bericht_pdf # noqa: E402 import firma as firma_lib # noqa: E402 from pypdf import PdfReader # noqa: E402 import app as app_mod # noqa: E402 +from tkinter import messagebox # noqa: E402 + +# Kein echtes Fenster im Pruefstand: sonst steht der Lauf vor einem Dialog, den +# niemand anklickt. Der Erprobungshinweis wird hier bejaht - geprueft wird er +# eigens in pruef_hinweis.py des Rechnungstools. Eine UNBEKANNTE Frage bricht +# ab, statt still 'ja' zu sagen: sonst uebergeht der Lauf genau die Faelle, +# wegen denen es die Pruefung gibt. +_antworten = {"Erprobungsfassung": True, "jetzt erstellen?": True, + "Fertig": False, "Hausnummer": True} + + +def _askyesno(titel, text="", **kw): + for schluessel, wert in _antworten.items(): + if schluessel in str(titel): + return wert + raise AssertionError(f"unangemeldeter Dialog: {titel!r}") + + +messagebox.askyesno = _askyesno +messagebox.showinfo = lambda *a, **k: None +messagebox.showwarning = lambda *a, **k: None + from gui_monat import SPALTEN # noqa: E402 pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})") diff --git a/pruef_wache.py b/pruef_wache.py index 5ab1baa..809728c 100644 --- a/pruef_wache.py +++ b/pruef_wache.py @@ -71,6 +71,10 @@ messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: None journal = JA.App() +# Der Erprobungshinweis haengt an after(0) - ohne Ereignisschleife kaeme er nie +# dran, und damit auch der verzoegerte Start nicht. update() treibt die Schleife +# einmal an, genau wie es mainloop() taete. Abgekuerzt wird nichts. +journal.update() journal.withdraw() journal.pdf_ordner = rechnungen journal.journal.setze("pdf_ordner", rechnungen) @@ -85,8 +89,15 @@ pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner), journal._anmelden() pruefe(gemeinsam.journal_laeuft(gemeinsam_ordner), "nach der Anmeldung sieht man an der PID, dass es laeuft") -pruefe(os.listdir(gemeinsam_ordner) == [gemeinsam.DATEINAME], - f"im gemeinsamen Ordner liegt genau EINE Datei ({os.listdir(gemeinsam_ordner)})") +# Die Absicht dieser Pruefung: im gemeinsamen Ordner liegt NICHTS Fremdes - +# keine Herzschlagdateien, keine Zwischenstaende. Erlaubt sind genau zwei: +# das Nummernbuch und der Schluessel der Mini-API, der seit dem Umbau vom +# 05.09.2026 dorthin gehoert (beide Programme muessen ihn lesen koennen). +# Alles darueber hinaus faellt weiterhin auf. +ERLAUBT = {gemeinsam.DATEINAME, "api_schluessel.txt"} +_liegt = set(os.listdir(gemeinsam_ordner)) +pruefe(_liegt <= ERLAUBT, + f"im gemeinsamen Ordner liegt nichts Fremdes ({sorted(_liegt)})") # ---- 1b. Die Mini-API: Port im Telefonbuch, und sie meldet sich ----------- import api # noqa: E402 @@ -118,8 +129,12 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal import app as A # noqa: E402 +# Erprobungshinweis und Freigabe vor dem Beleg werden hier bejaht - geprueft +# werden sie eigens in pruef_hinweis.py und pruef_freigabe.py des Rechnungstools. antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True, "Storno wirklich": True, "Storno verwerfen": True} +antworten["Erprobungsfassung"] = True +antworten["jetzt erstellen?"] = True def askyesno(titel, text, **kw): @@ -132,6 +147,7 @@ def askyesno(titel, text, **kw): messagebox.askyesno = askyesno tool = A.RechnungsApp() +tool.update() # dasselbe fuer das Rechnungstool tool.cfg["output_dir"] = rechnungen tool.cfg["gemeinsam_ordner"] = gemeinsam_ordner tool._buch_oeffnen()