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
175 lines
7.5 KiB
Python
175 lines
7.5 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)
|
||
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))
|