diff --git a/app.py b/app.py index 960fe55..39fe703 100644 --- a/app.py +++ b/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 Angebot? Gibt "rechnung", "angebot" oder None. + """Popup: Rechnung oder Proforma? Gibt "rechnung", "proforma" 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), "angebot" oder None fuer Abbrechen. + # "rechnung" (Vorgabe), "proforma" 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=( - "ANGEBOT – unverbindliche Kostenaufstellung vorab (§ 145 BGB). Keine " + "PROFORMA-RECHNUNG – unverbindliche Kostenaufstellung vorab. Keine " "Rechnung im Sinne des § 14 UStG, kein Vorsteuerabzug, nicht zu zahlen. " - "Es verbraucht KEINE Rechnungsnummer (eigene Reihe) und wird nirgends " + "Sie 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="Angebot erstellen", width=20, - command=lambda: nimm("angebot")).pack(side="left", padx=8) + ttk.Button(knoepfe, text="Proforma-Rechnung", width=20, + command=lambda: nimm("proforma")).pack(side="left", padx=8) ttk.Button(knoepfe, text="Abbrechen", width=14, command=d.destroy).pack(side="right") @@ -899,46 +899,30 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk): self.wait_window(d) return wahl["art"] - def _angebot_ordner(self): - """Unterordner "Angebote" neben den Rechnungen. + def _naechste_proforma_nummer(self): + """JJJJ-Pnnn aus der EIGENEN Reihe. Der Jahreswechsel faengt bei P001 an. - Angebote sind keine Buchhaltung - im Rechnungsordner haetten sie nur - zwischen den Belegen gelegen. Aufzubewahren sind sie trotzdem: ein - abgesandtes Angebot ist ein Handelsbrief (§ 257 HGB). Das Steuerjournal - liest den Ordner mit, ueberspringt sie aber (art="angebot"). - """ - ordner = os.path.join(config.ensure_output_dir(self.cfg), "Angebote") - try: - os.makedirs(ordner, exist_ok=True) - except OSError as e: # noqa: BLE001 - dann eben neben die Rechnungen - print("[angebot] Unterordner nicht anlegbar:", e) - return config.ensure_output_dir(self.cfg) - return ordner - - def _naechstes_angebot(self): - """JJJJ-Annn aus der EIGENEN Reihe. Der Jahreswechsel faengt bei A001 an. - - Wichtig: hier wird der Rechnungszaehler nicht angefasst. Ein Angebot + Wichtig: hier wird der Rechnungszaehler nicht angefasst. Eine Proforma 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("angebot_zaehler", 0) or 0) - if int(self.cfg.get("angebot_jahr", 0) or 0) != jahr: + zaehler = int(self.cfg.get("proforma_zaehler", 0) or 0) + if int(self.cfg.get("proforma_jahr", 0) or 0) != jahr: zaehler = 0 - ordner = self._angebot_ordner() + out_dir = config.ensure_output_dir(self.cfg) try: - vorhanden = os.listdir(ordner) + vorhanden = os.listdir(out_dir) except OSError: vorhanden = [] for _ in range(1000): zaehler += 1 - nummer = f"{jahr}-A{zaehler:03d}" - if not any(d.startswith(f"Angebot_{nummer}") for d in vorhanden): + nummer = f"{jahr}-P{zaehler:03d}" + if not any(d.startswith(f"Proforma_{nummer}") for d in vorhanden): return jahr, zaehler, nummer - return jahr, zaehler, f"{jahr}-A{zaehler:03d}" + return jahr, zaehler, f"{jahr}-P{zaehler:03d}" - def pdf_erstellen(self, angebot=False): + def pdf_erstellen(self, proforma=False): montage = self.ist_montage() r = self.recalc() if not r.zeilen: @@ -1003,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 - angebot = wahl == "angebot" - if angebot: + proforma = wahl == "proforma" + if proforma: # EIGENE Reihe: Nummernfeld und Rechnungszaehler bleiben # unangetastet, die naechste Rechnung behaelt ihre Nummer. - a_jahr, a_zaehler, nummer = self._naechstes_angebot() + p_jahr, p_zaehler, nummer = self._naechste_proforma_nummer() # Auf der Rechnung stehen Vor- und Nachname wie gewohnt in einer Zeile; # der Nachname geht zusätzlich getrennt mit, damit das Steuerjournal ihn @@ -1060,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 angebot: - kopf["art"] = "angebot" + if proforma: + kopf["art"] = "proforma" kopf["rechnungsnummer"] = nummer if stand: kopf["art"] = "korrektur" @@ -1156,32 +1140,32 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk): self.r_nummer.set(nummer) kopf["rechnungsnummer"] = nummer - if angebot: - name = f"Angebot_{safe_filename(nummer)}" + if proforma: + name = f"Proforma_{safe_filename(nummer)}" if self.k_name.get().strip(): name += "_" + safe_filename(self.k_name.get().strip()) - pfad = os.path.join(self._angebot_ordner(), name + ".pdf") + pfad = os.path.join(out_dir, name + ".pdf") try: pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf) except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen messagebox.showerror( "Fehler beim Erstellen", - f"Angebot konnte nicht erstellt werden:\n{e}") - print("[angebot] FEHLER:", e) + f"Proforma konnte nicht erstellt werden:\n{e}") + print("[proforma] FEHLER:", e) return - # NUR der Angebots-Zaehler wandert weiter. Rechnungsnummer, deren + # NUR der Proforma-Zaehler wandert weiter. Rechnungsnummer, deren # Zaehler, das gemeinsame Nummernbuch und die Meldung ans - # Steuerjournal bleiben unberuehrt - gebucht wird ein Angebot + # Steuerjournal bleiben unberuehrt - gebucht wird eine Proforma # nirgends. - self.cfg["angebot_jahr"] = a_jahr - self.cfg["angebot_zaehler"] = a_zaehler + self.cfg["proforma_jahr"] = p_jahr + self.cfg["proforma_zaehler"] = p_zaehler try: config.save_config(self.cfg) except OSError as e: # noqa: BLE001 - print("[angebot] Zaehler nicht gespeichert:", e) - print(f"[angebot] erstellt -> {pfad}") + print("[proforma] Zaehler nicht gespeichert:", e) + print(f"[proforma] erstellt -> {pfad}") if messagebox.askyesno( - "Fertig", f"Angebot erstellt:\n{pfad}\n\n" + "Fertig", f"Proforma-Rechnung erstellt:\n{pfad}\n\n" "Jetzt öffnen (zum Drucken)?"): self._oeffne_datei(pfad) return diff --git a/config.py b/config.py index 0b2a5fb..a9f4670 100644 --- a/config.py +++ b/config.py @@ -140,10 +140,10 @@ DEFAULT_CONFIG = { "texte": DEFAULT_TEXTE, "katalog": DEFAULT_KATALOG, "montage": DEFAULT_MONTAGE, - # Das Angebot hat eine EIGENE Reihe (2026-A001): es darf keine + # Die Proforma hat eine EIGENE Reihe (2026-P001): sie darf keine # Rechnungsnummer verbrauchen - die gehoert zu einer echten Rechnung. - "angebot_jahr": 0, - "angebot_zaehler": 0, + "proforma_jahr": 0, + "proforma_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. diff --git a/pdf_renderer.py b/pdf_renderer.py index 9e74c17..70e48aa 100644 --- a/pdf_renderer.py +++ b/pdf_renderer.py @@ -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("Angebot an:" if kopf.get("art") == "angebot" + kunde_flow = [Paragraph("Proforma an:" if kopf.get("art") == "proforma" else "Rechnung an:", st["addr_label"])] for z in kopf.get("kunde_zeilen", []): if str(z).strip(): @@ -291,14 +291,12 @@ 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" - angebot = art == "angebot" + proforma = art == "proforma" info_rows = [ ("Stornonummer:" if art == "storno" else - "Angebotsnummer:" if angebot else "Rechnungsnummer:", + "Proforma-Nr.:" if proforma else "Rechnungsnummer:", kopf.get("rechnungsnummer", "")), - # 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", "")), + ("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"): @@ -362,38 +360,39 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont): # eindeutig als Aufhebung erkennbar ist. titel = {"storno": "Stornorechnung", "berichtigung": "Berichtigte Rechnung", - "angebot": "Unverbindliches Angebot"}.get(art, "Rechnungsübersicht") + "proforma": "Proforma-Rechnung"}.get(art, "Rechnungsübersicht") titel += " (für das Amt)" if gesamtrechnung_betont else "" story.append(Paragraph(titel, st["h"])) - if angebot: + if proforma: # 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( - "Dies ist ein unverbindliches Angebot und keine Rechnung im " - "Sinne des § 14 UStG: es berechtigt nicht zum Vorsteuerabzug und ist " - "nicht zu bezahlen. Alle Beträge in Euro inkl. gesetzlicher " - "Umsatzsteuer; ausgewiesen wird " - "sie erst in der Rechnung nach dem Aufenthalt.", st["dank"])) + "Dies ist eine Proforma-Rechnung – eine unverbindliche Vorab-" + "Aufstellung der Kosten. Sie ist keine Rechnung im Sinne des § 14 " + "UStG, berechtigt nicht zum Vorsteuerabzug und ist nicht zu bezahlen. " + "Die Nummer oben ist keine Rechnungsnummer. 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=angebot) + _positionstabelle(story, st, rechnung, ohne_ust=proforma) story.append(Spacer(1, 3 * mm)) # ---- Summenblock: Netto -> USt je Satz -> Zwischensumme (brutto) # -> zzgl. Beherbergungssteuer -> Gesamtbetrag ---- - # Auf dem Angebot steht weder Netto noch Umsatzsteuer: ausgewiesen wird + # Auf der Proforma 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 angebot else [[ + summen_rows = [] if proforma else [[ Paragraph("Nettobetrag:", st["info_l"]), Paragraph(eur(rechnung.netto_gesamt), st["cell_r"]), ]] - for b in ([] if angebot else rechnung.bloecke): + for b in ([] if proforma else rechnung.bloecke): name = b.bezeichnung or "USt" summen_rows.append([ Paragraph(f"{name} {b.satz} %:", st["info_l"]), @@ -409,10 +408,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 dem Angebot wird gar keine Steuer ausgewiesen - weder Umsatzsteuer + # Auf der Proforma 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 angebot: + if rechnung.aufschlaege and not gesamtrechnung_betont and not proforma: summen_rows.append([ Paragraph("Zwischensumme:", st["info_l"]), Paragraph(eur(rechnung.brutto), st["cell_r"]), @@ -426,7 +425,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 angebot else + Paragraph("Voraussichtlicher Gesamtbetrag:" if proforma else "Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:", st["total_l"]), Paragraph(eur(endsumme), st["total_v"]), @@ -445,7 +444,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 angebot: + if proforma: story.append(Spacer(1, 1.5 * mm)) story.append(Paragraph( "In diesem Betrag sind alle Steuern und Abgaben enthalten. Aufgeschlüsselt " @@ -502,15 +501,14 @@ 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 angebot: + elif proforma: # 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( - "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.", + "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.", st["dank"])) else: if art == "korrektur" and kopf.get("ersetzt"): @@ -552,12 +550,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") == "angebot": - # Ein Angebot ist kein Umsatz - es traegt Nullen. Ein Programm, das + if kopf.get("art") == "proforma": + # Eine Proforma ist kein Umsatz - sie 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": "angebot", + "quelle": "rechnungstool", "version": 3, "art": "proforma", "rechnungsnummer": kopf.get("rechnungsnummer", ""), "datum": kopf.get("datum", ""), "nachname": kopf.get("nachname", ""), "vorname": kopf.get("vorname", ""), @@ -654,16 +652,16 @@ def render_rechnung(pfad, cfg, rechnung, kopf): leftMargin=MARGIN_L, rightMargin=MARGIN_R, topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT, title=({"storno": "Stornorechnung", - "angebot": "Angebot"}.get(kopf.get("art"), "Rechnung")) + "proforma": "Proforma-Rechnung"}.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 - # Ein Angebot bekommt KEIN Blatt fuers Amt: gemeldet wird eine + # Eine Proforma bekommt KEIN Blatt fuers Amt: gemeldet wird eine # Uebernachtung erst, wenn sie abgerechnet ist. - if rechnung.aufschlaege and kopf.get("art") != "angebot": + if rechnung.aufschlaege and kopf.get("art") != "proforma": story.append(PageBreak()) story.append(_AmtMarke(marke)) _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt diff --git a/pruef_angebot.py b/pruef_proforma.py similarity index 74% rename from pruef_angebot.py rename to pruef_proforma.py index d4f8e54..64265ee 100644 --- a/pruef_angebot.py +++ b/pruef_proforma.py @@ -1,23 +1,21 @@ # -*- coding: utf-8 -*- -"""Pruefstand: das unverbindliche Angebot. +"""Pruefstand: die Proforma-Rechnung. -Sein Auftrag: ein Knopf, im Popup waehlt man Rechnung oder Angebot - *"musst +Sein Auftrag: ein Knopf, im Popup waehlt man Rechnung oder Proforma - *"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: das Angebot laeuft in einer EIGENEN -Reihe (2026-A001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt. +Genau das ist hier die Hauptpruefung: die Proforma laeuft in einer EIGENEN +Reihe (2026-P001) 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 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). + * deutlich als "Proforma-Rechnung" kennzeichnen, sonst kann das Finanzamt das + Blatt als echte Rechnung werten Gefahren wird das ECHTE Popup - die Knoepfe werden angeklickt, nicht umgangen. """ @@ -53,7 +51,7 @@ _wecker.daemon = True _wecker.start() os.environ["CAMPINGHOF_TEST"] = "1" -tmp = tempfile.mkdtemp(prefix="angebot_pruef_") +tmp = tempfile.mkdtemp(prefix="proforma_pruef_") ordner = os.path.join(tmp, "rechnungen") os.makedirs(ordner) @@ -134,8 +132,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("Angebot" in t for t in knopf_texte), - "und kein zweiter Knopf fuers Angebot - gewaehlt wird im Popup") +pruefe(not any("Proforma" in t for t in knopf_texte), + "und kein zweiter Knopf fuer die Proforma - gewaehlt wird im Popup") kunde("Anna", "Vorab") nummer_vorher = f.r_nummer.get() @@ -147,41 +145,33 @@ klicke_im_popup("Abbrechen") f.pdf_erstellen() pruefe(not os.listdir(ordner), f"Abbrechen schreibt nichts ({os.listdir(ordner)})") -# ---- 3. Angebot waehlen -------------------------------------------------- -klicke_im_popup("Angebot") +# ---- 3. Proforma waehlen ------------------------------------------------- +klicke_im_popup("Proforma") f.pdf_erstellen() -pruefe(sorted(os.listdir(ordner)) == ["Angebote"], - f"im Rechnungsordner liegt nur der Unterordner ({sorted(os.listdir(ordner))})") -angebote = os.path.join(ordner, "Angebote") -dateien = sorted(os.listdir(angebote)) -pruefe(dateien == ["Angebot_2026-A001_Vorab.pdf"], - f"das Angebot liegt im Unterordner Angebote ({dateien})") +dateien = sorted(os.listdir(ordner)) +pruefe(dateien == ["Proforma_2026-P001_Vorab.pdf"], f"die Proforma 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("angebot_zaehler") == 1, - f"nur der Angebots-Zaehler ist weitergelaufen ({f.cfg.get('angebot_zaehler')})") +pruefe(f.cfg.get("proforma_zaehler") == 1, + f"nur der Proforma-Zaehler ist weitergelaufen ({f.cfg.get('proforma_zaehler')})") # ---- 5. Was auf dem Blatt steht ----------------------------------------- from pypdf import PdfReader # noqa: E402 -pfad = os.path.join(angebote, "Angebot_2026-A001_Vorab.pdf") +pfad = os.path.join(ordner, "Proforma_2026-P001_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("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("Proforma-Rechnung" in text, "es steht deutlich Proforma-Rechnung drauf") pruefe("keine Rechnung im Sinne des § 14 UStG" in text, "mit dem Satz, dass es keine Rechnung nach § 14 UStG ist") -pruefe("Angebotsnummer" in text, "die Nummer heisst Angebotsnummer") -pruefe("Rechnungsnummer" not in text, - "das Wort Rechnungsnummer kommt gar nicht mehr vor") -pruefe("2026-A001" in text and "2026-001" not in text, - "nur die Angebots-Nummer steht drauf, keine Rechnungsnummer") +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("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, @@ -192,7 +182,7 @@ pruefe("Voraussichtlicher Gesamtbetrag" in text, "der Betrag heisst voraussichtlich, nicht Rechnungsbetrag") meta = BE.kenndaten_lesen(pfad) or {} -pruefe(meta.get("art") == "angebot", f"art = angebot ({meta.get('art')})") +pruefe(meta.get("art") == "proforma", f"art = proforma ({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')})") @@ -201,20 +191,20 @@ antworten["Storno"] = True klicke_im_popup("Rechnung 2026-001 erstellen") f.pdf_erstellen() pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-001_Vorab.pdf")), - f"die Rechnung liegt bei den Rechnungen ({sorted(os.listdir(ordner))})") + f"die Rechnung 2026-001 ist geschrieben ({sorted(os.listdir(ordner))})") pruefe(f.cfg.get("rechnung_zaehler") == 1, f"jetzt erst laeuft der Rechnungszaehler weiter ({f.cfg.get('rechnung_zaehler')})") -# ---- 7. Das zweite Angebot zaehlt in seiner Reihe weiter --------------- +# ---- 7. Die zweite Proforma zaehlt in ihrer Reihe weiter --------------- f.reset_formular() kunde("Bernd", "Zweitvorab") -klicke_im_popup("Angebot") +klicke_im_popup("Proforma") f.pdf_erstellen() -pruefe(os.path.exists(os.path.join(angebote, "Angebot_2026-A002_Zweitvorab.pdf")), - f"das zweite Angebot heisst A002 ({sorted(os.listdir(angebote))})") +pruefe(os.path.exists(os.path.join(ordner, "Proforma_2026-P002_Zweitvorab.pdf")), + f"die zweite Proforma heisst P002 ({sorted(os.listdir(ordner))})") f.destroy() -# ---- 8. Das Steuerjournal bucht das Angebot NICHT --------------------- +# ---- 8. Das Steuerjournal bucht die Proforma NICHT -------------------- sys.path.append(JOURNAL) import db as BST # noqa: E402 @@ -224,8 +214,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("A0" in b.voll_nummer for b in buchungen), - "keine Angebots-Nummer im Journal") +pruefe(not any("P0" in b.voll_nummer for b in buchungen), + "keine Proforma-Nummer im Journal") journal.schliessen() if not fehler: