# -*- coding: utf-8 -*- """ Datenmodell + Rechenkern der Beherbergungssteuer. Eine Buchung = eine Zeile im Steuerjournal = eine Rechnung. Gerechnet wird ausschließlich hier, damit GUI, Excel-Export und PDF-Import garantiert dieselben Zahlen zeigen. Steuerlogik (wie in der Excel-Vorlage und im Rechnungstool): Steuer = Gezahlt / 100 * Satz wobei "Gezahlt" die Zwischensumme der Rechnung ist (Leistungen brutto, also inkl. USt, aber ohne die Beherbergungssteuer selbst). """ import re from dataclasses import dataclass, field from datetime import date MONATE = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", ] MONATE_KURZ = ["JAN", "FEB", "MÄR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEZ"] STANDARD_SATZ = 5.0 # Prozent Beherbergungssteuer def runde(x) -> float: """Kaufmännisch auf 2 Nachkommastellen runden.""" return round(float(x) + 1e-9, 2) def steuer_betrag(gezahlt, satz) -> float: """Beherbergungssteuer auf einen Rechnungsbetrag: Gezahlt / 100 * Satz.""" return runde(float(gezahlt or 0) / 100.0 * float(satz or 0)) @dataclass class Buchung: """Eine Zeile des Steuerjournals.""" datum: date rechnungsnummer: str # nur der Zähler, z.B. "238" (das Jahr steckt in .jahr) jahr: int nachname: str naechte: int gezahlt: float # Zwischensumme der Rechnung (Basis der Steuer) satz: float = STANDARD_SATZ quelle: str = "pdf" # pdf | xlsx | manuell pdf_pfad: str = "" id: int = 0 @property def steuer(self) -> float: return steuer_betrag(self.gezahlt, self.satz) @property def monat(self) -> int: """1-12""" return self.datum.month @property def voll_nummer(self) -> str: return f"{self.jahr}-{self.rechnungsnummer}" @property def schluessel(self) -> str: """Eindeutig pro Rechnung – verhindert Doppel-Import derselben PDF.""" return f"{self.jahr}-{self.rechnungsnummer}" @dataclass class Summe: """Aufsummierte Kennzahlen einer Menge von Buchungen (Monat oder Jahr).""" buchungen: int = 0 naechte: int = 0 gezahlt: float = 0.0 steuer: float = 0.0 gesamt: float = field(default=0.0) # gezahlt + steuer (wie I2 in der Vorlage) def summiere(buchungen) -> Summe: s = Summe() for b in buchungen: s.buchungen += 1 s.naechte += int(b.naechte or 0) s.gezahlt += float(b.gezahlt or 0) s.steuer += b.steuer s.gezahlt = runde(s.gezahlt) s.steuer = runde(s.steuer) s.gesamt = runde(s.gezahlt + s.steuer) return s def eur(x) -> str: """1234.5 -> '1.234,50 €' (deutsche Schreibweise).""" s = f"{float(x or 0):,.2f}" s = s.replace(",", "#").replace(".", ",").replace("#", ".") return s + " €" def eur_kurz(x) -> str: """Wie eur(), aber ohne Währungszeichen (für enge Tabellenspalten).""" s = f"{float(x or 0):,.2f}" return s.replace(",", "#").replace(".", ",").replace("#", ".") RE_NUMMER = re.compile(r"^\s*(?:(?P\d{4})\s*[-/]\s*)?(?P.+?)\s*$") def zerlege_nummer(text, standard_jahr: int): """ Bringt jede Schreibweise der Rechnungsnummer auf eine Form. '2026-238' -> (2026, '238') '013' -> (standard_jahr, '013') '13' -> (standard_jahr, '013') dreistellig wie im Rechnungstool 'A-7' -> (standard_jahr, 'A-7') nicht-numerisch bleibt, wie es ist Ohne diese Normalisierung wären '17' und '2026-17' zwei verschiedene Buchungen – in der alten Excel-Tabelle kommt beides vor. """ s = " ".join(str(text or "").split()) if not s: return standard_jahr, "" m = RE_NUMMER.match(s) jahr = int(m.group("jahr")) if m.group("jahr") else standard_jahr nr = m.group("nr") if nr.isdigit(): nr = f"{int(nr):03d}" return jahr, nr def zahl_aus_text(text) -> float: """'1.234,50 €' / '87,00' / '13.00' -> float. Leere/kaputte Eingabe -> 0.0""" if text is None: return 0.0 if isinstance(text, (int, float)): return float(text) s = "".join(c for c in str(text) if c.isdigit() or c in ",.-") if not s: return 0.0 # Deutsche Schreibweise: Punkt = Tausender, Komma = Dezimal if "," in s: s = s.replace(".", "").replace(",", ".") try: return float(s) except ValueError: return 0.0