Beherbergungssteuer: Steuerjournal aus den Rechnungs-PDFs
Ersetzt die Excel-Mappe des Campinghofs. Die Rechnungs-PDFs des Rechnungstools sind die Quelle; SQLite ist nur Cache, Excel nur noch Import des Altbestands. Bericht fuers Amt als PDF im Briefkopf der Rechnungen (Firmendaten aus der config.json des Rechnungstools). - pdf_parser: liest Datum, Nummer, Name, Naechte, Zwischensumme, Satz - db: Journal + Scan mit Cache (Pfad/mtime/Groesse) - xlsx_io: Import der alten Tabelle, vereinheitlicht Rechnungsnummern - bericht_pdf: Monats- und Jahresbericht (reportlab) - GUI: Monatsreiter, Suche, Anhaken + Papierkorb, Kennzahlen-Leiste Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
80967de26f
12 changed files with 1908 additions and 0 deletions
454
app.py
Normal file
454
app.py
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
# -*- 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
|
||||
|
||||
import db
|
||||
import xlsx_io
|
||||
import firma as firma_lib
|
||||
import bericht_pdf
|
||||
from modell import MONATE, STANDARD_SATZ, summiere, eur
|
||||
from gui_monat import MonatsTab, SummenLeiste
|
||||
from gui_dialoge import BuchungDialog
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(sys.argv[0] if getattr(sys, "frozen", False) else __file__))
|
||||
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("<F5>", lambda _e: self.einlesen())
|
||||
self.bind("<Control-f>", lambda _e: self.suchfeld.focus_set())
|
||||
self.bind("<Delete>", 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="Buchung von Hand anlegen…", command=self.buchung_anlegen)
|
||||
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)
|
||||
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("<<ComboboxSelected>>", lambda _e: self.jahr_gewechselt())
|
||||
|
||||
ttk.Button(leiste, text="Rechnungen einlesen", command=self.einlesen).pack(side="left")
|
||||
ttk.Button(leiste, text="Neue Buchung", command=self.buchung_anlegen).pack(side="left", padx=6)
|
||||
ttk.Button(leiste, text="PDF fürs Amt", command=self.bericht_monat).pack(side="left")
|
||||
|
||||
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("<Escape>", 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))
|
||||
|
||||
fuss = ttk.Frame(rahmen)
|
||||
fuss.pack(fill="x")
|
||||
self.status = ttk.Label(fuss, text="", foreground="black")
|
||||
self.status.pack(side="left")
|
||||
self.ordner_anzeige = ttk.Label(fuss, text="", foreground="black")
|
||||
self.ordner_anzeige.pack(side="right")
|
||||
|
||||
# ----------------------------------------------------------- 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
|
||||
|
||||
for m in range(1, 13):
|
||||
self.monate[m].fuellen([b for b in sichtbar if b.monat == m])
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def buchung_anlegen(self):
|
||||
d = BuchungDialog(self, jahr=self.jahr.get(), satz=self.satz)
|
||||
self.wait_window(d)
|
||||
if d.ergebnis:
|
||||
self.journal.speichern(d.ergebnis)
|
||||
self.jahr.set(d.ergebnis.jahr)
|
||||
self.aktualisieren()
|
||||
self.blaetter.select(d.ergebnis.monat - 1)
|
||||
self.melde(f"Buchung {d.ergebnis.voll_nummer} gespeichert")
|
||||
|
||||
def buchung_bearbeiten(self, buchung):
|
||||
d = BuchungDialog(self, jahr=buchung.jahr, satz=self.satz, buchung=buchung)
|
||||
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("<Escape>", lambda _e: fenster.destroy())
|
||||
fenster.bind("<Return>", lambda _e: speichern())
|
||||
eingabe.focus_set()
|
||||
|
||||
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__":
|
||||
App().mainloop()
|
||||
Loading…
Add table
Add a link
Reference in a new issue