# -*- 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("", self._klick) if bei_doppelklick: self.baum.bind("", 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))