Beherbergungssteuer: Steuerjournal aus den Rechnungs-PDFs

Ersetzt die Excel-Mappe des Campinghofs. Die Rechnungs-PDFs des
Rechnungstools sind die Quelle; SQLite ist nur Cache, Excel nur noch
Import des Altbestands. Bericht fuers Amt als PDF im Briefkopf der
Rechnungen (Firmendaten aus der config.json des Rechnungstools).

- pdf_parser: liest Datum, Nummer, Name, Naechte, Zwischensumme, Satz
- db: Journal + Scan mit Cache (Pfad/mtime/Groesse)
- xlsx_io: Import der alten Tabelle, vereinheitlicht Rechnungsnummern
- bericht_pdf: Monats- und Jahresbericht (reportlab)
- GUI: Monatsreiter, Suche, Anhaken + Papierkorb, Kennzahlen-Leiste

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TheMockTv 2026-07-12 19:15:44 +02:00
commit 80967de26f
12 changed files with 1908 additions and 0 deletions

86
xlsx_io.py Normal file
View file

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""
Excel-Import.
Liest die bisherige Steuertabelle (`Beherbergungsteuer.xlsx`) ins Journal
sowohl den Altbestand aus der Zeit vor dem Rechnungstool als auch jede spätere
Fassung, die noch von Hand in Excel gepflegt wurde.
Ausgegeben wird nicht mehr nach Excel: Der Bericht fürs Amt ist ein PDF
(siehe `bericht_pdf.py`).
"""
import re
import logging
from datetime import datetime, date
import openpyxl
from modell import Buchung, MONATE, STANDARD_SATZ, runde, zerlege_nummer
log = logging.getLogger("bst.xlsx")
ERSTE_ZEILE = 5
RE_BLATT_JAHR = re.compile(r"(20\d{2})")
def _blatt_jahr(ws):
"""Jahr aus der Blatt-Überschrift ('JAN 2026 - Beherbergungssteuer') oder None."""
m = RE_BLATT_JAHR.search(str(ws["A1"].value or ""))
return int(m.group(1)) if m else None
def importiere(pfad: str, standard_satz: float = STANDARD_SATZ, quelle: str = "xlsx"):
"""
Liest eine Steuertabelle im Layout der Vorlage ein:
A Datum | B Rechnungsnummer | C Nachname | D Übernachtungen | E Gezahlt | F Satz
Zeilen, deren Datums-Jahr nicht zum Jahr des Monatsblattes passt (in der alten
Tabelle steht z.B. 27.01.2025 mitten im Januar-2026-Blatt), werden als Vertipper
behandelt und aufs Blatt-Jahr gezogen mit Eintrag im Log.
Gibt (buchungen, fehler) zurück; fehler = Liste von (Ort, Grund).
"""
buchungen, fehler = [], []
wb = openpyxl.load_workbook(pfad, data_only=True)
for ws in wb.worksheets:
if ws.title not in MONATE: # nur die Monatsblätter
continue
jahr_blatt = _blatt_jahr(ws)
for row in ws.iter_rows(min_row=ERSTE_ZEILE, max_row=ws.max_row, max_col=6):
datum, nummer, nachname, naechte, gezahlt, satz = [c.value for c in row]
if datum is None and nachname is None:
continue
ort = f"{ws.title}!{row[0].row}"
if not isinstance(datum, (datetime, date)):
fehler.append((ort, f"Datum fehlt oder ist kein Datum: {datum!r}"))
continue
d = datum.date() if isinstance(datum, datetime) else datum
if jahr_blatt and d.year != jahr_blatt:
log.warning("%s: Datum %s passt nicht zum Blatt-Jahr %d auf %d korrigiert",
ort, d.strftime("%d.%m.%Y"), jahr_blatt, jahr_blatt)
d = d.replace(year=jahr_blatt)
jahr, nr = zerlege_nummer(nummer, d.year)
if not nr:
fehler.append((ort, "keine Rechnungsnummer"))
continue
buchungen.append(Buchung(
datum=d,
rechnungsnummer=nr,
jahr=jahr,
nachname=str(nachname or "").strip(),
naechte=int(naechte or 0),
gezahlt=runde(gezahlt or 0),
satz=float(satz or standard_satz),
quelle=quelle,
pdf_pfad="",
))
log.info("Import %s: %d Zeilen, %d übersprungen", pfad, len(buchungen), len(fehler))
return buchungen, fehler