# -*- coding: utf-8 -*- """ Rechnungsberechnung - die einzige Stelle, an der gerechnet wird. Wird sowohl von der GUI (Live-Summen) als auch vom PDF-Renderer benutzt, damit Bildschirm und Ausdruck garantiert identisch sind. Logik (BRUTTO-Preise, USt wird herausgerechnet): - Katalog-Preise sind BRUTTO (Steuer ist bereits enthalten). - Position-Brutto = anzahl * preis (Standard) = anzahl * preis * personen (pro_person-Positionen, Pos. 5+6) - Je Steuersatz: enthaltene USt = Brutto-Summe * satz/(100+satz) Netto = Brutto-Summe - USt (7 % = ermaessigt/Beherbergung, 19 % = regulaer) - Rechnungsbetrag (Leistungen) = Summe der Brutto-Positionen (USt ist schon drin). - Beherbergungssteuer (optional): satz% auf den Rechnungsbetrag, OBENDRAUF, direkt darunter ausgewiesen. - Gesamtbetrag = Rechnungsbetrag + Beherbergungssteuer """ from dataclasses import dataclass, field from typing import List def runde(x): """Kaufmaennisch auf 2 Nachkommastellen runden.""" return round(float(x) + 1e-9, 2) @dataclass class PositionsZeile: nr: int leistung: str satz: int # USt-Satz in Prozent preis: float # Brutto-Einzelpreis (Steuer enthalten) anzahl: float # Tage / Menge personen: float # nur relevant bei pro_person pro_person: bool brutto: float = 0.0 # Brutto-Gesamt dieser Zeile (anzahl*preis[*personen]) netto: float = 0.0 # darin enthaltenes Netto (Brutto - enthaltene USt) @dataclass class Steuerblock: satz: int netto: float = 0.0 # Netto-Basis dieses Satzes ust: float = 0.0 # darauf entfallende USt bezeichnung: str = "" # Name des Satzes (z.B. "Mehrwertsteuer") extra_blatt: bool = False @property def brutto(self): return runde(self.netto + self.ust) @dataclass class Aufschlag: """Steuer, die auf die Endsumme (Brutto inkl. USt) aufgeschlagen wird (z.B. Beherbergungssteuer).""" satz: float bezeichnung: str = "" basis: float = 0.0 # Brutto-Endsumme, auf die der Aufschlag gerechnet wird betrag: float = 0.0 extra_blatt: bool = True @dataclass class Rechnung: zeilen: List[PositionsZeile] = field(default_factory=list) # nur aktive (anzahl>0) bloecke: List[Steuerblock] = field(default_factory=list) # USt-Bloecke (auf Netto je Satz) aufschlaege: List[Aufschlag] = field(default_factory=list) # Aufschlaege auf die Endsumme netto_gesamt: float = 0.0 # Summe aller Positionen (netto) ust_gesamt: float = 0.0 # Summe aller USt-Betraege brutto: float = 0.0 # netto + USt = Rechnungsbetrag der Leistung gesamt: float = 0.0 # brutto + alle Aufschlaege def zeile_brutto(preis, anzahl, personen, pro_person): """Brutto-Gesamt einer einzelnen Katalogzeile (Preise sind brutto).""" a = float(anzahl or 0) p = float(preis or 0) if pro_person: return runde(a * p * float(personen or 0)) return runde(a * p) def berechne(katalog, eingaben, cfg): """ katalog : Liste der Katalog-Dicts (config['katalog']) eingaben : dict {position_nr: {'anzahl': x, 'personen': y}} cfg : geladene Konfiguration (fuer Steuersaetze + Bettensteuer) Gibt eine Rechnung mit allen berechneten Werten zurueck. """ r = Rechnung() for pos in katalog: nr = pos["nr"] e = eingaben.get(nr, {}) anzahl = float(e.get("anzahl", 0) or 0) personen = float(e.get("personen", 0) or 0) if anzahl <= 0: continue # Position nicht auf der Rechnung if pos["pro_person"] and personen <= 0: continue # pro-Person-Position ohne Personenzahl -> kein Betrag brutto = zeile_brutto(pos["preis"], anzahl, personen, pos["pro_person"]) if brutto <= 0: continue satz = int(pos["satz"]) netto = runde(brutto * 100 / (100 + satz)) if satz else brutto # USt herausrechnen r.zeilen.append(PositionsZeile( nr=nr, leistung=pos["leistung"], satz=satz, preis=float(pos["preis"]), anzahl=anzahl, personen=personen, pro_person=bool(pos["pro_person"]), brutto=brutto, netto=netto, )) # Je Steuersatz EINEN Block bilden: die Brutto-Summe dieses Satzes herausrechnen # (enthaltene USt = Brutto * satz/(100+satz)). Es wird NIE eine Steuer neu # aufgeschlagen - die Preise sind bereits brutto. brutto_je_satz = {} for z in r.zeilen: brutto_je_satz[z.satz] = round(brutto_je_satz.get(z.satz, 0.0) + z.brutto, 2) r.brutto = runde(sum(z.brutto for z in r.zeilen)) # Rechnungsbetrag der Leistungen (brutto) saetze = cfg.get("steuersaetze", []) info = {int(s.get("satz", -1)): s for s in saetze} netto_total = 0.0 ust_total = 0.0 for satz in sorted(brutto_je_satz.keys()): bru = runde(brutto_je_satz[satz]) if bru <= 0: continue ust = runde(bru * satz / (100 + satz)) # in diesem Block ENTHALTENE USt netto = runde(bru - ust) s = info.get(satz, {}) r.bloecke.append(Steuerblock(satz=satz, netto=netto, ust=ust, bezeichnung=s.get("bezeichnung", ""), extra_blatt=bool(s.get("extra_blatt", False)))) netto_total += netto ust_total += ust r.netto_gesamt = runde(netto_total) r.ust_gesamt = runde(ust_total) # r.brutto (= Summe der Brutto-Positionen) entspricht netto_gesamt + ust_gesamt. # Aufschlaege: NUR Saetze mit "extra_blatt" (z.B. Beherbergungssteuer) -> werden auf # den Rechnungsbetrag (brutto) OBENDRAUF gerechnet und direkt darunter ausgewiesen. # Normale USt-Saetze (extra_blatt=False) werden NIE aufgeschlagen. positions_saetze = set(brutto_je_satz.keys()) summe = r.brutto for s in saetze: if not s.get("extra_blatt", False): continue satz = float(s.get("satz", 0) or 0) if satz <= 0 or int(satz) in positions_saetze: continue betrag = runde(r.brutto * satz / 100) if betrag <= 0: continue r.aufschlaege.append(Aufschlag(satz=satz, bezeichnung=s.get("bezeichnung", ""), basis=r.brutto, betrag=betrag, extra_blatt=True)) summe = runde(summe + betrag) r.gesamt = summe return r def eur(x): """1234.5 -> '1.234,50 €' (deutsche Schreibweise).""" s = f"{float(x):,.2f}" # 1,234.50 s = s.replace(",", "#").replace(".", ",").replace("#", ".") # 1.234,50 return s + " €" def eur_kurz(x): """Wie eur() aber ohne '€'-Suffix (fuer enge Tabellenspalten).""" s = f"{float(x):,.2f}" return s.replace(",", "#").replace(".", ",").replace("#", ".")