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:
TheMockTv 2026-07-12 19:15:44 +02:00
commit 80967de26f
12 changed files with 1908 additions and 0 deletions

142
gui_dialoge.py Normal file
View file

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
"""
Dialog für eine Buchung von Hand (z.B. Barzahlung ohne PDF-Rechnung)
und zum Nachbessern einer eingelesenen Zeile.
Schließen mit ESC, Speichern mit Enter oder Knopf.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime, date
from modell import (Buchung, STANDARD_SATZ, steuer_betrag, eur, zahl_aus_text,
zerlege_nummer)
class BuchungDialog(tk.Toplevel):
"""
Modal. Nach dem Schließen steht das Ergebnis in
.ergebnis die gespeicherte Buchung (oder None, wenn abgebrochen)
.loeschen True, wenn die Buchung gelöscht werden soll
"""
def __init__(self, master, jahr: int, satz: float = STANDARD_SATZ, buchung: Buchung = None):
super().__init__(master)
self.ergebnis = None
self.loeschen = False
self._vorlage = buchung
self._jahr = buchung.jahr if buchung else jahr
self.title("Buchung bearbeiten" if buchung else "Neue Buchung")
self.resizable(False, False)
self.transient(master)
self.grab_set()
rahmen = ttk.Frame(self, padding=14)
rahmen.grid(sticky="nsew")
self.felder = {}
zeilen = [
("datum", "Rechnungsdatum", (buchung.datum.strftime("%d.%m.%Y") if buchung
else date.today().strftime("%d.%m.%Y"))),
("nummer", "Rechnungsnummer", buchung.rechnungsnummer if buchung else ""),
("nachname", "Nachname", buchung.nachname if buchung else ""),
("naechte", "Übernachtungen", str(buchung.naechte) if buchung else "1"),
("gezahlt", "Gezahlt (Zwischensumme)", f"{buchung.gezahlt:.2f}".replace(".", ",") if buchung else ""),
("satz", "Steuersatz %", f"{(buchung.satz if buchung else satz):g}".replace(".", ",")),
]
for i, (schluessel, beschriftung, wert) in enumerate(zeilen):
ttk.Label(rahmen, text=beschriftung + ":").grid(row=i, column=0, sticky="w", pady=3, padx=(0, 10))
e = ttk.Entry(rahmen, width=26)
e.insert(0, wert)
e.grid(row=i, column=1, sticky="ew", pady=3)
e.bind("<KeyRelease>", lambda _e: self._vorschau())
self.felder[schluessel] = e
self.vorschau = ttk.Label(rahmen, text="", foreground="black",
font=("Segoe UI", 10, "bold"), anchor="e")
self.vorschau.grid(row=len(zeilen), column=0, columnspan=2, sticky="ew", pady=(10, 4))
knoepfe = ttk.Frame(rahmen)
knoepfe.grid(row=len(zeilen) + 1, column=0, columnspan=2, sticky="ew", pady=(6, 0))
ttk.Button(knoepfe, text="Speichern", command=self._speichern) \
.pack(side="left", fill="x", expand=True)
if buchung: # nur beim Bearbeiten eine neue Buchung gibt es noch nicht zu löschen
ttk.Button(knoepfe, text="Löschen", command=self._loeschen) \
.pack(side="left", fill="x", expand=True, padx=(8, 0))
rahmen.columnconfigure(1, weight=1)
self.bind("<Escape>", lambda _e: self.destroy())
self.bind("<Return>", lambda _e: self._speichern())
self.felder["datum" if buchung else "nummer"].focus_set()
self._vorschau()
self.update_idletasks()
x = master.winfo_rootx() + (master.winfo_width() - self.winfo_width()) // 2
y = master.winfo_rooty() + 120
self.geometry(f"+{max(0, x)}+{max(0, y)}")
# ---- Eingaben lesen ----------------------------------------------------
def _werte(self):
gezahlt = zahl_aus_text(self.felder["gezahlt"].get())
satz = zahl_aus_text(self.felder["satz"].get())
return gezahlt, satz
def _vorschau(self):
gezahlt, satz = self._werte()
self.vorschau.config(text=f"Beherbergungssteuer: {eur(steuer_betrag(gezahlt, satz))}")
def _loeschen(self):
b = self._vorlage
hinweis = ("\n\nDie Rechnungs-PDF bleibt liegen beim nächsten Einlesen "
"taucht die Buchung wieder auf.") if b.quelle == "pdf" else ""
if not messagebox.askyesno(
"Buchung löschen",
f"Rechnung {b.voll_nummer} ({b.nachname or 'ohne Namen'}) "
f"aus dem Journal löschen?{hinweis}", parent=self):
return
self.loeschen = True
self.destroy()
def _speichern(self):
text_datum = self.felder["datum"].get().strip()
datum = None
for fmt in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d"):
try:
datum = datetime.strptime(text_datum, fmt).date()
break
except ValueError:
continue
if not datum:
messagebox.showerror("Datum", f"Datum nicht lesbar: {text_datum}\n"
"Erwartet wird z.B. 12.07.2026", parent=self)
return
# "238" und "2026-238" werden beide angenommen
jahr, nummer = zerlege_nummer(self.felder["nummer"].get(), datum.year)
if not nummer:
messagebox.showerror("Rechnungsnummer", "Bitte eine Rechnungsnummer eintragen "
"(z.B. 238).", parent=self)
return
gezahlt, satz = self._werte()
if gezahlt <= 0:
messagebox.showerror("Gezahlt", "Bitte den Rechnungsbetrag (Zwischensumme) "
"eintragen.", parent=self)
return
self.ergebnis = Buchung(
datum=datum,
rechnungsnummer=nummer,
jahr=jahr,
nachname=self.felder["nachname"].get().strip(),
naechte=int(zahl_aus_text(self.felder["naechte"].get()) or 0),
gezahlt=gezahlt,
satz=satz,
quelle=self._vorlage.quelle if self._vorlage else "manuell",
pdf_pfad=self._vorlage.pdf_pfad if self._vorlage else "",
id=self._vorlage.id if self._vorlage else 0,
)
self.destroy()