Zwei Ansagen von ihm, beide am selben Blatt:
1. "weil die nummer hast nur auf der storno selber aber nicht am
stornierten nummer um die es geht" - die aufgehobene Rechnung stand
nur als "storniert" da. Jetzt steht dort "storniert mit 2026-901".
Damit ist das Paar in BEIDE Richtungen lesbar, und das zaehlt vor
allem dann, wenn der Storno in einem anderen Monat liegt und auf
diesem Blatt gar nicht auftaucht.
2. "mach eine neue spalte mit grund oder so was" - der Vermerk stand als
zweite Zeile unter der Rechnungsnummer. Jetzt ist es eine eigene
Spalte "Grund" zwischen Rechnung und Name, eine Zeile je Buchung.
Feste Breiten neu verteilt (20/20/34 mm + Rest fuer den Namen), damit
nichts umbricht.
Dafuer neu: modell.aufhebungen() gibt {id der Rechnung: Storno-Buchung}
statt nur der ids - die Anzeige will auch sagen, WOMIT storniert wurde,
nicht nur DASS. aufgehobene() bleibt als duenne Huelle darueber, damit
zeig_stornos und doppelte_nummern unveraendert weiterlaufen.
Fenster und Blatt zeigen wieder dieselben Worte.
pruef_storno_verrechnet prueft beide Richtungen mit, auch ueber die
Monatsgrenze ("storniert mit 2026-911" im August, der Storno selbst steht
im September). Angesehen: Tabelle als Bild gerendert, alles einzeilig,
auch mit langem Namen. doppelte und berichtigung gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
177 lines
7.7 KiB
Python
177 lines
7.7 KiB
Python
# -*- 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"}
|
||
|
||
BG = "#eef1ee" # heller Streifen wie im PDF-Bericht
|
||
RAHMEN = "#c3ccc5"
|
||
GRUEN = "#1f5c3d" # Campinghof-Grün für den Steuerbetrag
|
||
WARNUNG = "#a00000" # doppelt vergebene Rechnungsnummer
|
||
|
||
|
||
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 - 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
|
||
for tag in ("pdf", "xlsx", "manuell"):
|
||
self.baum.tag_configure(tag, foreground="black")
|
||
# Rot ist einzig für den Fehlerfall reserviert: die Nummer gibt es zweimal.
|
||
self.baum.tag_configure("doppelt", foreground=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:
|
||
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))
|