Compare commits
No commits in common. "ad1ac9af6f0eb5ed57b0e761c915dcabee95d4bd" and "94c2c7881a10444f108ced13d80adf9feb6e801c" have entirely different histories.
ad1ac9af6f
...
94c2c7881a
4 changed files with 96 additions and 124 deletions
84
app.py
84
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)]
|
for name, wert in felder if wert and not parse_date(wert)]
|
||||||
|
|
||||||
def _art_waehlen(self, nummer, betrag, wer):
|
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
|
Ersetzt die fruehere Sicherheitsfrage - EIN Fenster statt zwei. Die
|
||||||
Warnung, dass eine ausgestellte Rechnung nicht mehr zu aendern ist,
|
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.
|
# setzt _dialog_zeigen und drueckt die Knoepfe wirklich.
|
||||||
if TESTLAUF and not getattr(self, "_dialog_zeigen", False):
|
if TESTLAUF and not getattr(self, "_dialog_zeigen", False):
|
||||||
# _testwahl: was der Pruefstand im Fenster geklickt haette.
|
# _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")
|
return getattr(self, "_testwahl", "rechnung")
|
||||||
d = tk.Toplevel(self)
|
d = tk.Toplevel(self)
|
||||||
d.title("Was soll erstellt werden?")
|
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(
|
"bucht sie und meldet die Übernachtungen ans Amt.")).grid(
|
||||||
row=1, column=0, sticky="w", pady=(10, 8))
|
row=1, column=0, sticky="w", pady=(10, 8))
|
||||||
ttk.Label(rahmen, wraplength=560, justify="left", text=(
|
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. "
|
"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(
|
f"gebucht – die nächste Rechnung bleibt {nummer}.")).grid(
|
||||||
row=2, column=0, sticky="w", pady=(0, 14))
|
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")
|
knoepfe.grid(row=3, column=0, sticky="ew")
|
||||||
ttk.Button(knoepfe, text=f"Rechnung {nummer} erstellen", width=28,
|
ttk.Button(knoepfe, text=f"Rechnung {nummer} erstellen", width=28,
|
||||||
command=lambda: nimm("rechnung")).pack(side="left")
|
command=lambda: nimm("rechnung")).pack(side="left")
|
||||||
ttk.Button(knoepfe, text="Angebot erstellen", width=20,
|
ttk.Button(knoepfe, text="Proforma-Rechnung", width=20,
|
||||||
command=lambda: nimm("angebot")).pack(side="left", padx=8)
|
command=lambda: nimm("proforma")).pack(side="left", padx=8)
|
||||||
ttk.Button(knoepfe, text="Abbrechen", width=14,
|
ttk.Button(knoepfe, text="Abbrechen", width=14,
|
||||||
command=d.destroy).pack(side="right")
|
command=d.destroy).pack(side="right")
|
||||||
|
|
||||||
|
|
@ -899,46 +899,30 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
self.wait_window(d)
|
self.wait_window(d)
|
||||||
return wahl["art"]
|
return wahl["art"]
|
||||||
|
|
||||||
def _angebot_ordner(self):
|
def _naechste_proforma_nummer(self):
|
||||||
"""Unterordner "Angebote" neben den Rechnungen.
|
"""JJJJ-Pnnn aus der EIGENEN Reihe. Der Jahreswechsel faengt bei P001 an.
|
||||||
|
|
||||||
Angebote sind keine Buchhaltung - im Rechnungsordner haetten sie nur
|
Wichtig: hier wird der Rechnungszaehler nicht angefasst. Eine Proforma
|
||||||
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
|
|
||||||
darf keine Rechnungsnummer verbrauchen - sonst muesste man die Nummern
|
darf keine Rechnungsnummer verbrauchen - sonst muesste man die Nummern
|
||||||
wieder von Hand aendern koennen, und genau daher kamen die doppelten.
|
wieder von Hand aendern koennen, und genau daher kamen die doppelten.
|
||||||
"""
|
"""
|
||||||
jahr = datetime.date.today().year
|
jahr = datetime.date.today().year
|
||||||
zaehler = int(self.cfg.get("angebot_zaehler", 0) or 0)
|
zaehler = int(self.cfg.get("proforma_zaehler", 0) or 0)
|
||||||
if int(self.cfg.get("angebot_jahr", 0) or 0) != jahr:
|
if int(self.cfg.get("proforma_jahr", 0) or 0) != jahr:
|
||||||
zaehler = 0
|
zaehler = 0
|
||||||
ordner = self._angebot_ordner()
|
out_dir = config.ensure_output_dir(self.cfg)
|
||||||
try:
|
try:
|
||||||
vorhanden = os.listdir(ordner)
|
vorhanden = os.listdir(out_dir)
|
||||||
except OSError:
|
except OSError:
|
||||||
vorhanden = []
|
vorhanden = []
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
zaehler += 1
|
zaehler += 1
|
||||||
nummer = f"{jahr}-A{zaehler:03d}"
|
nummer = f"{jahr}-P{zaehler:03d}"
|
||||||
if not any(d.startswith(f"Angebot_{nummer}") for d in vorhanden):
|
if not any(d.startswith(f"Proforma_{nummer}") for d in vorhanden):
|
||||||
return jahr, zaehler, nummer
|
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()
|
montage = self.ist_montage()
|
||||||
r = self.recalc()
|
r = self.recalc()
|
||||||
if not r.zeilen:
|
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)
|
wahl = self._art_waehlen(nummer, berechnung.eur(r.gesamt), wer)
|
||||||
if wahl is None:
|
if wahl is None:
|
||||||
return
|
return
|
||||||
angebot = wahl == "angebot"
|
proforma = wahl == "proforma"
|
||||||
if angebot:
|
if proforma:
|
||||||
# EIGENE Reihe: Nummernfeld und Rechnungszaehler bleiben
|
# EIGENE Reihe: Nummernfeld und Rechnungszaehler bleiben
|
||||||
# unangetastet, die naechste Rechnung behaelt ihre Nummer.
|
# 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;
|
# Auf der Rechnung stehen Vor- und Nachname wie gewohnt in einer Zeile;
|
||||||
# der Nachname geht zusätzlich getrennt mit, damit das Steuerjournal ihn
|
# 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.
|
# dahinter ist eine ganz normale: Zwischensumme + Beherbergungssteuer.
|
||||||
kopf["vorlage"] = "montage"
|
kopf["vorlage"] = "montage"
|
||||||
kopf["montage_text"] = self._montage_text()
|
kopf["montage_text"] = self._montage_text()
|
||||||
if angebot:
|
if proforma:
|
||||||
kopf["art"] = "angebot"
|
kopf["art"] = "proforma"
|
||||||
kopf["rechnungsnummer"] = nummer
|
kopf["rechnungsnummer"] = nummer
|
||||||
if stand:
|
if stand:
|
||||||
kopf["art"] = "korrektur"
|
kopf["art"] = "korrektur"
|
||||||
|
|
@ -1156,32 +1140,32 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
self.r_nummer.set(nummer)
|
self.r_nummer.set(nummer)
|
||||||
kopf["rechnungsnummer"] = nummer
|
kopf["rechnungsnummer"] = nummer
|
||||||
|
|
||||||
if angebot:
|
if proforma:
|
||||||
name = f"Angebot_{safe_filename(nummer)}"
|
name = f"Proforma_{safe_filename(nummer)}"
|
||||||
if self.k_name.get().strip():
|
if self.k_name.get().strip():
|
||||||
name += "_" + safe_filename(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:
|
try:
|
||||||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||||||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Fehler beim Erstellen",
|
"Fehler beim Erstellen",
|
||||||
f"Angebot konnte nicht erstellt werden:\n{e}")
|
f"Proforma konnte nicht erstellt werden:\n{e}")
|
||||||
print("[angebot] FEHLER:", e)
|
print("[proforma] FEHLER:", e)
|
||||||
return
|
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
|
# Zaehler, das gemeinsame Nummernbuch und die Meldung ans
|
||||||
# Steuerjournal bleiben unberuehrt - gebucht wird ein Angebot
|
# Steuerjournal bleiben unberuehrt - gebucht wird eine Proforma
|
||||||
# nirgends.
|
# nirgends.
|
||||||
self.cfg["angebot_jahr"] = a_jahr
|
self.cfg["proforma_jahr"] = p_jahr
|
||||||
self.cfg["angebot_zaehler"] = a_zaehler
|
self.cfg["proforma_zaehler"] = p_zaehler
|
||||||
try:
|
try:
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
except OSError as e: # noqa: BLE001
|
except OSError as e: # noqa: BLE001
|
||||||
print("[angebot] Zaehler nicht gespeichert:", e)
|
print("[proforma] Zaehler nicht gespeichert:", e)
|
||||||
print(f"[angebot] erstellt -> {pfad}")
|
print(f"[proforma] erstellt -> {pfad}")
|
||||||
if messagebox.askyesno(
|
if messagebox.askyesno(
|
||||||
"Fertig", f"Angebot erstellt:\n{pfad}\n\n"
|
"Fertig", f"Proforma-Rechnung erstellt:\n{pfad}\n\n"
|
||||||
"Jetzt öffnen (zum Drucken)?"):
|
"Jetzt öffnen (zum Drucken)?"):
|
||||||
self._oeffne_datei(pfad)
|
self._oeffne_datei(pfad)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,10 @@ DEFAULT_CONFIG = {
|
||||||
"texte": DEFAULT_TEXTE,
|
"texte": DEFAULT_TEXTE,
|
||||||
"katalog": DEFAULT_KATALOG,
|
"katalog": DEFAULT_KATALOG,
|
||||||
"montage": DEFAULT_MONTAGE,
|
"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.
|
# Rechnungsnummer verbrauchen - die gehoert zu einer echten Rechnung.
|
||||||
"angebot_jahr": 0,
|
"proforma_jahr": 0,
|
||||||
"angebot_zaehler": 0,
|
"proforma_zaehler": 0,
|
||||||
# Alle Steuern als EINE benannte Liste.
|
# Alle Steuern als EINE benannte Liste.
|
||||||
# - extra_blatt = False: USt-Satz, wird auf das Netto der zugeordneten Leistungen gerechnet.
|
# - 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.
|
# - 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))
|
story.append(Spacer(1, 5 * mm))
|
||||||
|
|
||||||
# Kundenadresse (links) | Rechnungsinfos (rechts)
|
# 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"])]
|
else "Rechnung an:", st["addr_label"])]
|
||||||
for z in kopf.get("kunde_zeilen", []):
|
for z in kopf.get("kunde_zeilen", []):
|
||||||
if str(z).strip():
|
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
|
# Keine Positionstabelle, keine Naechte im Kopf - der Zeitraum steht in der
|
||||||
# einen Textzeile, so wie Bartl das Blatt bisher von Hand geschrieben hat.
|
# einen Textzeile, so wie Bartl das Blatt bisher von Hand geschrieben hat.
|
||||||
montage = kopf.get("vorlage") == "montage"
|
montage = kopf.get("vorlage") == "montage"
|
||||||
angebot = art == "angebot"
|
proforma = art == "proforma"
|
||||||
info_rows = [
|
info_rows = [
|
||||||
("Stornonummer:" if art == "storno" else
|
("Stornonummer:" if art == "storno" else
|
||||||
"Angebotsnummer:" if angebot else "Rechnungsnummer:",
|
"Proforma-Nr.:" if proforma else "Rechnungsnummer:",
|
||||||
kopf.get("rechnungsnummer", "")),
|
kopf.get("rechnungsnummer", "")),
|
||||||
# Auf dem Angebot faellt das Wort "Rechnung" ueberall weg: es soll gar
|
("Rechnungsdatum:", kopf.get("datum", "")),
|
||||||
# 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.
|
# Der Bezug auf die alte Rechnung ist Pflicht (§ 31 Abs. 5 UStDV): Nummer UND Datum.
|
||||||
if art == "storno" and kopf.get("storno_zu"):
|
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.
|
# eindeutig als Aufhebung erkennbar ist.
|
||||||
titel = {"storno": "Stornorechnung",
|
titel = {"storno": "Stornorechnung",
|
||||||
"berichtigung": "Berichtigte Rechnung",
|
"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 ""
|
titel += " (für das Amt)" if gesamtrechnung_betont else ""
|
||||||
story.append(Paragraph(titel, st["h"]))
|
story.append(Paragraph(titel, st["h"]))
|
||||||
if angebot:
|
if proforma:
|
||||||
# Ohne diesen Satz sieht das Blatt aus wie eine Rechnung. Es ist keine -
|
# Ohne diesen Satz sieht das Blatt aus wie eine Rechnung. Es ist keine -
|
||||||
# und ohne die deutliche Kennzeichnung kann das Finanzamt es als eine
|
# und ohne die deutliche Kennzeichnung kann das Finanzamt es als eine
|
||||||
# werten. Ein Steuersatz oder Steuerbetrag steht bewusst NIRGENDS auf
|
# werten. Ein Steuersatz oder Steuerbetrag steht bewusst NIRGENDS auf
|
||||||
# dem Blatt: wer Steuer ausweist, schuldet sie (§ 14c UStG).
|
# dem Blatt: wer Steuer ausweist, schuldet sie (§ 14c UStG).
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
"<b>Dies ist ein unverbindliches Angebot</b> und <b>keine Rechnung im "
|
"<b>Dies ist eine Proforma-Rechnung</b> – eine unverbindliche Vorab-"
|
||||||
"Sinne des § 14 UStG</b>: es berechtigt nicht zum Vorsteuerabzug und ist "
|
"Aufstellung der Kosten. Sie ist <b>keine Rechnung im Sinne des § 14 "
|
||||||
"nicht zu bezahlen. Alle Beträge in Euro inkl. gesetzlicher "
|
"UStG</b>, berechtigt nicht zum Vorsteuerabzug und ist nicht zu bezahlen. "
|
||||||
"Umsatzsteuer; ausgewiesen wird "
|
"Die Nummer oben ist <b>keine Rechnungsnummer</b>. Alle Beträge in Euro "
|
||||||
"sie erst in der Rechnung nach dem Aufenthalt.", st["dank"]))
|
"inkl. gesetzlicher Umsatzsteuer; ausgewiesen wird sie erst in der "
|
||||||
|
"Rechnung nach dem Aufenthalt.", st["dank"]))
|
||||||
story.append(Spacer(1, 3 * mm))
|
story.append(Spacer(1, 3 * mm))
|
||||||
|
|
||||||
if montage:
|
if montage:
|
||||||
_montage_zeile(story, st, rechnung, kopf)
|
_montage_zeile(story, st, rechnung, kopf)
|
||||||
else:
|
else:
|
||||||
_positionstabelle(story, st, rechnung, ohne_ust=angebot)
|
_positionstabelle(story, st, rechnung, ohne_ust=proforma)
|
||||||
story.append(Spacer(1, 3 * mm))
|
story.append(Spacer(1, 3 * mm))
|
||||||
|
|
||||||
# ---- Summenblock: Netto -> USt je Satz -> Zwischensumme (brutto)
|
# ---- Summenblock: Netto -> USt je Satz -> Zwischensumme (brutto)
|
||||||
# -> zzgl. Beherbergungssteuer -> Gesamtbetrag ----
|
# -> 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
|
# sie erst mit der Rechnung. Die Beherbergungssteuer ist keine Umsatzsteuer
|
||||||
# und darf bleiben - der Gast soll sehen, was er zahlen wird.
|
# 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("Nettobetrag:", st["info_l"]),
|
||||||
Paragraph(eur(rechnung.netto_gesamt), st["cell_r"]),
|
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"
|
name = b.bezeichnung or "USt"
|
||||||
summen_rows.append([
|
summen_rows.append([
|
||||||
Paragraph(f"{name} {b.satz} %:", st["info_l"]),
|
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
|
# 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.
|
# 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
|
# noch Beherbergungssteuer. Es steht nur, was der Gast voraussichtlich
|
||||||
# zahlt; aufgeschluesselt wird erst in der Rechnung.
|
# 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([
|
summen_rows.append([
|
||||||
Paragraph("Zwischensumme:", st["info_l"]),
|
Paragraph("Zwischensumme:", st["info_l"]),
|
||||||
Paragraph(eur(rechnung.brutto), st["cell_r"]),
|
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
|
endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt
|
||||||
summen_rows.append([
|
summen_rows.append([
|
||||||
Paragraph("Voraussichtlicher Gesamtbetrag:" if angebot else
|
Paragraph("Voraussichtlicher Gesamtbetrag:" if proforma else
|
||||||
"Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:",
|
"Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:",
|
||||||
st["total_l"]),
|
st["total_l"]),
|
||||||
Paragraph(eur(endsumme), st["total_v"]),
|
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
|
# Hinweis zur Beherbergungssteuer - nur auf der Kundenseite. Auf dem Amt-Blatt
|
||||||
# wird die Steuer gar nicht mehr ausgewiesen, dann waere der Satz sinnlos.
|
# 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(Spacer(1, 1.5 * mm))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
"In diesem Betrag sind alle Steuern und Abgaben enthalten. Aufgeschlüsselt "
|
"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"]))
|
"ihre Nummer.", st["dank"]))
|
||||||
if texte.get("dank1"):
|
if texte.get("dank1"):
|
||||||
block.append(Paragraph(texte["dank1"], st["dank"]))
|
block.append(Paragraph(texte["dank1"], st["dank"]))
|
||||||
elif angebot:
|
elif proforma:
|
||||||
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
|
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
|
||||||
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
|
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
|
||||||
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
|
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
|
||||||
block.append(Paragraph(
|
block.append(Paragraph(
|
||||||
"Dieses Angebot ist freibleibend und unverbindlich (§ 145 BGB) und "
|
"Diese Aufstellung ist unverbindlich und dient nur zur Information. "
|
||||||
"keine Zahlungsaufforderung. Sagen Sie uns Bescheid, dann reservieren "
|
"Sie ist keine Zahlungsaufforderung – die Rechnung erhalten Sie nach "
|
||||||
"wir – die Rechnung erhalten Sie nach dem Aufenthalt. Für Rückfragen "
|
"dem Aufenthalt. Für Rückfragen stehen wir gern zur Verfügung.",
|
||||||
"stehen wir gern zur Verfügung.",
|
|
||||||
st["dank"]))
|
st["dank"]))
|
||||||
else:
|
else:
|
||||||
if art == "korrektur" and kopf.get("ersetzt"):
|
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).
|
Steuerrechner summieren dadurch von allein richtig (+100 -100 +120 = 120).
|
||||||
"""
|
"""
|
||||||
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
|
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
|
||||||
if kopf.get("art") == "angebot":
|
if kopf.get("art") == "proforma":
|
||||||
# Ein Angebot ist kein Umsatz - es traegt Nullen. Ein Programm, das
|
# Eine Proforma ist kein Umsatz - sie traegt Nullen. Ein Programm, das
|
||||||
# "art" nicht kennt, zaehlt dann 0,00 statt einer Uebernachtung, die nie
|
# "art" nicht kennt, zaehlt dann 0,00 statt einer Uebernachtung, die nie
|
||||||
# abgerechnet wurde.
|
# abgerechnet wurde.
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"quelle": "rechnungstool", "version": 3, "art": "angebot",
|
"quelle": "rechnungstool", "version": 3, "art": "proforma",
|
||||||
"rechnungsnummer": kopf.get("rechnungsnummer", ""),
|
"rechnungsnummer": kopf.get("rechnungsnummer", ""),
|
||||||
"datum": kopf.get("datum", ""),
|
"datum": kopf.get("datum", ""),
|
||||||
"nachname": kopf.get("nachname", ""), "vorname": kopf.get("vorname", ""),
|
"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,
|
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
|
||||||
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
||||||
title=({"storno": "Stornorechnung",
|
title=({"storno": "Stornorechnung",
|
||||||
"angebot": "Angebot"}.get(kopf.get("art"), "Rechnung"))
|
"proforma": "Proforma-Rechnung"}.get(kopf.get("art"), "Rechnung"))
|
||||||
+ f" {kopf.get('rechnungsnummer','')}",
|
+ f" {kopf.get('rechnungsnummer','')}",
|
||||||
author=cfg["firma"].get("name", ""),
|
author=cfg["firma"].get("name", ""),
|
||||||
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
||||||
)
|
)
|
||||||
story = []
|
story = []
|
||||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Kunde
|
_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.
|
# 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(PageBreak())
|
||||||
story.append(_AmtMarke(marke))
|
story.append(_AmtMarke(marke))
|
||||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
|
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
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
|
muessen wir wieder die nummern aenderbar machen und dann macht er wieder
|
||||||
fehler"*.
|
fehler"*.
|
||||||
|
|
||||||
Genau das ist hier die Hauptpruefung: das Angebot laeuft in einer EIGENEN
|
Genau das ist hier die Hauptpruefung: die Proforma laeuft in einer EIGENEN
|
||||||
Reihe (2026-A001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt.
|
Reihe (2026-P001) und laesst Nummernfeld, Zaehler und Nummernbuch unberuehrt.
|
||||||
|
|
||||||
Dazu die Rechtslage (nachgeschlagen am 03.09.2026):
|
Dazu die Rechtslage (nachgeschlagen am 03.09.2026):
|
||||||
* keine Nummer aus dem regulaeren Kreis - die fortlaufende Nummerierung nach
|
* keine Nummer aus dem regulaeren Kreis - die fortlaufende Nummerierung nach
|
||||||
§ 14 Abs. 4 Nr. 4 UStG gilt nur fuer echte Rechnungen
|
§ 14 Abs. 4 Nr. 4 UStG gilt nur fuer echte Rechnungen
|
||||||
* kein Steuersatz und kein Steuerbetrag auf dem Blatt (§ 14c UStG: wer Steuer
|
* kein Steuersatz und kein Steuerbetrag auf dem Blatt (§ 14c UStG: wer Steuer
|
||||||
ausweist, schuldet sie)
|
ausweist, schuldet sie)
|
||||||
* deutlich kennzeichnen, sonst kann das Finanzamt das Blatt als echte Rechnung
|
* deutlich als "Proforma-Rechnung" kennzeichnen, sonst kann das Finanzamt das
|
||||||
werten (§ 14 Abs. 1 S. 1 UStG: die Bezeichnung ist gleichgueltig). "Angebot"
|
Blatt als echte Rechnung werten
|
||||||
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.
|
Gefahren wird das ECHTE Popup - die Knoepfe werden angeklickt, nicht umgangen.
|
||||||
"""
|
"""
|
||||||
|
|
@ -53,7 +51,7 @@ _wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
os.environ["CAMPINGHOF_TEST"] = "1"
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
tmp = tempfile.mkdtemp(prefix="angebot_pruef_")
|
tmp = tempfile.mkdtemp(prefix="proforma_pruef_")
|
||||||
ordner = os.path.join(tmp, "rechnungen")
|
ordner = os.path.join(tmp, "rechnungen")
|
||||||
os.makedirs(ordner)
|
os.makedirs(ordner)
|
||||||
|
|
||||||
|
|
@ -134,8 +132,8 @@ def kunde(vorname, nachname):
|
||||||
# ---- 1. Der eine Knopf und das Popup --------------------------------------
|
# ---- 1. Der eine Knopf und das Popup --------------------------------------
|
||||||
knopf_texte = [str(k.cget("text")) for k in knoepfe_von(f)]
|
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("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),
|
pruefe(not any("Proforma" in t for t in knopf_texte),
|
||||||
"und kein zweiter Knopf fuers Angebot - gewaehlt wird im Popup")
|
"und kein zweiter Knopf fuer die Proforma - gewaehlt wird im Popup")
|
||||||
|
|
||||||
kunde("Anna", "Vorab")
|
kunde("Anna", "Vorab")
|
||||||
nummer_vorher = f.r_nummer.get()
|
nummer_vorher = f.r_nummer.get()
|
||||||
|
|
@ -147,41 +145,33 @@ klicke_im_popup("Abbrechen")
|
||||||
f.pdf_erstellen()
|
f.pdf_erstellen()
|
||||||
pruefe(not os.listdir(ordner), f"Abbrechen schreibt nichts ({os.listdir(ordner)})")
|
pruefe(not os.listdir(ordner), f"Abbrechen schreibt nichts ({os.listdir(ordner)})")
|
||||||
|
|
||||||
# ---- 3. Angebot waehlen --------------------------------------------------
|
# ---- 3. Proforma waehlen -------------------------------------------------
|
||||||
klicke_im_popup("Angebot")
|
klicke_im_popup("Proforma")
|
||||||
f.pdf_erstellen()
|
f.pdf_erstellen()
|
||||||
pruefe(sorted(os.listdir(ordner)) == ["Angebote"],
|
dateien = sorted(os.listdir(ordner))
|
||||||
f"im Rechnungsordner liegt nur der Unterordner ({sorted(os.listdir(ordner))})")
|
pruefe(dateien == ["Proforma_2026-P001_Vorab.pdf"], f"die Proforma liegt da ({dateien})")
|
||||||
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})")
|
|
||||||
|
|
||||||
# ---- 4. DIE Hauptsache: die Rechnungsnummer ist unberuehrt --------------
|
# ---- 4. DIE Hauptsache: die Rechnungsnummer ist unberuehrt --------------
|
||||||
pruefe(f.r_nummer.get() == nummer_vorher,
|
pruefe(f.r_nummer.get() == nummer_vorher,
|
||||||
f"das Nummernfeld steht unveraendert auf {nummer_vorher} ({f.r_nummer.get()})")
|
f"das Nummernfeld steht unveraendert auf {nummer_vorher} ({f.r_nummer.get()})")
|
||||||
pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher,
|
pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher,
|
||||||
f"der Rechnungszaehler ist unveraendert ({f.cfg.get('rechnung_zaehler')})")
|
f"der Rechnungszaehler ist unveraendert ({f.cfg.get('rechnung_zaehler')})")
|
||||||
pruefe(f.cfg.get("angebot_zaehler") == 1,
|
pruefe(f.cfg.get("proforma_zaehler") == 1,
|
||||||
f"nur der Angebots-Zaehler ist weitergelaufen ({f.cfg.get('angebot_zaehler')})")
|
f"nur der Proforma-Zaehler ist weitergelaufen ({f.cfg.get('proforma_zaehler')})")
|
||||||
|
|
||||||
# ---- 5. Was auf dem Blatt steht -----------------------------------------
|
# ---- 5. Was auf dem Blatt steht -----------------------------------------
|
||||||
from pypdf import PdfReader # noqa: E402
|
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
|
seiten = PdfReader(pfad).pages
|
||||||
pruefe(len(seiten) == 1, f"eine Seite - kein Blatt fuers Amt ({len(seiten)})")
|
pruefe(len(seiten) == 1, f"eine Seite - kein Blatt fuers Amt ({len(seiten)})")
|
||||||
text = (seiten[0].extract_text() or "").replace("\n", " ")
|
text = (seiten[0].extract_text() or "").replace("\n", " ")
|
||||||
pruefe("Unverbindliches Angebot" in text, "es steht deutlich Unverbindliches Angebot drauf")
|
pruefe("Proforma-Rechnung" in text, "es steht deutlich Proforma-Rechnung 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,
|
pruefe("keine Rechnung im Sinne des § 14 UStG" in text,
|
||||||
"mit dem Satz, dass es keine Rechnung nach § 14 UStG ist")
|
"mit dem Satz, dass es keine Rechnung nach § 14 UStG ist")
|
||||||
pruefe("Angebotsnummer" in text, "die Nummer heisst Angebotsnummer")
|
pruefe("keine Rechnungsnummer" in text, "und dass die Nummer keine Rechnungsnummer ist")
|
||||||
pruefe("Rechnungsnummer" not in text,
|
pruefe("2026-P001" in text and "2026-001" not in text,
|
||||||
"das Wort Rechnungsnummer kommt gar nicht mehr vor")
|
"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,
|
pruefe("Mehrwertsteuer" not in text and "Nettobetrag" not in text,
|
||||||
"kein Steuerbetrag und kein Nettoausweis (§ 14c UStG)")
|
"kein Steuerbetrag und kein Nettoausweis (§ 14c UStG)")
|
||||||
pruefe("Beherbergungssteuer" not in text and "Zwischensumme" not in text,
|
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")
|
"der Betrag heisst voraussichtlich, nicht Rechnungsbetrag")
|
||||||
|
|
||||||
meta = BE.kenndaten_lesen(pfad) or {}
|
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,
|
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')})")
|
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")
|
klicke_im_popup("Rechnung 2026-001 erstellen")
|
||||||
f.pdf_erstellen()
|
f.pdf_erstellen()
|
||||||
pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-001_Vorab.pdf")),
|
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,
|
pruefe(f.cfg.get("rechnung_zaehler") == 1,
|
||||||
f"jetzt erst laeuft der Rechnungszaehler weiter ({f.cfg.get('rechnung_zaehler')})")
|
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()
|
f.reset_formular()
|
||||||
kunde("Bernd", "Zweitvorab")
|
kunde("Bernd", "Zweitvorab")
|
||||||
klicke_im_popup("Angebot")
|
klicke_im_popup("Proforma")
|
||||||
f.pdf_erstellen()
|
f.pdf_erstellen()
|
||||||
pruefe(os.path.exists(os.path.join(angebote, "Angebot_2026-A002_Zweitvorab.pdf")),
|
pruefe(os.path.exists(os.path.join(ordner, "Proforma_2026-P002_Zweitvorab.pdf")),
|
||||||
f"das zweite Angebot heisst A002 ({sorted(os.listdir(angebote))})")
|
f"die zweite Proforma heisst P002 ({sorted(os.listdir(ordner))})")
|
||||||
f.destroy()
|
f.destroy()
|
||||||
|
|
||||||
# ---- 8. Das Steuerjournal bucht das Angebot NICHT ---------------------
|
# ---- 8. Das Steuerjournal bucht die Proforma NICHT --------------------
|
||||||
sys.path.append(JOURNAL)
|
sys.path.append(JOURNAL)
|
||||||
import db as BST # noqa: E402
|
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(len(buchungen) == 1, f"nur die echte Rechnung ist gebucht ({len(buchungen)})")
|
||||||
pruefe(buchungen and buchungen[0].voll_nummer == "2026-001",
|
pruefe(buchungen and buchungen[0].voll_nummer == "2026-001",
|
||||||
f"und zwar 2026-001 ({[b.voll_nummer for b in buchungen]})")
|
f"und zwar 2026-001 ({[b.voll_nummer for b in buchungen]})")
|
||||||
pruefe(not any("A0" in b.voll_nummer for b in buchungen),
|
pruefe(not any("P0" in b.voll_nummer for b in buchungen),
|
||||||
"keine Angebots-Nummer im Journal")
|
"keine Proforma-Nummer im Journal")
|
||||||
journal.schliessen()
|
journal.schliessen()
|
||||||
|
|
||||||
if not fehler:
|
if not fehler:
|
||||||
Loading…
Add table
Add a link
Reference in a new issue