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
187
gui_monat.py
Normal file
187
gui_monat.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ein Monatsblatt der Oberfläche: Tabelle der Buchungen + Summenzeile darunter.
|
||||
Entspricht einem Monatsblatt der alten Excel-Mappe.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, font as tkfont
|
||||
|
||||
from modell import summiere, eur, eur_kurz
|
||||
|
||||
HAKEN_AN = "☑"
|
||||
HAKEN_AUS = "☐"
|
||||
|
||||
SPALTEN = [
|
||||
("haken", HAKEN_AUS, 34, "center"), # zum Anhaken fürs Löschen
|
||||
("datum", "Datum", 90, "center"),
|
||||
("nummer", "Rechnung", 95, "center"),
|
||||
("nachname", "Nachname", 200, "w"),
|
||||
("naechte", "Nächte", 65, "center"),
|
||||
("gezahlt", "Gezahlt", 105, "e"),
|
||||
("satz", "Satz %", 60, "center"),
|
||||
("steuer", "Steuer", 105, "e"),
|
||||
("quelle", "Quelle", 75, "center"),
|
||||
]
|
||||
|
||||
QUELLE_TEXT = {"pdf": "PDF", "xlsx": "Excel", "manuell": "manuell"}
|
||||
|
||||
BG = "#eef1ee" # heller Streifen wie im PDF-Bericht
|
||||
RAHMEN = "#c3ccc5"
|
||||
GRUEN = "#1f5c3d" # Campinghof-Grün für den Steuerbetrag
|
||||
|
||||
|
||||
class SummenLeiste(tk.Frame):
|
||||
"""Kennzahlen als abgesetzte Felder statt einer langen Textzeile."""
|
||||
|
||||
FELDER = [
|
||||
("rechnungen", "Rechnungen", False),
|
||||
("naechte", "Übernachtungen", False),
|
||||
("gezahlt", "Gezahlt", False),
|
||||
("steuer", "Beherbergungssteuer", True), # hervorgehoben
|
||||
]
|
||||
# Die Jahresleiste zeigt zusätzlich, was im Schnitt pro Monat hereinkam:
|
||||
# einmal der Rechnungsbetrag (bleibt beim Hof) und einmal das, was der Gast
|
||||
# insgesamt gezahlt hat (inkl. der Beherbergungssteuer, die ans Amt durchläuft).
|
||||
FELDER_JAHR = FELDER + [
|
||||
("schnitt_ohne", "Ø je Monat (ohne Steuer)", False),
|
||||
("schnitt_mit", "Ø je Monat (mit Steuer)", False),
|
||||
]
|
||||
|
||||
def __init__(self, master, mit_schnitt: bool = False):
|
||||
super().__init__(master, bg=BG, highlightbackground=RAHMEN, highlightthickness=1)
|
||||
self.werte = {}
|
||||
self.felder = self.FELDER_JAHR if mit_schnitt else self.FELDER
|
||||
|
||||
for spalte, (schluessel, titel, betont) in enumerate(self.felder):
|
||||
if spalte:
|
||||
tk.Frame(self, bg=RAHMEN, width=1).grid(row=0, column=spalte * 2 - 1,
|
||||
sticky="ns", pady=8)
|
||||
zelle = tk.Frame(self, bg=BG)
|
||||
zelle.grid(row=0, column=spalte * 2, sticky="ew", padx=16, pady=8)
|
||||
|
||||
tk.Label(zelle, text=titel.upper(), bg=BG, fg="#4a544c",
|
||||
font=("Segoe UI", 7, "bold")).pack(anchor="w")
|
||||
wert = tk.Label(zelle, text="–", bg=BG,
|
||||
fg=GRUEN if betont else "black",
|
||||
font=("Segoe UI", 13 if betont else 12,
|
||||
"bold" if betont else "normal"))
|
||||
wert.pack(anchor="w")
|
||||
self.werte[schluessel] = wert
|
||||
self.columnconfigure(spalte * 2, weight=1)
|
||||
|
||||
def setzen(self, s, buchungen=None):
|
||||
self.werte["rechnungen"].config(text=str(s.buchungen))
|
||||
self.werte["naechte"].config(text=str(s.naechte))
|
||||
self.werte["gezahlt"].config(text=eur(s.gezahlt))
|
||||
self.werte["steuer"].config(text=eur(s.steuer))
|
||||
if "schnitt_ohne" in self.werte:
|
||||
monate = len({b.monat for b in (buchungen or [])}) # nur Monate mit Buchungen
|
||||
self.werte["schnitt_ohne"].config(
|
||||
text=eur(s.gezahlt / monate) if monate else "–")
|
||||
self.werte["schnitt_mit"].config(
|
||||
text=eur(s.gesamt / monate) if monate else "–")
|
||||
|
||||
|
||||
class MonatsTab(ttk.Frame):
|
||||
"""Tabelle eines Monats. `bei_auswahl` wird mit der markierten Buchung gerufen."""
|
||||
|
||||
def __init__(self, master, monat: int, bei_doppelklick=None, bei_haken=None):
|
||||
super().__init__(master, padding=(8, 8, 8, 6))
|
||||
self.monat = monat
|
||||
self.bei_doppelklick = bei_doppelklick
|
||||
self.bei_haken = bei_haken # meldet dem Fenster, dass sich die Auswahl änderte
|
||||
self._buchungen = []
|
||||
self._angehakt = set() # ids der angehakten Buchungen
|
||||
|
||||
self.baum = ttk.Treeview(self, columns=[s[0] for s in SPALTEN],
|
||||
show="headings", selectmode="browse")
|
||||
for schluessel, titel, breite, anker in SPALTEN:
|
||||
self.baum.heading(schluessel, text=titel)
|
||||
self.baum.column(schluessel, width=breite, anchor=anker, stretch=False)
|
||||
|
||||
rollbalken = ttk.Scrollbar(self, orient="vertical", command=self.baum.yview)
|
||||
self.baum.configure(yscrollcommand=rollbalken.set)
|
||||
self.baum.grid(row=0, column=0, sticky="nsew")
|
||||
rollbalken.grid(row=0, column=1, sticky="ns")
|
||||
|
||||
# Woher eine Buchung stammt, steht in der Spalte "Quelle" – nicht in der Schriftfarbe
|
||||
for tag in ("pdf", "xlsx", "manuell"):
|
||||
self.baum.tag_configure(tag, foreground="black")
|
||||
|
||||
self.summen = SummenLeiste(self)
|
||||
self.summen.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(8, 0))
|
||||
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
|
||||
self.baum.bind("<Button-1>", self._klick)
|
||||
if bei_doppelklick:
|
||||
self.baum.bind("<Double-1>", lambda _e: self._doppelklick())
|
||||
|
||||
# ---- Anhaken -----------------------------------------------------------
|
||||
def _klick(self, ereignis):
|
||||
"""Klick in die Haken-Spalte schaltet die Zeile an/aus."""
|
||||
if self.baum.identify_region(ereignis.x, ereignis.y) != "cell":
|
||||
return
|
||||
if self.baum.identify_column(ereignis.x) != "#1":
|
||||
return
|
||||
zeile = self.baum.identify_row(ereignis.y)
|
||||
if not zeile:
|
||||
return
|
||||
b = self._buchungen[int(zeile)]
|
||||
if b.id in self._angehakt:
|
||||
self._angehakt.discard(b.id)
|
||||
else:
|
||||
self._angehakt.add(b.id)
|
||||
self.baum.set(zeile, "haken", HAKEN_AN if b.id in self._angehakt else HAKEN_AUS)
|
||||
if self.bei_haken:
|
||||
self.bei_haken()
|
||||
return "break" # kein Zeilen-Markieren durch den Haken-Klick
|
||||
|
||||
def angehakte(self):
|
||||
return [b for b in self._buchungen if b.id in self._angehakt]
|
||||
|
||||
def haken_loesen(self):
|
||||
self._angehakt.clear()
|
||||
|
||||
def _doppelklick(self):
|
||||
b = self.markierte()
|
||||
if b:
|
||||
self.bei_doppelklick(b)
|
||||
|
||||
def markierte(self):
|
||||
"""Aktuell markierte Buchung oder None."""
|
||||
sel = self.baum.selection()
|
||||
if not sel:
|
||||
return None
|
||||
idx = int(sel[0])
|
||||
return self._buchungen[idx] if 0 <= idx < len(self._buchungen) else None
|
||||
|
||||
def _namensspalte_anpassen(self):
|
||||
"""Namensspalte nur so breit wie der breiteste Name."""
|
||||
schrift = tkfont.nametofont("TkDefaultFont")
|
||||
breit = max([schrift.measure(b.nachname) for b in self._buchungen]
|
||||
+ [schrift.measure("Nachname")])
|
||||
self.baum.column("nachname", width=breit + 24, minwidth=90)
|
||||
|
||||
def fuellen(self, buchungen):
|
||||
self._buchungen = sorted(buchungen, key=lambda b: (b.datum, b.rechnungsnummer))
|
||||
# Haken nur für Zeilen behalten, die es noch gibt
|
||||
self._angehakt &= {b.id for b in self._buchungen}
|
||||
self._namensspalte_anpassen()
|
||||
self.baum.delete(*self.baum.get_children())
|
||||
for i, b in enumerate(self._buchungen):
|
||||
self.baum.insert("", "end", iid=str(i), tags=(b.quelle,), values=(
|
||||
HAKEN_AN if b.id in self._angehakt else HAKEN_AUS,
|
||||
b.datum.strftime("%d.%m.%Y"),
|
||||
b.voll_nummer,
|
||||
b.nachname,
|
||||
b.naechte,
|
||||
eur_kurz(b.gezahlt),
|
||||
eur_kurz(b.satz).replace(",00", ""),
|
||||
eur_kurz(b.steuer),
|
||||
QUELLE_TEXT.get(b.quelle, b.quelle),
|
||||
))
|
||||
|
||||
self.summen.setzen(summiere(self._buchungen))
|
||||
Loading…
Add table
Add a link
Reference in a new issue