Sein Befund "und beim steuertool fehlt das theam in genze oder wird nicht durch die automatik gesetzt" "nur die einstellunden hilfe leiste die ist noch hell und der icon und titel leiste auch" Er hatte recht, und zwar wortwoertlich: dieses Programm lief im Tk-Standard, mit dreimal fest eingetragenem foreground="black". Es gab hier gar keine theme.py. Gebaut theme.py aus dem Rechnungstool uebernommen und dabei von config.py geloest - eine Datei, die in BEIDEN Programmen liegt, darf nichts aus einem der beiden importieren. Beim ersten Uebernehmen flog sie genau daran beim Import auseinander. Die Einstellungen liegen hier in der Journal-Tabelle, nicht in einer config.json. _einstellungen() ist die Bruecke: etwas mit .get() und []=, wie hinweis.py und theme.py es erwarten. Menueleiste aus menueleiste.py statt tk.Menu - ein natives Menue laesst sich unter Windows nicht einfaerben, das war der letzte helle Rest. Treeview-Stil in theme.py ergaenzt (ttk zeichnet Listen nicht ueber den Grundstil), Reiter schmaler, damit alle zwoelf Monatsnamen lesbar bleiben. Die Summenleisten in gui_monat.py holen ihre Farben jetzt vom Fenster statt sie fest zu tragen - sie waren ein weisser Block unten im dunklen Bild. Logo und Fenstersymbol werden fuer die dunkle Darstellung aufgehellt. ⚠️ Altfehler behoben: die Meldung des Rechnungstools stand nie pruef_wache lief mal gruen und mal rot - je nachdem, wann er hinsah. Das war kein Wackler im Pruefstand, sondern sein Symptom: meldet das Rechnungstool eine neue Rechnung, laeuft sofort das Einlesen an, und dessen Ergebnis landete in derselben Zeile. Der Benutzer sah NIE, dass etwas hereingekommen war. melde() kennt jetzt einen Vorrang in Sekunden; die Meldung des Rechnungstools bekommt sechs. Dreimal hintereinander stabil gruen. Ein wackelnder Pruefstand ist keine Nebensache, die man wegdrueckt. AUFGABE_STEUERSAETZE_DYNAMISCH.md Seine naechste Ansage notiert: die festen Saetze sollen raus, dynamisch per Tabelle wie im Rechnungstool. Mit dem, worauf dabei zu achten ist - ein Satz gilt ab einem DATUM, und die Saetze bereits erfasster Buchungen bleiben stehen (§ 147 AO). Fassung 1.9.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
214 lines
9.3 KiB
Python
214 lines
9.3 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"}
|
||
|
||
# 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))
|