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>
86 lines
3 KiB
Python
86 lines
3 KiB
Python
# -*- 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
|