beherbergungssteuer/xlsx_io.py
TheMockTv 80967de26f 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>
2026-07-12 19:15:44 +02:00

86 lines
3 KiB
Python
Raw Permalink 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 -*-
"""
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