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:
commit
80967de26f
12 changed files with 1908 additions and 0 deletions
86
xlsx_io.py
Normal file
86
xlsx_io.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue