diff --git a/Beherbergungssteuer.exe b/Beherbergungssteuer.exe
index 18b25bd..bd5b6b4 100644
Binary files a/Beherbergungssteuer.exe and b/Beherbergungssteuer.exe differ
diff --git a/app.py b/app.py
index 91c0623..97b1df1 100644
--- a/app.py
+++ b/app.py
@@ -52,7 +52,7 @@ import firma as firma_lib
import bericht_pdf
import ablage
from modell import (MONATE, STANDARD_SATZ, summiere, eur, eur_kurz,
- aufgehobene, doppelte_nummern)
+ aufhebungen, doppelte_nummern)
from gui_monat import MonatsTab, SummenLeiste, WARNUNG
# Als EXE liegt alles in einem eigenen Datenordner - siehe ablage.py.
@@ -235,7 +235,9 @@ class App(tk.Tk):
# Was ein Storno aufgehoben hat, steht weiter da - in normaler Schrift,
# gekennzeichnet nur ueber die Spalte "Art", und es zaehlt nicht mehr
# als offene Rechnung.
- self._aufgehoben = aufgehobene(alle)
+ # dict {id der Rechnung: Storno-Buchung} - die Anzeige will auch sagen,
+ # WOMIT storniert wurde, nicht nur DASS.
+ self._aufgehoben = aufhebungen(alle)
if self._doppelte:
# Gehoert ins Log: der Amtsbericht wird aus diesen Zeilen gebaut, und
# spaeter soll nachvollziehbar sein, wann der Fall zuerst dastand.
diff --git a/bericht_pdf.py b/bericht_pdf.py
index 2ebf3c8..e030792 100644
--- a/bericht_pdf.py
+++ b/bericht_pdf.py
@@ -23,7 +23,7 @@ from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
-from modell import MONATE, summiere, aufgehobene, eur, eur_kurz
+from modell import MONATE, summiere, aufhebungen, eur, eur_kurz
log = logging.getLogger("bst.bericht")
@@ -209,7 +209,11 @@ def _art_text(b, weg) -> str:
if b.storno_zu:
return f"Storno zu {b.storno_zu}"
if b.id in weg:
- return "storniert"
+ # Nicht nur "storniert": auch WOMIT. Sonst steht auf dem Blatt ein
+ # Vermerk ohne Gegenstueck und man muss die Zeile suchen - erst recht,
+ # wenn der Storno in einem anderen Monat liegt und gar nicht auf
+ # demselben Blatt steht. Sein Hinweis vom 04.09.2026.
+ return f"storniert mit {weg[b.id].voll_nummer}"
if b.art == "korrektur":
return "Neuausstellung"
return ""
@@ -219,7 +223,7 @@ def render_monat(pfad, firma, jahr: int, monat: int, buchungen):
st = _styles()
# Ueber ALLE uebergebenen Buchungen bestimmt (das ganze Jahr): der Storno
# kann in einem anderen Monat liegen als die Rechnung, die er aufhebt.
- weg = aufgehobene(buchungen)
+ weg = aufhebungen(buchungen)
# Storno und aufgehobene Rechnung stehen BEIDE im Bericht, jede in ihrem
# Monat und mit ihrem Vorzeichen - so ist die Meldung aus sich heraus
# nachvollziehbar (Entscheidung des Auftraggebers, siehe modell.summiere).
@@ -233,15 +237,16 @@ def render_monat(pfad, firma, jahr: int, monat: int, buchungen):
_titel(story, st, firma, "Beherbergungssteuer – Steueranmeldung",
f"Zeitraum: {MONATE[monat - 1]} {jahr}")
- kopf = ["Datum", "Rechnung", "Name", "Nächte", "Entgelt", "Satz %", "Steuer"]
- stile = ["th", "th", "th_l", "th", "th", "th", "th"]
+ # Eigene Spalte "Grund" statt einer zweiten Zeile unter der Nummer - seine
+ # Ansage vom 04.09.2026: "mach eine neue spalte mit grund oder so was".
+ kopf = ["Datum", "Rechnung", "Grund", "Name", "Nächte", "Entgelt", "Satz %", "Steuer"]
+ stile = ["th", "th", "th_l", "th_l", "th", "th", "th", "th"]
daten = [[Paragraph(k, st[s_]) for k, s_ in zip(kopf, stile)]]
for b in liste:
daten.append([
Paragraph(b.datum.strftime("%d.%m.%Y"), st["zelle_c"]),
- Paragraph(b.voll_nummer + (f'
'
- f'{_art_text(b, weg)}'
- if _art_text(b, weg) else ""), st["zelle_c"]),
+ Paragraph(b.voll_nummer, st["zelle_c"]),
+ Paragraph(f'{_art_text(b, weg)}', st["zelle"]),
Paragraph(b.nachname, st["zelle"]),
Paragraph(str(b.naechte), st["zelle_c"]),
Paragraph(eur_kurz(b.gezahlt), st["zelle_r"]),
@@ -251,15 +256,18 @@ def render_monat(pfad, firma, jahr: int, monat: int, buchungen):
if len(daten) == 1:
daten.append([Paragraph("", st["zelle_c"]),
Paragraph("(keine Buchungen in diesem Monat)", st["zelle"]),
- "", "", "", "", ""])
+ "", "", "", "", "", ""])
# Namensspalte nur so breit wie der breiteste Name (Rest der Tabelle ist fest)
breiteste = max([stringWidth(b.nachname, "Helvetica", 8.5) for b in liste]
+ [stringWidth("Name", "Helvetica-Bold", 8)])
- name_w = min(max(breiteste + 6 * mm, 25 * mm), CONTENT_W - 120 * mm)
+ # Die feste Breite ist durch die Grund-Spalte gewachsen; der Name bekommt,
+ # was uebrig bleibt (34 mm reichen fuer die ueblichen Namen, lange brechen um).
+ name_w = min(max(breiteste + 6 * mm, 22 * mm), CONTENT_W - 140 * mm)
tabelle = Table(daten, repeatRows=1, hAlign="LEFT",
- colWidths=[22 * mm, 22 * mm, name_w, 15 * mm, 22 * mm, 14 * mm, 25 * mm])
+ colWidths=[20 * mm, 20 * mm, 34 * mm, name_w,
+ 13 * mm, 20 * mm, 12 * mm, 21 * mm])
tabelle.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("LINEBELOW", (0, 0), (-1, 0), 0.8, ACCENT),
diff --git a/gui_monat.py b/gui_monat.py
index ff55bf7..38ee93a 100644
--- a/gui_monat.py
+++ b/gui_monat.py
@@ -139,7 +139,9 @@ class MonatsTab(ttk.Frame):
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)
+ # 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())
@@ -153,7 +155,7 @@ class MonatsTab(ttk.Frame):
if b.storno_zu:
art_text = f"Storno zu {b.storno_zu}"
elif b.id in aufgehoben:
- art_text = "storniert"
+ art_text = f"storniert mit {aufgehoben[b.id].voll_nummer}"
elif b.art == "korrektur":
art_text = "Neuausstellung"
else:
diff --git a/modell.py b/modell.py
index 169ad2c..2e9f218 100644
--- a/modell.py
+++ b/modell.py
@@ -77,8 +77,8 @@ class Buchung:
return f"{self.jahr}-{self.rechnungsnummer}"
-def aufgehobene(buchungen) -> set:
- """ids der Buchungen, die durch einen Storno aufgehoben sind.
+def aufhebungen(buchungen) -> dict:
+ """Welcher Storno hebt welche Rechnung auf: {id der Rechnung: Storno-Buchung}.
Der Bezug ist Nummer + Datum + Nachname, nicht die Nummer allein. Genau
darum geht es hier: bei einer DOPPELT vergebenen Nummer waere die Nummer
@@ -86,14 +86,19 @@ def aufgehobene(buchungen) -> set:
berichtigendes Dokument "spezifisch und eindeutig" auf die Rechnung
bezieht. Steht auf dem Storno also 2026-020 vom 05.08. fuer Zweitgast, dann
ist die Rechnung von Erstgast NICHT aufgehoben.
+
+ Zurueckgegeben wird der Storno selbst und nicht nur ein Haken, damit die
+ Anzeige auch sagen kann, WOMIT eine Rechnung aufgehoben wurde
+ ("storniert mit 2026-901"). Sonst zeigt das Blatt nur "storniert" und man
+ muss die Gegenzeile suchen.
"""
stornos = [b for b in buchungen if b.storno_zu]
if not stornos:
- return set()
- weg = set()
+ return {}
+ zu = {}
for s in stornos:
for b in buchungen:
- if b.id in weg or b is s or not b.id:
+ if b.id in zu or b is s or not b.id:
continue
if b.voll_nummer != s.storno_zu:
continue
@@ -102,9 +107,14 @@ def aufgehobene(buchungen) -> set:
continue
if s.nachname and b.nachname and s.nachname != b.nachname:
continue
- weg.add(b.id)
+ zu[b.id] = s
break
- return weg
+ return zu
+
+
+def aufgehobene(buchungen) -> set:
+ """Nur die ids - fuer alle, die den Storno selbst nicht brauchen."""
+ return set(aufhebungen(buchungen))
def doppelte_nummern(buchungen) -> dict:
diff --git a/pruef_storno_verrechnet.py b/pruef_storno_verrechnet.py
index 2d37f1b..a790c68 100644
--- a/pruef_storno_verrechnet.py
+++ b/pruef_storno_verrechnet.py
@@ -136,14 +136,19 @@ pruefe({b.voll_nummer for b in alle if b.id in weg} == {"2026-900", "2026-910"},
print("--- Fall 3: Amtsbericht aus der Datenbank ---")
pfad = os.path.join(tmp, "August.pdf")
bericht_pdf.render_monat(pfad, firma_lib.lade(), JAHR, 8, alle)
-text = (PdfReader(pfad).pages[0].extract_text() or "").replace("\n", " ")
+# Der Text wird im PDF umbrochen ("storniert mit" / "2026-901") - Leerraum
+# also vereinheitlichen, sonst prueft man den Zeilenumbruch statt den Inhalt.
+text = " ".join((PdfReader(pfad).pages[0].extract_text() or "").split())
pruefe("2026-900" in text and "2026-901" in text,
"Rechnung UND Storno stehen im Bericht - die Meldung erklaert sich selbst")
pruefe("-52,00" in text, "der Storno steht mit seinem Minusbetrag drin")
pruefe("117,00" in text, "und die Monatssumme ist 117,00 Euro")
# Ohne Beschriftung steht im Bericht eine Stornonummer neben einer voellig
# anderen Rechnungsnummer - sein Wort: "musst dann rein schreiben storniert".
-pruefe("storniert" in text, "die aufgehobene Rechnung ist im Bericht als 'storniert' beschriftet")
+pruefe("storniert mit 2026-901" in text,
+ "die aufgehobene Rechnung nennt den Storno, der sie aufhebt")
+pruefe("storniert mit 2026-911" in text,
+ "auch dann, wenn dieser Storno in einem ANDEREN Monat liegt")
pruefe("Storno zu 2026-900" in text, "und der Storno nennt die Rechnung, die er aufhebt")
pruefe("Neuausstellung" in text, "die neue Rechnung ist als Neuausstellung beschriftet")
@@ -161,8 +166,8 @@ pruefe(not any("aufgehoben" in t for _nr, _a, t in reihen),
arten = {nr: art for nr, art, _t in reihen}
pruefe(arten.get("2026-901") == "Storno zu 2026-900",
f"der Storno ist als Storno getaggt ({arten.get('2026-901')!r})")
-pruefe(arten.get("2026-900") == "storniert",
- f"die aufgehobene Rechnung ist als storniert getaggt ({arten.get('2026-900')!r})")
+pruefe(arten.get("2026-900") == "storniert mit 2026-901",
+ f"die aufgehobene Rechnung nennt ihren Storno ({arten.get('2026-900')!r})")
pruefe(arten.get("2026-902") == "Neuausstellung",
f"und die neue als Neuausstellung ({arten.get('2026-902')!r})")