Aus der Proforma-Rechnung wird ein unverbindliches Angebot

Seine Frage war: "was ist rechtssicher, kostenvoranschlag oder proforma - ich
bin kein jurist, deswegen sage ich ja § schauen". Nachgesehen:

* Kostenanschlag ist ein Begriff des WERKvertrags (§ 632 Abs. 3 BGB "Ein
  Kostenanschlag ist im Zweifel nicht zu verguten", § 649 BGB) - das ist der
  Handwerker-Fall. Bartl vermietet, das passt nicht.
* "Proforma-Rechnung" steht in keinem Gesetz (Zoll- und Exportbegriff) und
  traegt als Einziges das Wort "Rechnung".
* Das Angebot ist § 145 BGB: gebunden ist man daran, "es sei denn, dass er die
  Gebundenheit ausgeschlossen hat" - deshalb steht "unverbindlich" drauf.
* Im Camping- und Hotelgeschaeft ist "unverbindliches Angebot" der uebliche
  Begriff (auch in den Muster-AGB des Branchenverbands BVCD).

Seine Entscheidung: "dann mach angebot draus, ist das gaengigste" - Begruendung
"dann ist der name rechnung weg und erweckt nicht den anschein das man eine
bekommen hat".

Deshalb heisst es jetzt ueberall Angebot: Ueberschrift "Unverbindliches
Angebot", "Angebot an:", "Angebot-Nr.: 2026-A001" und "Datum:" statt
"Rechnungsdatum:" - das Wort Rechnung kommt im ganzen Kopf nicht mehr vor, und
genau das prueft pruef_angebot.py auch nach. Eigene Reihe 2026-A001, Dateiname
Angebot_..., eigener Zaehler (angebot_jahr/angebot_zaehler).

Steuerlich aendert der Name nichts (§ 14 Abs. 1 S. 1 UStG: die Bezeichnung des
Dokuments ist gleichgueltig) - die Sicherheit kommt weiter vom fehlenden
Steuerausweis, siehe UStAE 14c.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
TheMockTv 2026-09-03 21:37:32 +02:00
parent c190887621
commit 82d84f7aea
4 changed files with 96 additions and 90 deletions

View file

@ -278,7 +278,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
story.append(Spacer(1, 5 * mm))
# Kundenadresse (links) | Rechnungsinfos (rechts)
kunde_flow = [Paragraph("Proforma an:" if kopf.get("art") == "proforma"
kunde_flow = [Paragraph("Angebot an:" if kopf.get("art") == "angebot"
else "Rechnung an:", st["addr_label"])]
for z in kopf.get("kunde_zeilen", []):
if str(z).strip():
@ -291,12 +291,14 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
# Keine Positionstabelle, keine Naechte im Kopf - der Zeitraum steht in der
# einen Textzeile, so wie Bartl das Blatt bisher von Hand geschrieben hat.
montage = kopf.get("vorlage") == "montage"
proforma = art == "proforma"
angebot = art == "angebot"
info_rows = [
("Stornonummer:" if art == "storno" else
"Proforma-Nr.:" if proforma else "Rechnungsnummer:",
"Angebot-Nr.:" if angebot else "Rechnungsnummer:",
kopf.get("rechnungsnummer", "")),
("Rechnungsdatum:", kopf.get("datum", "")),
# Auf dem Angebot faellt das Wort "Rechnung" ueberall weg: es soll gar
# nicht erst der Eindruck entstehen, der Gast haette schon eine.
("Datum:" if angebot else "Rechnungsdatum:", kopf.get("datum", "")),
]
# Der Bezug auf die alte Rechnung ist Pflicht (§ 31 Abs. 5 UStDV): Nummer UND Datum.
if art == "storno" and kopf.get("storno_zu"):
@ -360,39 +362,38 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
# eindeutig als Aufhebung erkennbar ist.
titel = {"storno": "Stornorechnung",
"berichtigung": "Berichtigte Rechnung",
"proforma": "Proforma-Rechnung (Kostenvoranschlag)"}.get(art, "Rechnungsübersicht")
"angebot": "Unverbindliches Angebot"}.get(art, "Rechnungsübersicht")
titel += " (für das Amt)" if gesamtrechnung_betont else ""
story.append(Paragraph(titel, st["h"]))
if proforma:
if angebot:
# Ohne diesen Satz sieht das Blatt aus wie eine Rechnung. Es ist keine -
# und ohne die deutliche Kennzeichnung kann das Finanzamt es als eine
# werten. Ein Steuersatz oder Steuerbetrag steht bewusst NIRGENDS auf
# dem Blatt: wer Steuer ausweist, schuldet sie (§ 14c UStG).
story.append(Paragraph(
"<b>Dies ist ein Kostenvoranschlag</b> – eine unverbindliche Vorab-"
"Aufstellung der Kosten. Sie ist <b>keine Rechnung im Sinne des § 14 "
"UStG</b>, berechtigt nicht zum Vorsteuerabzug und ist nicht zu bezahlen. "
"Die Nummer oben ist <b>keine Rechnungsnummer</b>. Alle Beträge in Euro "
"inkl. gesetzlicher Umsatzsteuer; ausgewiesen wird sie erst in der "
"Rechnung nach dem Aufenthalt.", st["dank"]))
"<b>Dies ist ein unverbindliches Angebot</b> und <b>keine Rechnung im "
"Sinne des § 14 UStG</b>: es berechtigt nicht zum Vorsteuerabzug und ist "
"nicht zu bezahlen. Die Nummer oben ist <b>keine Rechnungsnummer</b>. "
"Alle Beträge in Euro inkl. gesetzlicher Umsatzsteuer; ausgewiesen wird "
"sie erst in der Rechnung nach dem Aufenthalt.", st["dank"]))
story.append(Spacer(1, 3 * mm))
if montage:
_montage_zeile(story, st, rechnung, kopf)
else:
_positionstabelle(story, st, rechnung, ohne_ust=proforma)
_positionstabelle(story, st, rechnung, ohne_ust=angebot)
story.append(Spacer(1, 3 * mm))
# ---- Summenblock: Netto -> USt je Satz -> Zwischensumme (brutto)
# -> zzgl. Beherbergungssteuer -> Gesamtbetrag ----
# Auf der Proforma steht weder Netto noch Umsatzsteuer: ausgewiesen wird
# Auf dem Angebot steht weder Netto noch Umsatzsteuer: ausgewiesen wird
# sie erst mit der Rechnung. Die Beherbergungssteuer ist keine Umsatzsteuer
# und darf bleiben - der Gast soll sehen, was er zahlen wird.
summen_rows = [] if proforma else [[
summen_rows = [] if angebot else [[
Paragraph("Nettobetrag:", st["info_l"]),
Paragraph(eur(rechnung.netto_gesamt), st["cell_r"]),
]]
for b in ([] if proforma else rechnung.bloecke):
for b in ([] if angebot else rechnung.bloecke):
name = b.bezeichnung or "USt"
summen_rows.append([
Paragraph(f"{name} {b.satz} %:", st["info_l"]),
@ -408,10 +409,10 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
#
# Auf der Kundenseite bleibt alles wie gehabt - dort MUSS die Steuer ausgewiesen
# sein und im Gesamtbetrag stecken, das ist der Betrag, den der Gast zahlt.
# Auf der Proforma wird gar keine Steuer ausgewiesen - weder Umsatzsteuer
# Auf dem Angebot wird gar keine Steuer ausgewiesen - weder Umsatzsteuer
# noch Beherbergungssteuer. Es steht nur, was der Gast voraussichtlich
# zahlt; aufgeschluesselt wird erst in der Rechnung.
if rechnung.aufschlaege and not gesamtrechnung_betont and not proforma:
if rechnung.aufschlaege and not gesamtrechnung_betont and not angebot:
summen_rows.append([
Paragraph("Zwischensumme:", st["info_l"]),
Paragraph(eur(rechnung.brutto), st["cell_r"]),
@ -425,7 +426,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt
summen_rows.append([
Paragraph("Voraussichtlicher Gesamtbetrag:" if proforma else
Paragraph("Voraussichtlicher Gesamtbetrag:" if angebot else
"Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:",
st["total_l"]),
Paragraph(eur(endsumme), st["total_v"]),
@ -444,7 +445,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
# Hinweis zur Beherbergungssteuer - nur auf der Kundenseite. Auf dem Amt-Blatt
# wird die Steuer gar nicht mehr ausgewiesen, dann waere der Satz sinnlos.
if proforma:
if angebot:
story.append(Spacer(1, 1.5 * mm))
story.append(Paragraph(
"In diesem Betrag sind alle Steuern und Abgaben enthalten. Aufgeschlüsselt "
@ -501,14 +502,15 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
"ihre Nummer.", st["dank"]))
if texte.get("dank1"):
block.append(Paragraph(texte["dank1"], st["dank"]))
elif proforma:
elif angebot:
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
block.append(Paragraph(
"Diese Aufstellung ist unverbindlich und dient nur zur Information. "
"Sie ist keine Zahlungsaufforderung – die Rechnung erhalten Sie nach "
"dem Aufenthalt. Für Rückfragen stehen wir gern zur Verfügung.",
"Dieses Angebot ist freibleibend und unverbindlich (§ 145 BGB) und "
"keine Zahlungsaufforderung. Sagen Sie uns Bescheid, dann reservieren "
"wir – die Rechnung erhalten Sie nach dem Aufenthalt. Für Rückfragen "
"stehen wir gern zur Verfügung.",
st["dank"]))
else:
if art == "korrektur" and kopf.get("ersetzt"):
@ -550,12 +552,12 @@ def _kenndaten(rechnung, kopf):
Steuerrechner summieren dadurch von allein richtig (+100 -100 +120 = 120).
"""
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
if kopf.get("art") == "proforma":
# Eine Proforma ist kein Umsatz - sie traegt Nullen. Ein Programm, das
if kopf.get("art") == "angebot":
# Ein Angebot ist kein Umsatz - es traegt Nullen. Ein Programm, das
# "art" nicht kennt, zaehlt dann 0,00 statt einer Uebernachtung, die nie
# abgerechnet wurde.
return json.dumps({
"quelle": "rechnungstool", "version": 3, "art": "proforma",
"quelle": "rechnungstool", "version": 3, "art": "angebot",
"rechnungsnummer": kopf.get("rechnungsnummer", ""),
"datum": kopf.get("datum", ""),
"nachname": kopf.get("nachname", ""), "vorname": kopf.get("vorname", ""),
@ -652,16 +654,16 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
title=({"storno": "Stornorechnung",
"proforma": "Proforma-Rechnung"}.get(kopf.get("art"), "Rechnung"))
"angebot": "Angebot"}.get(kopf.get("art"), "Rechnung"))
+ f" {kopf.get('rechnungsnummer','')}",
author=cfg["firma"].get("name", ""),
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
)
story = []
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Kunde
# Eine Proforma bekommt KEIN Blatt fuers Amt: gemeldet wird eine
# Ein Angebot bekommt KEIN Blatt fuers Amt: gemeldet wird eine
# Uebernachtung erst, wenn sie abgerechnet ist.
if rechnung.aufschlaege and kopf.get("art") != "proforma":
if rechnung.aufschlaege and kopf.get("art") != "angebot":
story.append(PageBreak())
story.append(_AmtMarke(marke))
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt