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
142
gui_dialoge.py
Normal file
142
gui_dialoge.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue