Sein Fund: "an der storno verweist du auf die alte rechnung, muss aber
lauten neu zu und neue rechnungs nummer. heisst kette ist so: rechnung
storniert weist auf die storno nummer, storno verweist auf neue rechnung.
wer soll denn sonst wissen wo er suchen muss."
Er hat recht: eine Zeile, die nur zurueckzeigt, laesst den Leser stehen.
Jetzt sagt jede Zeile, wo es WEITERGEHT:
Rechnung -> "storniert mit 2026-901"
Storno -> "neue Rechnung 2026-902" (Fall A)
-> "neu abgerechnet mit 2026-343" (Fall B, Doppelung)
-> "Storno zu 2026-900" (Fall C / alte Belege ohne Angabe)
neue R. -> "Neuausstellung"
Dafuer wandert die Folgenummer aus den PDF-Kenndaten bis ins Journal:
Buchung.folge_nummer + folge_art (neu | vorhanden | ""), gelesen aus
korrektur_nummer/ersatz_vorhanden/nur_storno, zwei neue DB-Spalten, alte
Journale bauen sich wie gehabt selbst um.
An seinen ECHTEN Belegen geprueft: Storno 2026-455 -> folge 2026-369
(vorhanden), 2026-456 -> 2026-343 (vorhanden). Die Angabe steht in den
Kenndaten schon drin, sie wurde bisher nur nicht gelesen.
Fenster und Blatt zeigen dieselben Worte. Erklaerungs-PDF um eine
Kettentabelle ergaenzt. Pruefkette gruen (storno_verrechnet inkl.
September-Blatt, doppelte_leistung, nur_storno).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
183 lines
8 KiB
Python
183 lines
8 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:
|
||
# Kette vorwaerts: Rechnung -> Storno -> die Rechnung, die gilt.
|
||
if b.folge_nummer and b.folge_art == "vorhanden":
|
||
art_text = f"neu 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))
|