Steuerrechnungstool/gui_monat.py
TheMockTv 9d7aa1eec7 Darstellung: das Programm hatte ueberhaupt kein Theme
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
2026-09-07 00:34:38 +02:00

214 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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))