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})")