# -*- coding: utf-8 -*- """ Beherbergungssteuer – 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` """ import os import sys import logging from datetime import date import tkinter as tk from tkinter import ttk, filedialog, messagebox # Nur EIN Fenster. Zwei Instanzen lesen denselben Rechnungsordner in dieselbe # journal.sqlite3: was das eine loescht, steht im anderen noch da, und der # Bericht fuers Amt kaeme aus dem veralteten Fenster. Die Sperre steht VOR # ablage.daten_ordner() - der legt beim ersten Start den Datenordner an und # 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. from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn, laufende_pid_lesen) _SPERRE = None if __name__ == "__main__": _SPERRE = einzelinstanz_pruefen() if _SPERRE is False: if not fenster_nach_vorn(laufende_pid_lesen()): _wurzel = tk.Tk() _wurzel.withdraw() _wurzel.attributes("-topmost", True) messagebox.showerror( "Beherbergungssteuer 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() sys.exit(0) import db import xlsx_io import firma as firma_lib import bericht_pdf import ablage from modell import MONATE, STANDARD_SATZ, summiere, eur, eur_kurz, doppelte_nummern from gui_monat import MonatsTab, SummenLeiste, WARNUNG from gui_dialoge import BuchungDialog # Als EXE liegt alles in einem eigenen Datenordner - siehe ablage.py. BASE_DIR = ablage.daten_ordner() JOURNAL = os.path.join(BASE_DIR, "journal.sqlite3") LOGDATEI = os.path.join(BASE_DIR, "beherbergungssteuer.log") BERICHTE = os.path.join(BASE_DIR, "berichte") logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)-8s %(message)s", handlers=[logging.FileHandler(LOGDATEI, encoding="utf-8"), logging.StreamHandler()], ) log = logging.getLogger("bst.app") class App(tk.Tk): def __init__(self): super().__init__() self.title("Beherbergungssteuer – Campinghof Bartl") self.geometry("1000x680") self.minsize(820, 520) self.journal = db.Journal(JOURNAL) self.satz = float(self.journal.hole("satz", str(STANDARD_SATZ))) self.pdf_ordner = self.journal.hole("pdf_ordner", "") self.jahr = tk.IntVar(value=int(self.journal.hole("jahr", str(date.today().year)))) self.firma = firma_lib.lade(self.journal.hole("rechnungstool_config", "")) 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.bind("", lambda _e: self.buchung_loeschen()) self.protocol("WM_DELETE_WINDOW", self._beenden) self.aktualisieren() if self.pdf_ordner and os.path.isdir(self.pdf_ordner): self.after(200, lambda: self.einlesen(still=True)) # ------------------------------------------------------------ Aufbau ---- def _menue(self): leiste = tk.Menu(self) datei = tk.Menu(leiste, tearoff=0) datei.add_command(label="Rechnungs-Ordner wählen…", command=self.ordner_waehlen) datei.add_separator() datei.add_command(label="Monatsbericht fürs Amt (PDF)…", command=self.bericht_monat) datei.add_command(label="Jahresbericht fürs Amt (PDF)…", command=self.bericht_jahr) datei.add_separator() datei.add_command(label="Excel-Tabelle einlesen…", command=self.altbestand_importieren) datei.add_separator() datei.add_command(label="Beenden", command=self._beenden) leiste.add_cascade(label="Datei", menu=datei) daten = tk.Menu(leiste, tearoff=0) daten.add_command(label="Rechnungen einlesen (F5)", command=self.einlesen) daten.add_command(label="Alle Rechnungen neu einlesen", command=lambda: self.einlesen(voll=True)) daten.add_separator() daten.add_command(label="Markierte Buchung löschen (Entf)", command=self.buchung_loeschen) daten.add_separator() daten.add_command(label="Steuersatz ändern…", command=self.satz_aendern) daten.add_command(label="Firmendaten des Rechnungstools wählen…", command=self.firma_waehlen) leiste.add_cascade(label="Daten", menu=daten) self.config(menu=leiste) def _kopfleiste(self): leiste = ttk.Frame(self, padding=(10, 8)) leiste.pack(fill="x") ttk.Label(leiste, text="Jahr:").pack(side="left") self.jahr_wahl = ttk.Combobox(leiste, width=6, state="readonly", textvariable=self.jahr, values=self._jahre()) self.jahr_wahl.pack(side="left", padx=(4, 14)) self.jahr_wahl.bind("<>", lambda _e: self.jahr_gewechselt()) ttk.Button(leiste, text="Rechnungen einlesen", command=self.einlesen).pack(side="left") ttk.Button(leiste, text="PDF fürs Amt", command=self.bericht_monat).pack(side="left", padx=(6, 0)) self.papierkorb = ttk.Button(leiste, text="🗑 Löschen", state="disabled", command=self.angehakte_loeschen) self.papierkorb.pack(side="left", padx=(6, 0)) ttk.Label(leiste, text="Suche:").pack(side="left", padx=(20, 4)) self.suchfeld = ttk.Entry(leiste, textvariable=self.suche, width=22) self.suchfeld.pack(side="left") self.suchfeld.bind("", lambda _e: self.suche_leeren()) ttk.Button(leiste, text="×", width=3, command=self.suche_leeren).pack(side="left", padx=(3, 0)) def _blaetter(self): self.blaetter = ttk.Notebook(self) self.blaetter.pack(fill="both", expand=True, padx=10, pady=(0, 6)) self.monate = {} for m in range(1, 13): tab = MonatsTab(self.blaetter, m, bei_doppelklick=self.buchung_bearbeiten, bei_haken=self.papierkorb_auffrischen) self.blaetter.add(tab, text=MONATE[m - 1]) self.monate[m] = tab self.blaetter.select(min(date.today().month, 12) - 1) def _statusleiste(self): rahmen = ttk.Frame(self, padding=(10, 2, 10, 6)) rahmen.pack(fill="x") self.jahr_titel = ttk.Label(rahmen, text="", font=("Segoe UI", 9, "bold")) self.jahr_titel.pack(anchor="w") self.jahr_summe = SummenLeiste(rahmen, mit_schnitt=True) self.jahr_summe.pack(fill="x", pady=(2, 4)) self.fuss = ttk.Frame(rahmen) self.fuss.pack(fill="x") self.status = ttk.Label(self.fuss, text="", foreground="black") self.status.pack(side="left") self.ordner_anzeige = ttk.Label(self.fuss, text="", foreground="black") self.ordner_anzeige.pack(side="right") # Doppelt vergebene Rechnungsnummern. Die Leiste wird erst eingehaengt, # wenn es wirklich welche gibt - im Normalfall soll hier nichts stehen # und auch kein leerer Streifen Platz wegnehmen. self._doppelte = {} self.doppelt_leiste = ttk.Frame(rahmen) self.knopf_doppelte = ttk.Button(self.doppelt_leiste, text="Doppelte Rechnungen anzeigen…", command=self.doppelte_zeigen) self.knopf_doppelte.pack(side="right", padx=(10, 0)) self.warnung = tk.Label(self.doppelt_leiste, text="", fg=WARNUNG, anchor="w", justify="left", wraplength=760, font=("Segoe UI", 9)) self.warnung.pack(side="left", fill="x", expand=True) # ----------------------------------------------------------- Anzeige ---- def _jahre(self): vorhanden = set(self.journal.jahre()) | {date.today().year, self.jahr.get()} return [str(j) for j in sorted(vorhanden)] def _passt(self, b, begriff: str) -> bool: """Sucht in Nachname, Rechnungsnummer und Datum.""" return begriff in " ".join(( b.nachname.lower(), b.voll_nummer.lower(), b.rechnungsnummer.lower(), b.datum.strftime("%d.%m.%Y"), )) def aktualisieren(self): jahr = self.jahr.get() alle = self.journal.buchungen(jahr=jahr) begriff = self.suche.get().strip().lower() sichtbar = [b for b in alle if self._passt(b, begriff)] if begriff else alle # Ueber ALLE Buchungen des Jahres, nicht ueber die gefilterten: sonst # versteckt die Suche den zweiten Beleg und die Dublette waere weg. self._doppelte = doppelte_nummern(alle) for m in range(1, 13): self.monate[m].fuellen([b for b in sichtbar if b.monat == m], self._doppelte) # Reiter des Monats markieren, damit man die Dublette auch dann # sieht, wenn gerade ein anderer Monat offen ist. betroffen = any(b.voll_nummer in self._doppelte for b in alle if b.monat == m) self.blaetter.tab(m - 1, text=("⚠ " if betroffen else "") + MONATE[m - 1]) self._doppelte_melden() if begriff: self.jahr_titel.config(text=f"Jahr {jahr} – Treffer für „{self.suche.get().strip()}“") # zum ersten Monat mit Treffern springen monate_mit_treffern = sorted({b.monat for b in sichtbar}) if monate_mit_treffern and self._monat not in monate_mit_treffern: self.blaetter.select(monate_mit_treffern[0] - 1) else: self.jahr_titel.config(text=f"Jahr {jahr} – gesamt") self.jahr_summe.setzen(summiere(sichtbar), sichtbar) self.jahr_wahl.config(values=self._jahre()) self.papierkorb_auffrischen() self.ordner_anzeige.config( text=f"Rechnungen: {self.pdf_ordner}" if self.pdf_ordner else "kein Rechnungs-Ordner gewählt (Datei → Rechnungs-Ordner wählen…)") def suche_leeren(self): self.suche.set("") self.suchfeld.focus_set() def melde(self, text: str): self.status.config(text=text) log.info(text) # ---- Doppelt vergebene Rechnungsnummern -------------------------------- def _doppelte_melden(self): """Warnleiste zeigen, solange es Dubletten gibt - sonst verschwindet sie.""" if not self._doppelte: self.doppelt_leiste.pack_forget() self.warnung.config(text="") return teile = [] for nr, liste in list(self._doppelte.items())[:3]: namen = ", ".join(sorted({b.nachname or "?" for b in liste})) teile.append(f"{nr} ({len(liste)}× – {namen})") rest = len(self._doppelte) - len(teile) self.warnung.config(text=( f"⚠ {len(self._doppelte)} Rechnungsnummer(n) doppelt vergeben: " + "; ".join(teile) + (f" … und {rest} weitere" if rest else "") + ". Die Beträge stehen trotzdem in allen Summen und im Amtsbericht – " "hier wird nichts stillschweigend weggerechnet.")) self.doppelt_leiste.pack(fill="x", pady=(0, 4), before=self.fuss) def doppelte_zeigen(self): """Alle Belege auflisten, die sich eine Rechnungsnummer teilen.""" fenster = tk.Toplevel(self) fenster.title("Doppelt vergebene Rechnungsnummern") fenster.transient(self) fenster.geometry("940x520") rahmen = ttk.Frame(fenster, padding=12) rahmen.pack(fill="both", expand=True) ttk.Label(rahmen, wraplength=900, justify="left", text=( f"Jahr {self.jahr.get()}: diese Rechnungsnummern kommen mehrfach vor. " "Jede Nummer darf es nur einmal geben (§ 14 Abs. 4 Nr. 4 UStG).\n\n" "Alle Beträge sind in den Summen und im Bericht fürs Amt enthalten – hier wird " "nichts weggerechnet. Zwei Zeilen zur selben Rechnung (z. B. einmal aus dem " "Excel-Altbestand und einmal aus der PDF) melden die Übernachtung doppelt ans " "Amt; zwei verschiedene Gäste unter einer Nummer sind ein Fehler im " "Rechnungstool. Was zu tun ist, entscheidest du – das Programm ändert nichts " "von allein.")).pack(anchor="w", pady=(0, 10)) innen = ttk.Frame(rahmen) innen.pack(fill="both", expand=True) innen.rowconfigure(0, weight=1) innen.columnconfigure(0, weight=1) senkrecht = ttk.Scrollbar(innen, orient="vertical") senkrecht.grid(row=0, column=1, sticky="ns") # Waagerecht, weil am Zeilenende der Dateiname steht - genau der sagt, # welche der beiden Rechnungen die zweite ist. waagerecht = ttk.Scrollbar(innen, orient="horizontal") waagerecht.grid(row=1, column=0, sticky="ew") text = tk.Text(innen, wrap="none", yscrollcommand=senkrecht.set, xscrollcommand=waagerecht.set, relief="flat", font=("Consolas", 9)) text.grid(row=0, column=0, sticky="nsew") senkrecht.configure(command=text.yview) waagerecht.configure(command=text.xview) text.tag_configure("kopf", foreground=WARNUNG, font=("Consolas", 9, "bold")) for nr, liste in self._doppelte.items(): text.insert("end", f"{nr} – {len(liste)} Belege\n", "kopf") for b in liste: datei = os.path.basename(b.pdf_pfad) if b.pdf_pfad else f"({b.quelle})" text.insert("end", f" {b.datum:%d.%m.%Y} {(b.nachname or '?'):22.22s} " f"{b.naechte:>2} {'Nacht ' if b.naechte == 1 else 'Nächte'} " f"{eur_kurz(b.gezahlt):>10s} " f"Steuer {eur_kurz(b.steuer):>8s} {datei}\n") text.insert("end", "\n") text.configure(state="disabled") ttk.Button(rahmen, text="Schließen", command=fenster.destroy).pack(side="right", pady=(10, 0)) fenster.bind("", lambda _e: fenster.destroy()) def jahr_gewechselt(self): self.journal.setze("jahr", self.jahr.get()) self.aktualisieren() # ---------------------------------------------------------- Aktionen ---- def ordner_waehlen(self): ordner = filedialog.askdirectory( title="Ordner mit den Rechnungs-PDFs wählen", initialdir=self.pdf_ordner or os.path.expanduser("~")) if not ordner: return self.pdf_ordner = os.path.abspath(ordner) self.journal.setze("pdf_ordner", self.pdf_ordner) self.aktualisieren() self.einlesen() def einlesen(self, voll: bool = False, still: bool = False): if not self.pdf_ordner or not os.path.isdir(self.pdf_ordner): if not still: messagebox.showinfo("Rechnungs-Ordner", "Bitte zuerst den Ordner mit den Rechnungs-PDFs wählen.", parent=self) return self.config(cursor="watch") self.melde("Rechnungen werden eingelesen…") self.update_idletasks() try: neu, akt, unveraendert, fehler = self.journal.scanne( self.pdf_ordner, self.satz, voll=voll) finally: self.config(cursor="") self.aktualisieren() text = f"{neu} neu, {akt} aktualisiert, {unveraendert} unverändert" if fehler: text += f", {len(fehler)} übersprungen" self.melde(text) if fehler and not still: namen = "\n".join(f"• {os.path.basename(p)} – {g}" for p, g in fehler[:12]) if len(fehler) > 12: namen += f"\n… und {len(fehler) - 12} weitere" messagebox.showwarning( "Nicht eingelesen", f"{len(fehler)} Datei(en) sind keine lesbare Rechnung:\n\n{namen}", parent=self) # Es gibt bewusst KEIN "Buchung von Hand anlegen" mehr: jede Zeile im Journal # gehoert zu einer Rechnung aus dem Rechnungstool. Wer eine Buchung braucht, # schreibt die Rechnung - sonst meldet das Journal ans Amt, was in keinem # Rechnungsordner steht. Bestehende Zeilen lassen sich weiter bearbeiten. def buchung_bearbeiten(self, buchung): d = BuchungDialog(self, buchung=buchung, satz=self.satz) self.wait_window(d) if d.loeschen: self.journal.loeschen(buchung.id) self.aktualisieren() self.melde(f"Buchung {buchung.voll_nummer} gelöscht") return if d.ergebnis: # Nummer/Jahr können geändert worden sein -> alte Zeile weicht der neuen if d.ergebnis.schluessel != buchung.schluessel and buchung.id: self.journal.loeschen(buchung.id) self.journal.speichern(d.ergebnis) self.aktualisieren() self.melde(f"Buchung {d.ergebnis.voll_nummer} geändert") def _markierte(self): return self.monate[self._monat].markierte() def buchung_loeschen(self): b = self._markierte() if not b: return if not messagebox.askyesno( "Buchung löschen", f"Rechnung {b.voll_nummer} ({b.nachname}) aus dem Journal löschen?\n\n" "Die PDF-Datei bleibt erhalten – beim nächsten Einlesen " "taucht die Buchung wieder auf.", parent=self): return self.journal.loeschen(b.id) self.aktualisieren() self.melde(f"Buchung {b.voll_nummer} gelöscht") # ---- Anhaken + Papierkorb ---------------------------------------------- def _angehakte(self): """Alle angehakten Buchungen – über alle Monate hinweg.""" return [b for m in range(1, 13) for b in self.monate[m].angehakte()] def papierkorb_auffrischen(self): anzahl = len(self._angehakte()) self.papierkorb.config( text=f"🗑 Löschen ({anzahl})" if anzahl else "🗑 Löschen", state="normal" if anzahl else "disabled") def angehakte_loeschen(self): angehakt = self._angehakte() if not angehakt: return aus_pdf = [b for b in angehakt if b.quelle == "pdf"] namen = "\n".join(f"• {b.datum.strftime('%d.%m.%Y')} {b.voll_nummer} " f"{b.nachname or 'ohne Namen'}" for b in angehakt[:15]) if len(angehakt) > 15: namen += f"\n… und {len(angehakt) - 15} weitere" hinweis = "" if aus_pdf: hinweis = (f"\n\n{len(aus_pdf)} davon stammen aus einer Rechnungs-PDF – die Dateien " "bleiben liegen und tauchen beim nächsten Einlesen wieder auf.") if not messagebox.askyesno( "Angehakte Buchungen löschen", f"{len(angehakt)} Buchung(en) aus dem Journal löschen?\n\n{namen}{hinweis}", parent=self): return for b in angehakt: self.journal.loeschen(b.id) for m in range(1, 13): self.monate[m].haken_loesen() self.aktualisieren() self.papierkorb_auffrischen() self.melde(f"{len(angehakt)} Buchung(en) gelöscht") def satz_aendern(self): fenster = tk.Toplevel(self) fenster.title("Steuersatz") fenster.resizable(False, False) fenster.transient(self) fenster.grab_set() rahmen = ttk.Frame(fenster, padding=14) rahmen.pack() ttk.Label(rahmen, text="Beherbergungssteuer in Prozent:").grid(row=0, column=0, padx=(0, 8)) eingabe = ttk.Entry(rahmen, width=8) eingabe.insert(0, f"{self.satz:g}".replace(".", ",")) eingabe.grid(row=0, column=1) ttk.Label(rahmen, text="Gilt für neu eingelesene und neue Buchungen.\n" "Bereits erfasste Zeilen behalten ihren Satz.", foreground="black").grid(row=1, column=0, columnspan=2, pady=(8, 10)) def speichern(): from modell import zahl_aus_text wert = zahl_aus_text(eingabe.get()) if wert <= 0: messagebox.showerror("Steuersatz", "Bitte einen Satz größer 0 eintragen.", parent=fenster) return self.satz = wert self.journal.setze("satz", wert) self.melde(f"Steuersatz auf {wert:g} % gesetzt") fenster.destroy() ttk.Button(rahmen, text="Speichern", command=speichern) \ .grid(row=2, column=0, columnspan=2, sticky="ew") fenster.bind("", lambda _e: fenster.destroy()) fenster.bind("", lambda _e: speichern()) eingabe.focus_set() def firma_waehlen(self): """config.json des Rechnungstools zeigen – sie liefert den Briefkopf des Berichts.""" pfad = filedialog.askopenfilename( title="config.json des Rechnungstools wählen", initialfile="config.json", filetypes=[("Einstellungsdatei", "config.json"), ("Alle Dateien", "*.*")]) if not pfad: return self.journal.setze("rechnungstool_config", os.path.abspath(pfad)) self.firma = firma_lib.lade(os.path.abspath(pfad)) name = self.firma.get("name", "") self.melde(f"Briefkopf: {name} (aus {os.path.basename(pfad)})") messagebox.showinfo("Firmendaten", f"Der Bericht trägt jetzt den Briefkopf von:\n\n{name}", parent=self) def altbestand_importieren(self): pfad = filedialog.askopenfilename( title="Alte Beherbergungssteuer-Tabelle wählen", filetypes=[("Excel-Mappe", "*.xlsx"), ("Alle Dateien", "*.*")]) if not pfad: return buchungen, fehler = xlsx_io.importiere(pfad, self.satz) if not buchungen: messagebox.showwarning("Import", "In der Tabelle wurden keine Buchungen gefunden.", parent=self) return s = summiere(buchungen) if not messagebox.askyesno( "Alte Tabelle importieren", f"{s.buchungen} Buchungen gefunden " f"({s.naechte} Übernachtungen, Steuer {eur(s.steuer)}).\n\n" "Bestehende Zeilen mit gleicher Rechnungsnummer werden überschrieben.\n" "Importieren?", parent=self): return for b in buchungen: self.journal.speichern(b) self.aktualisieren() self.melde(f"{len(buchungen)} Buchungen aus {os.path.basename(pfad)} importiert" + (f", {len(fehler)} Zeilen übersprungen" if fehler else "")) @property def _monat(self) -> int: """Der Monat des gerade offenen Reiters.""" return self.blaetter.index("current") + 1 def _bericht_bauen(self, jahr: int, monat: int = None) -> str: """Schreibt den Bericht nach berichte/ und gibt den Pfad zurück ('' = keine Buchungen).""" buchungen = self.journal.buchungen(jahr=jahr) if monat: passend = [b for b in buchungen if b.monat == monat] if not passend: messagebox.showinfo( "Bericht", f"Für {MONATE[monat - 1]} {jahr} sind keine Buchungen erfasst.", parent=self) return "" pfad = os.path.join(BERICHTE, f"Beherbergungssteuer_{jahr}_{monat:02d}_" f"{MONATE[monat - 1]}.pdf") bericht_pdf.render_monat(pfad, self.firma, jahr, monat, buchungen) else: if not buchungen: messagebox.showinfo("Bericht", f"Für {jahr} sind keine Buchungen erfasst.", parent=self) return "" pfad = os.path.join(BERICHTE, f"Beherbergungssteuer_{jahr}_Jahr.pdf") bericht_pdf.render_jahr(pfad, self.firma, jahr, buchungen) return pfad def bericht_monat(self): jahr, monat = self.jahr.get(), self._monat try: pfad = self._bericht_bauen(jahr, monat) except PermissionError: messagebox.showerror("Bericht", "Die PDF ist geöffnet und konnte nicht " "überschrieben werden. Bitte schließen.", parent=self) return if not pfad: return s = summiere(self.journal.buchungen(jahr=jahr, monat=monat)) self.melde(f"Bericht geschrieben: {pfad}") if messagebox.askyesno( "Bericht fertig", f"{MONATE[monat - 1]} {jahr}: {s.buchungen} Rechnungen, " f"{s.naechte} Übernachtungen,\nSteuer {eur(s.steuer)}\n\n{pfad}\n\n" "PDF jetzt öffnen?", parent=self): os.startfile(pfad) # noqa: S606 - Windows-Standardprogramm def bericht_jahr(self): jahr = self.jahr.get() try: pfad = self._bericht_bauen(jahr) except PermissionError: messagebox.showerror("Bericht", "Die PDF ist geöffnet und konnte nicht " "überschrieben werden. Bitte schließen.", parent=self) return if not pfad: return s = summiere(self.journal.buchungen(jahr=jahr)) self.melde(f"Jahresbericht geschrieben: {pfad}") if messagebox.askyesno( "Bericht fertig", f"Jahr {jahr}: {s.buchungen} Rechnungen, {s.naechte} Übernachtungen,\n" f"Steuer {eur(s.steuer)}\n\n{pfad}\n\nPDF jetzt öffnen?", parent=self): os.startfile(pfad) # noqa: S606 def _beenden(self): self.journal.setze("jahr", self.jahr.get()) self.journal.schliessen() self.destroy() if __name__ == "__main__": log.info("Beherbergungssteuer startet (Python %s)", sys.version.split()[0]) # Die Sperre selbst greift schon ganz oben, vor dem Datenordner. eigene_pid_merken() App().mainloop()