# -*- 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 SPALTEN = [ ("datum", "Datum", 90, "center"), ("nummer", "Rechnung", 95, "center"), ("art", "Art", 132, "center"), # Storno zu ... / storniert / Neuausstellung ("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"} # Rueckfallfarben. Sie gelten nur, wenn das Fenster (noch) keine Farben # traegt - etwa in einem Pruefstand, der ein Widget einzeln baut. Im laufenden # Programm kommen sie aus theme.py ueber das Hauptfenster; siehe farben(). BG = "#eef1ee" # heller Streifen wie im PDF-Bericht RAHMEN = "#c3ccc5" GRUEN = "#1f5c3d" # Campinghof-Grün für den Steuerbetrag WARNUNG = "#a00000" # doppelt vergebene Rechnungsnummer def farben(widget): """Die Farben des Fensters, zu dem dieses Widget gehoert. Diese Datei zeichnet mit tk-Widgets (nicht ttk), und die tragen ihre Farbe fest. Ohne diese Abfrage blieb die ganze Summenleiste hell, waehrend das uebrige Fenster dunkel war - ein weisser Block unten im Bild (07.09.2026 aufgefallen). getattr mit Rueckfall statt einer harten Abhaengigkeit: so laesst sich ein Widget aus dieser Datei auch ohne die App bauen. """ top = widget.winfo_toplevel() return { "bg": getattr(top, "col_card", BG), "rahmen": getattr(top, "col_line", RAHMEN), "betont": getattr(top, "col_accent", GRUEN), "text": getattr(top, "col_fg", "black"), "leise": getattr(top, "col_muted", "#4a544c"), "warnung": getattr(top, "col_gefahr", WARNUNG), } 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): f = farben(master) super().__init__(master, bg=f["bg"], highlightbackground=f["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=f["rahmen"], width=1).grid( row=0, column=spalte * 2 - 1, sticky="ns", pady=8) zelle = tk.Frame(self, bg=f["bg"]) zelle.grid(row=0, column=spalte * 2, sticky="ew", padx=16, pady=8) tk.Label(zelle, text=titel.upper(), bg=f["bg"], fg=f["leise"], font=("Segoe UI", 7, "bold")).pack(anchor="w") wert = tk.Label(zelle, text="–", bg=f["bg"], fg=f["betont"] if betont else f["text"], 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 - reine Anzeige. Hier wird nichts angehakt und nichts geloescht: die Zahlen kommen aus den Rechnungen, nicht aus diesem Fenster. """ def __init__(self, master, monat: int): super().__init__(master, padding=(8, 8, 8, 6)) self.monat = monat self._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. Die normale Schriftfarbe kommt aus dem Fenster: fest # "black" wuerde in der dunklen Darstellung schwarz auf dunkelgrau # ergeben - also unlesbar. f = farben(self) for tag in ("pdf", "xlsx", "manuell"): self.baum.tag_configure(tag, foreground=f["text"]) # Rot ist einzig für den Fehlerfall reserviert: die Nummer gibt es zweimal. self.baum.tag_configure("doppelt", foreground=f["warnung"]) # Storno und die von ihm aufgehobene Rechnung stehen NICHT grau da # (Ansage des Auftraggebers 04.09.2026: "ausgrauen wuerde ich es nicht, # ich wuerde es nur als Storno taggen"). Der Hinweis steht in der Spalte # "Art" - Schriftfarbe bleibt schwarz wie bei jeder anderen Zeile. 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) 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, doppelte=(), aufgehoben=()): """Zeilen des Monats. `doppelte` sind die doppelt vergebenen Nummern des ganzen JAHRES (aus modell.doppelte_nummern) - eine Nummer, deren Zwilling in einem anderen Monat liegt, gehört hier trotzdem rot markiert.""" doppelte = set(doppelte) # dict {id: Storno-Buchung}; "b.id in aufgehoben" arbeitet damit genauso, # aber die Zeile kann jetzt auch die Storno-Nummer nennen. aufgehoben = dict(aufgehoben or {}) self._buchungen = sorted(buchungen, key=lambda b: (b.datum, b.rechnungsnummer)) self._namensspalte_anpassen() self.baum.delete(*self.baum.get_children()) for i, b in enumerate(self._buchungen): weg = b.id in aufgehoben or bool(b.storno_zu) zweimal = b.voll_nummer in doppelte and not weg if zweimal: marke = ("doppelt",) else: marke = (b.quelle,) if b.storno_zu: # Kette vorwaerts: Rechnung -> Storno -> die Rechnung, die gilt. if b.folge_nummer and b.folge_art == "vorhanden": art_text = f"abgerechnet mit {b.folge_nummer}" elif b.folge_nummer: art_text = f"neue Rechnung {b.folge_nummer}" else: art_text = f"Storno zu {b.storno_zu}" elif b.id in aufgehoben: art_text = f"storniert mit {aufgehoben[b.id].voll_nummer}" elif b.art == "korrektur": art_text = "Neuausstellung" else: art_text = "" self.baum.insert("", "end", iid=str(i), tags=marke, values=( b.datum.strftime("%d.%m.%Y"), ("⚠ " + b.voll_nummer) if zweimal else b.voll_nummer, art_text, 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), )) # Verrechnet, nicht weggelassen: der Storno steht mit seinem Minus in # SEINEM Monat, die aufgehobene Rechnung mit ihrem Plus in ihrem. self.summen.setzen(summiere(self._buchungen))