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:
parent
c190887621
commit
82d84f7aea
4 changed files with 96 additions and 90 deletions
62
app.py
62
app.py
|
|
@ -839,7 +839,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
for name, wert in felder if wert and not parse_date(wert)]
|
||||
|
||||
def _art_waehlen(self, nummer, betrag, wer):
|
||||
"""Popup: Rechnung oder Proforma? Gibt "rechnung", "proforma" oder None.
|
||||
"""Popup: Rechnung oder Angebot? Gibt "rechnung", "angebot" oder None.
|
||||
|
||||
Ersetzt die fruehere Sicherheitsfrage - EIN Fenster statt zwei. Die
|
||||
Warnung, dass eine ausgestellte Rechnung nicht mehr zu aendern ist,
|
||||
|
|
@ -850,7 +850,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
# setzt _dialog_zeigen und drueckt die Knoepfe wirklich.
|
||||
if TESTLAUF and not getattr(self, "_dialog_zeigen", False):
|
||||
# _testwahl: was der Pruefstand im Fenster geklickt haette.
|
||||
# "rechnung" (Vorgabe), "proforma" oder None fuer Abbrechen.
|
||||
# "rechnung" (Vorgabe), "angebot" oder None fuer Abbrechen.
|
||||
return getattr(self, "_testwahl", "rechnung")
|
||||
d = tk.Toplevel(self)
|
||||
d.title("Was soll erstellt werden?")
|
||||
|
|
@ -871,9 +871,9 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
"bucht sie und meldet die Übernachtungen ans Amt.")).grid(
|
||||
row=1, column=0, sticky="w", pady=(10, 8))
|
||||
ttk.Label(rahmen, wraplength=560, justify="left", text=(
|
||||
"PROFORMA-RECHNUNG – unverbindliche Kostenaufstellung vorab. Keine "
|
||||
"ANGEBOT – unverbindliche Kostenaufstellung vorab (§ 145 BGB). Keine "
|
||||
"Rechnung im Sinne des § 14 UStG, kein Vorsteuerabzug, nicht zu zahlen. "
|
||||
"Sie verbraucht KEINE Rechnungsnummer (eigene Reihe) und wird nirgends "
|
||||
"Es verbraucht KEINE Rechnungsnummer (eigene Reihe) und wird nirgends "
|
||||
f"gebucht – die nächste Rechnung bleibt {nummer}.")).grid(
|
||||
row=2, column=0, sticky="w", pady=(0, 14))
|
||||
|
||||
|
|
@ -885,8 +885,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
knoepfe.grid(row=3, column=0, sticky="ew")
|
||||
ttk.Button(knoepfe, text=f"Rechnung {nummer} erstellen", width=28,
|
||||
command=lambda: nimm("rechnung")).pack(side="left")
|
||||
ttk.Button(knoepfe, text="Proforma-Rechnung", width=20,
|
||||
command=lambda: nimm("proforma")).pack(side="left", padx=8)
|
||||
ttk.Button(knoepfe, text="Angebot erstellen", width=20,
|
||||
command=lambda: nimm("angebot")).pack(side="left", padx=8)
|
||||
ttk.Button(knoepfe, text="Abbrechen", width=14,
|
||||
command=d.destroy).pack(side="right")
|
||||
|
||||
|
|
@ -899,16 +899,16 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.wait_window(d)
|
||||
return wahl["art"]
|
||||
|
||||
def _naechste_proforma_nummer(self):
|
||||
"""JJJJ-Pnnn aus der EIGENEN Reihe. Der Jahreswechsel faengt bei P001 an.
|
||||
def _naechstes_angebot(self):
|
||||
"""JJJJ-Annn aus der EIGENEN Reihe. Der Jahreswechsel faengt bei A001 an.
|
||||
|
||||
Wichtig: hier wird der Rechnungszaehler nicht angefasst. Eine Proforma
|
||||
Wichtig: hier wird der Rechnungszaehler nicht angefasst. Ein Angebot
|
||||
darf keine Rechnungsnummer verbrauchen - sonst muesste man die Nummern
|
||||
wieder von Hand aendern koennen, und genau daher kamen die doppelten.
|
||||
"""
|
||||
jahr = datetime.date.today().year
|
||||
zaehler = int(self.cfg.get("proforma_zaehler", 0) or 0)
|
||||
if int(self.cfg.get("proforma_jahr", 0) or 0) != jahr:
|
||||
zaehler = int(self.cfg.get("angebot_zaehler", 0) or 0)
|
||||
if int(self.cfg.get("angebot_jahr", 0) or 0) != jahr:
|
||||
zaehler = 0
|
||||
out_dir = config.ensure_output_dir(self.cfg)
|
||||
try:
|
||||
|
|
@ -917,12 +917,12 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
vorhanden = []
|
||||
for _ in range(1000):
|
||||
zaehler += 1
|
||||
nummer = f"{jahr}-P{zaehler:03d}"
|
||||
if not any(d.startswith(f"Proforma_{nummer}") for d in vorhanden):
|
||||
nummer = f"{jahr}-A{zaehler:03d}"
|
||||
if not any(d.startswith(f"Angebot_{nummer}") for d in vorhanden):
|
||||
return jahr, zaehler, nummer
|
||||
return jahr, zaehler, f"{jahr}-P{zaehler:03d}"
|
||||
return jahr, zaehler, f"{jahr}-A{zaehler:03d}"
|
||||
|
||||
def pdf_erstellen(self, proforma=False):
|
||||
def pdf_erstellen(self, angebot=False):
|
||||
montage = self.ist_montage()
|
||||
r = self.recalc()
|
||||
if not r.zeilen:
|
||||
|
|
@ -987,11 +987,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
wahl = self._art_waehlen(nummer, berechnung.eur(r.gesamt), wer)
|
||||
if wahl is None:
|
||||
return
|
||||
proforma = wahl == "proforma"
|
||||
if proforma:
|
||||
angebot = wahl == "angebot"
|
||||
if angebot:
|
||||
# EIGENE Reihe: Nummernfeld und Rechnungszaehler bleiben
|
||||
# unangetastet, die naechste Rechnung behaelt ihre Nummer.
|
||||
p_jahr, p_zaehler, nummer = self._naechste_proforma_nummer()
|
||||
a_jahr, a_zaehler, nummer = self._naechstes_angebot()
|
||||
|
||||
# Auf der Rechnung stehen Vor- und Nachname wie gewohnt in einer Zeile;
|
||||
# der Nachname geht zusätzlich getrennt mit, damit das Steuerjournal ihn
|
||||
|
|
@ -1044,8 +1044,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
# dahinter ist eine ganz normale: Zwischensumme + Beherbergungssteuer.
|
||||
kopf["vorlage"] = "montage"
|
||||
kopf["montage_text"] = self._montage_text()
|
||||
if proforma:
|
||||
kopf["art"] = "proforma"
|
||||
if angebot:
|
||||
kopf["art"] = "angebot"
|
||||
kopf["rechnungsnummer"] = nummer
|
||||
if stand:
|
||||
kopf["art"] = "korrektur"
|
||||
|
|
@ -1140,8 +1140,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.r_nummer.set(nummer)
|
||||
kopf["rechnungsnummer"] = nummer
|
||||
|
||||
if proforma:
|
||||
name = f"Proforma_{safe_filename(nummer)}"
|
||||
if angebot:
|
||||
name = f"Angebot_{safe_filename(nummer)}"
|
||||
if self.k_name.get().strip():
|
||||
name += "_" + safe_filename(self.k_name.get().strip())
|
||||
pfad = os.path.join(out_dir, name + ".pdf")
|
||||
|
|
@ -1150,22 +1150,22 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||
messagebox.showerror(
|
||||
"Fehler beim Erstellen",
|
||||
f"Proforma konnte nicht erstellt werden:\n{e}")
|
||||
print("[proforma] FEHLER:", e)
|
||||
f"Angebot konnte nicht erstellt werden:\n{e}")
|
||||
print("[angebot] FEHLER:", e)
|
||||
return
|
||||
# NUR der Proforma-Zaehler wandert weiter. Rechnungsnummer, deren
|
||||
# NUR der Angebots-Zaehler wandert weiter. Rechnungsnummer, deren
|
||||
# Zaehler, das gemeinsame Nummernbuch und die Meldung ans
|
||||
# Steuerjournal bleiben unberuehrt - gebucht wird eine Proforma
|
||||
# Steuerjournal bleiben unberuehrt - gebucht wird ein Angebot
|
||||
# nirgends.
|
||||
self.cfg["proforma_jahr"] = p_jahr
|
||||
self.cfg["proforma_zaehler"] = p_zaehler
|
||||
self.cfg["angebot_jahr"] = a_jahr
|
||||
self.cfg["angebot_zaehler"] = a_zaehler
|
||||
try:
|
||||
config.save_config(self.cfg)
|
||||
except OSError as e: # noqa: BLE001
|
||||
print("[proforma] Zaehler nicht gespeichert:", e)
|
||||
print(f"[proforma] erstellt -> {pfad}")
|
||||
print("[angebot] Zaehler nicht gespeichert:", e)
|
||||
print(f"[angebot] erstellt -> {pfad}")
|
||||
if messagebox.askyesno(
|
||||
"Fertig", f"Proforma-Rechnung erstellt:\n{pfad}\n\n"
|
||||
"Fertig", f"Angebot erstellt:\n{pfad}\n\n"
|
||||
"Jetzt öffnen (zum Drucken)?"):
|
||||
self._oeffne_datei(pfad)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -140,10 +140,10 @@ DEFAULT_CONFIG = {
|
|||
"texte": DEFAULT_TEXTE,
|
||||
"katalog": DEFAULT_KATALOG,
|
||||
"montage": DEFAULT_MONTAGE,
|
||||
# Die Proforma hat eine EIGENE Reihe (2026-P001): sie darf keine
|
||||
# Das Angebot hat eine EIGENE Reihe (2026-A001): es darf keine
|
||||
# Rechnungsnummer verbrauchen - die gehoert zu einer echten Rechnung.
|
||||
"proforma_jahr": 0,
|
||||
"proforma_zaehler": 0,
|
||||
"angebot_jahr": 0,
|
||||
"angebot_zaehler": 0,
|
||||
# Alle Steuern als EINE benannte Liste.
|
||||
# - extra_blatt = False: USt-Satz, wird auf das Netto der zugeordneten Leistungen gerechnet.
|
||||
# - extra_blatt = True : Aufschlag auf die Endsumme (z.B. Beherbergungssteuer), eigenes Blatt.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pruefstand: die Proforma-Rechnung.
|
||||
"""Pruefstand: das unverbindliche Angebot.
|
||||
|
||||
Sein Auftrag: ein Knopf, im Popup waehlt man Rechnung oder Proforma - *"musst
|
||||
Sein Auftrag: ein Knopf, im Popup waehlt man Rechnung oder Angebot - *"musst
|
||||
nur auf passen das der rechner dann die rechnungs nummer nicht aendert, sonst
|
||||
muessen wir wieder die nummern aenderbar machen und dann macht er wieder
|
||||
fehler"*.
|
||||
|
||||
Genau das ist hier die Hauptpruefung: die Proforma laeuft in einer EIGENEN
|
||||
Reihe (2026-P001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt.
|
||||
Genau das ist hier die Hauptpruefung: das Angebot laeuft in einer EIGENEN
|
||||
Reihe (2026-A001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt.
|
||||
|
||||
Dazu die Rechtslage (nachgeschlagen am 03.09.2026):
|
||||
* keine Nummer aus dem regulaeren Kreis - die fortlaufende Nummerierung nach
|
||||
§ 14 Abs. 4 Nr. 4 UStG gilt nur fuer echte Rechnungen
|
||||
* kein Steuersatz und kein Steuerbetrag auf dem Blatt (§ 14c UStG: wer Steuer
|
||||
ausweist, schuldet sie)
|
||||
* deutlich als "Proforma-Rechnung" kennzeichnen, sonst kann das Finanzamt das
|
||||
Blatt als echte Rechnung werten
|
||||
* deutlich kennzeichnen, sonst kann das Finanzamt das Blatt als echte Rechnung
|
||||
werten (§ 14 Abs. 1 S. 1 UStG: die Bezeichnung ist gleichgueltig). "Angebot"
|
||||
ist im Camping- und Hotelgeschaeft der uebliche Begriff, "Kostenanschlag"
|
||||
gehoert ins Werkvertragsrecht (§ 632 Abs. 3, § 649 BGB).
|
||||
|
||||
Gefahren wird das ECHTE Popup - die Knoepfe werden angeklickt, nicht umgangen.
|
||||
"""
|
||||
|
|
@ -51,7 +53,7 @@ _wecker.daemon = True
|
|||
_wecker.start()
|
||||
|
||||
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||
tmp = tempfile.mkdtemp(prefix="proforma_pruef_")
|
||||
tmp = tempfile.mkdtemp(prefix="angebot_pruef_")
|
||||
ordner = os.path.join(tmp, "rechnungen")
|
||||
os.makedirs(ordner)
|
||||
|
||||
|
|
@ -132,8 +134,8 @@ def kunde(vorname, nachname):
|
|||
# ---- 1. Der eine Knopf und das Popup --------------------------------------
|
||||
knopf_texte = [str(k.cget("text")) for k in knoepfe_von(f)]
|
||||
pruefe("PDF erstellen" in knopf_texte, f"unten steht EIN Knopf zum Erstellen ({knopf_texte})")
|
||||
pruefe(not any("Proforma" in t for t in knopf_texte),
|
||||
"und kein zweiter Knopf fuer die Proforma - gewaehlt wird im Popup")
|
||||
pruefe(not any("Angebot" in t for t in knopf_texte),
|
||||
"und kein zweiter Knopf fuers Angebot - gewaehlt wird im Popup")
|
||||
|
||||
kunde("Anna", "Vorab")
|
||||
nummer_vorher = f.r_nummer.get()
|
||||
|
|
@ -145,33 +147,35 @@ klicke_im_popup("Abbrechen")
|
|||
f.pdf_erstellen()
|
||||
pruefe(not os.listdir(ordner), f"Abbrechen schreibt nichts ({os.listdir(ordner)})")
|
||||
|
||||
# ---- 3. Proforma waehlen -------------------------------------------------
|
||||
klicke_im_popup("Proforma")
|
||||
# ---- 3. Angebot waehlen --------------------------------------------------
|
||||
klicke_im_popup("Angebot")
|
||||
f.pdf_erstellen()
|
||||
dateien = sorted(os.listdir(ordner))
|
||||
pruefe(dateien == ["Proforma_2026-P001_Vorab.pdf"], f"die Proforma liegt da ({dateien})")
|
||||
pruefe(dateien == ["Angebot_2026-A001_Vorab.pdf"], f"das Angebot liegt da ({dateien})")
|
||||
|
||||
# ---- 4. DIE Hauptsache: die Rechnungsnummer ist unberuehrt --------------
|
||||
pruefe(f.r_nummer.get() == nummer_vorher,
|
||||
f"das Nummernfeld steht unveraendert auf {nummer_vorher} ({f.r_nummer.get()})")
|
||||
pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher,
|
||||
f"der Rechnungszaehler ist unveraendert ({f.cfg.get('rechnung_zaehler')})")
|
||||
pruefe(f.cfg.get("proforma_zaehler") == 1,
|
||||
f"nur der Proforma-Zaehler ist weitergelaufen ({f.cfg.get('proforma_zaehler')})")
|
||||
pruefe(f.cfg.get("angebot_zaehler") == 1,
|
||||
f"nur der Angebots-Zaehler ist weitergelaufen ({f.cfg.get('angebot_zaehler')})")
|
||||
|
||||
# ---- 5. Was auf dem Blatt steht -----------------------------------------
|
||||
from pypdf import PdfReader # noqa: E402
|
||||
|
||||
pfad = os.path.join(ordner, "Proforma_2026-P001_Vorab.pdf")
|
||||
pfad = os.path.join(ordner, "Angebot_2026-A001_Vorab.pdf")
|
||||
seiten = PdfReader(pfad).pages
|
||||
pruefe(len(seiten) == 1, f"eine Seite - kein Blatt fuers Amt ({len(seiten)})")
|
||||
text = (seiten[0].extract_text() or "").replace("\n", " ")
|
||||
pruefe("Proforma-Rechnung" in text, "es steht deutlich Proforma-Rechnung drauf")
|
||||
pruefe("Unverbindliches Angebot" in text, "es steht deutlich Unverbindliches Angebot drauf")
|
||||
pruefe("Rechnung" not in text.split("Unverbindliches Angebot")[0],
|
||||
"und das Wort Rechnung steht nicht in der Ueberschrift")
|
||||
pruefe("keine Rechnung im Sinne des § 14 UStG" in text,
|
||||
"mit dem Satz, dass es keine Rechnung nach § 14 UStG ist")
|
||||
pruefe("keine Rechnungsnummer" in text, "und dass die Nummer keine Rechnungsnummer ist")
|
||||
pruefe("2026-P001" in text and "2026-001" not in text,
|
||||
"nur die Proforma-Nummer steht drauf, keine Rechnungsnummer")
|
||||
pruefe("2026-A001" in text and "2026-001" not in text,
|
||||
"nur die Angebots-Nummer steht drauf, keine Rechnungsnummer")
|
||||
pruefe("Mehrwertsteuer" not in text and "Nettobetrag" not in text,
|
||||
"kein Steuerbetrag und kein Nettoausweis (§ 14c UStG)")
|
||||
pruefe("Beherbergungssteuer" not in text and "Zwischensumme" not in text,
|
||||
|
|
@ -182,7 +186,7 @@ pruefe("Voraussichtlicher Gesamtbetrag" in text,
|
|||
"der Betrag heisst voraussichtlich, nicht Rechnungsbetrag")
|
||||
|
||||
meta = BE.kenndaten_lesen(pfad) or {}
|
||||
pruefe(meta.get("art") == "proforma", f"art = proforma ({meta.get('art')})")
|
||||
pruefe(meta.get("art") == "angebot", f"art = angebot ({meta.get('art')})")
|
||||
pruefe(float(meta.get("zwischensumme") or 0) == 0.0 and float(meta.get("gesamt") or 0) == 0.0,
|
||||
f"die Kenndaten tragen Nullen ({meta.get('zwischensumme')}, {meta.get('gesamt')})")
|
||||
|
||||
|
|
@ -195,16 +199,16 @@ pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-001_Vorab.pdf")),
|
|||
pruefe(f.cfg.get("rechnung_zaehler") == 1,
|
||||
f"jetzt erst laeuft der Rechnungszaehler weiter ({f.cfg.get('rechnung_zaehler')})")
|
||||
|
||||
# ---- 7. Die zweite Proforma zaehlt in ihrer Reihe weiter ---------------
|
||||
# ---- 7. Das zweite Angebot zaehlt in seiner Reihe weiter ---------------
|
||||
f.reset_formular()
|
||||
kunde("Bernd", "Zweitvorab")
|
||||
klicke_im_popup("Proforma")
|
||||
klicke_im_popup("Angebot")
|
||||
f.pdf_erstellen()
|
||||
pruefe(os.path.exists(os.path.join(ordner, "Proforma_2026-P002_Zweitvorab.pdf")),
|
||||
f"die zweite Proforma heisst P002 ({sorted(os.listdir(ordner))})")
|
||||
pruefe(os.path.exists(os.path.join(ordner, "Angebot_2026-A002_Zweitvorab.pdf")),
|
||||
f"das zweite Angebot heisst A002 ({sorted(os.listdir(ordner))})")
|
||||
f.destroy()
|
||||
|
||||
# ---- 8. Das Steuerjournal bucht die Proforma NICHT --------------------
|
||||
# ---- 8. Das Steuerjournal bucht das Angebot NICHT ---------------------
|
||||
sys.path.append(JOURNAL)
|
||||
import db as BST # noqa: E402
|
||||
|
||||
|
|
@ -214,8 +218,8 @@ buchungen = journal.buchungen(jahr=2026)
|
|||
pruefe(len(buchungen) == 1, f"nur die echte Rechnung ist gebucht ({len(buchungen)})")
|
||||
pruefe(buchungen and buchungen[0].voll_nummer == "2026-001",
|
||||
f"und zwar 2026-001 ({[b.voll_nummer for b in buchungen]})")
|
||||
pruefe(not any("P0" in b.voll_nummer for b in buchungen),
|
||||
"keine Proforma-Nummer im Journal")
|
||||
pruefe(not any("A0" in b.voll_nummer for b in buchungen),
|
||||
"keine Angebots-Nummer im Journal")
|
||||
journal.schliessen()
|
||||
|
||||
if not fehler:
|
||||
Loading…
Add table
Add a link
Reference in a new issue