Steuerrechnungstool/gui_monat.py
TheMockTv 13fe1d97f9 Storno wird wieder VERRECHNET statt weggelassen - seine Entscheidung
Heute Nacht war es andersherum gebaut (v1.6.3): Storno und aufgehobene
Rechnung zaehlten gar nicht mehr mit, die Berichtigung landete im Monat
der Rechnung. Sauber nach § 7 Abs. 5 der Satzung (angemeldet wird die im
Kalendermonat VEREINNAHMTE Steuer), aber nicht das, was er will.

Sein Wort:

    "ich wuerde das wie beim alten lassen, nicht das er der Stadt zu
     wenig gibt"
    "ob er im Sep dann weniger gemacht hat ist doch egal, weil im August
     hat er ja mehr - also gleicht es sich aus, es ist nur ein
     Time-Problem. Aber um was es mir geht: die Steuern sind sauber."

Und er hat recht: der Unterschied ist NUR der Monat, nie das Jahr, und
die Richtung stimmt - zu viel zuerst ist beim Amt nie ein Problem, zu
wenig schon.

Also zurueckgebaut:
- modell.zaehlbar() ist wieder raus, summiere() summiert alle Zeilen mit
  ihrem Vorzeichen. Die Begruendung samt Satzungs-Fundstelle steht im
  Docstring, damit es niemand "repariert" - in EINEM Monat sieht eine
  verrechnete Doppelung naemlich nach einem Fehler aus.
- Amtsbericht zeigt Rechnung und Storno wieder beide, jede in ihrem Monat
  und mit ihrem Vorzeichen. Damit erklaert sich die Meldung von selbst.
- aufgehobene() bleibt, wird aber nur noch fuer die Spalte "Art" und fuer
  die Dubletten-Meldung gebraucht, nicht mehr fuers Rechnen.

Nicht zurueckgebaut (das war eine eigene Ansage): Storno und aufgehobene
Rechnung werden NICHT mehr ausgegraut, sie stehen normal da und sind ueber
die Spalte "Art" gekennzeichnet.

pruef_storno_rechnet_nicht.py ist durch pruef_storno_verrechnet.py
ersetzt: derselbe Aufbau (Fake-Rechnungen in einer echten SQLite, auch
der Fall ueber die Monatsgrenze), aber es haelt jetzt das gewollte
Verhalten fest - August 9 Naechte/117,00, September -4/-52,00, Jahr
5/65,00. Restliche Kette (storno_journal, doppelte, berichtigung,
einzelinstanz, wache) gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
2026-09-04 01:09:08 +02:00

175 lines
7.5 KiB
Python
Raw 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"}
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)
aufgehoben = set(aufgehoben)
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 = "storniert"
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))