Beide Programme stehen nebeneinander offen. Bisher haette das Journal die neue Rechnung erst beim naechsten Einlesen gesehen. Kein Absuchen des Rechnungsordners und kein Server: nach jeder Rechnung und jedem Storno schaut das Rechnungstool nach, ob das Journal ueberhaupt laeuft - ueber dessen PID, nicht ueber ein Fenster und nicht ueber eine Uhrzeit. Windows beantwortet das genau; PIDs werden wiederverwendet, deshalb wird zusaetzlich der Name der EXE verglichen. Laeuft es, kommt eine Zeile in den Briefkasten. Der Briefkasten IST die gemeinsame SQLite (seine Ansage): zwei Tabellen mehr - "laeuft" (wer ist offen, mit PID und EXE) und "nachrichten". Damit liegt im gemeinsamen Ordner genau EINE Datei. Karteileichen (abgestuerztes Programm) werden beim Nachsehen weggeraeumt, alte Meldungen beim Melden. Ist das Journal zu, passiert nichts - es liest den Ordner beim naechsten Start ohnehin. Die Meldung ist eine Anklopfhilfe, keine Datenquelle. Pruefstand pruef_wache.py (im Steuerjournal, faehrt BEIDE Programme): PID-Pruefung mit toter Nummer und fremdem EXE-Namen, genau eine Datei im Ordner, Meldung nach Rechnung und nach Storno, Einlesen samt Anzeige, Ruhe ohne Meldung, kein Melden an ein geschlossenes Journal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
463 lines
23 KiB
Python
463 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Storno- und Berichtigungsweg.
|
||
|
||
Zwei Wege, ein Popup-Bauplan:
|
||
Storno - falsche Betraege. Stornorechnung mit eigener neuer Nummer,
|
||
danach die berichtigte Rechnung. Zwei Nummern verbraucht.
|
||
Berichtigung - nur die Kundenangaben stimmen nicht. Die Rechnung behaelt
|
||
Nummer, Datum und Betraege (§ 31 Abs. 5 UStDV), es wird nur
|
||
ein berichtigtes Blatt nachgereicht. Keine Nummer verbraucht.
|
||
|
||
Die alte Rechnung wird in beiden Faellen NIE angefasst. § 146 Abs. 4 AO verbietet
|
||
genau genommen nur Aenderungen, bei denen der urspruengliche Inhalt nicht mehr
|
||
feststellbar ist - ein protokollierter Storno-Vermerk waere also erlaubt. Dieses
|
||
Programm verzichtet trotzdem darauf: ein unberuehrter Ursprungsbeleg ist im
|
||
Pruefungsfall die einfachere Geschichte. Korrigiert wird ueber ein eigenes
|
||
Dokument - so sieht es das Gesetz selbst vor (§ 14 Abs. 6 Nr. 5 UStG,
|
||
§ 31 Abs. 5 UStDV).
|
||
"""
|
||
|
||
import datetime
|
||
import os
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox
|
||
|
||
import berechnung
|
||
import config
|
||
import pdf_renderer
|
||
from bestand import (kundendaten_ermitteln, naechste_freie_nummer,
|
||
naechste_freie_nummer_nach, rechnungen_im_ordner)
|
||
from helfer import kunde_maengel, nummer_normalisieren, parse_num, safe_filename
|
||
from storno import storno_rechnung_bauen
|
||
|
||
|
||
class KorrekturMixin:
|
||
"""Storno und Berichtigung. Erwartet die Felder der RechnungsApp."""
|
||
|
||
# ------------------------------------------------------------- Storno
|
||
|
||
|
||
|
||
def _storno_anzeigen(self):
|
||
"""Hinweiszeile und Sperre der Rechnungsnummer an den Zwischenspeicher anpassen."""
|
||
stand = self.storno_stand
|
||
if stand:
|
||
self.storno_hinweis.set(
|
||
f"Storno {stand['nummer']} zu Rechnung {stand['zu']} liegt bereit.\n"
|
||
f"Beim Erstellen wird noch einmal gefragt.")
|
||
self.nummer_entry.configure(state="readonly")
|
||
elif self.berichtigung_stand:
|
||
self.storno_hinweis.set(
|
||
f"Berichtigung der Rechnung {self.berichtigung_stand['zu']}.\n"
|
||
"Nummer und Beträge bleiben, nur die Kundenangaben ändern sich.")
|
||
self.nummer_entry.configure(state="readonly")
|
||
else:
|
||
self.storno_hinweis.set("")
|
||
self.nummer_entry.configure(state="normal")
|
||
|
||
def _berichtigung_starten(self, meta, out_dir):
|
||
"""Rechnung ins Formular holen; "PDF erstellen" schreibt das berichtigte Blatt."""
|
||
nummer = str(meta.get("rechnungsnummer", ""))
|
||
fehlend = self._formular_aus_meta(meta)
|
||
self.berichtigung_stand = {"zu": nummer, "datum": str(meta.get("datum") or ""),
|
||
"vorgang": str(meta.get("vorgang") or nummer),
|
||
"meta": meta}
|
||
# Die Rechnung behaelt ihre Nummer - genau das ist der Unterschied zum Storno.
|
||
self.r_nummer.set(nummer)
|
||
self.r_datum.set(str(meta.get("datum") or self.r_datum.get()))
|
||
self._storno_anzeigen()
|
||
hinweis = f"\n\nZum Kunden: {fehlend}." if fehlend else ""
|
||
messagebox.showinfo(
|
||
"Rechnung geladen",
|
||
f"Rechnung {nummer} vom {meta.get('datum','')} ist im Formular.\n\n"
|
||
"Jetzt die Kundenangaben richtigstellen und \"PDF erstellen\" drücken – "
|
||
"es entsteht ein berichtigtes Blatt zu dieser Rechnung. Die alte Rechnung "
|
||
f"bleibt liegen.{hinweis}")
|
||
|
||
def _storno_verwerfen(self, grund=""):
|
||
"""Storno rückgängig: PDF löschen, Zähler zurück, Zwischenspeicher leeren.
|
||
|
||
Zulaessig ist das nur hier: die Storno-PDF ist Sekunden alt und hat das
|
||
Haus nie verlassen. Die stornierte Rechnung selbst wird nie angefasst.
|
||
"""
|
||
stand = self.storno_stand
|
||
if not stand:
|
||
return
|
||
try:
|
||
if os.path.exists(stand["pfad"]):
|
||
os.remove(stand["pfad"])
|
||
print(f"[storno] verworfen, PDF gelöscht -> {stand['pfad']}")
|
||
except OSError as e: # noqa: BLE001
|
||
# Abbrechen, NICHT weiterlaufen: sonst liegt das Storno noch im Ordner,
|
||
# der Zähler ist zurückgedreht und die Rechnung gilt als storniert,
|
||
# obwohl der Nutzer sie behalten wollte.
|
||
messagebox.showerror(
|
||
"Storno konnte nicht gelöscht werden",
|
||
f"Die Datei\n{stand['pfad']}\nließ sich nicht löschen:\n{e}\n\n"
|
||
"Ist sie noch in einem PDF-Betrachter offen? Bitte dort schließen "
|
||
"und noch einmal versuchen. Es wurde nichts verändert.")
|
||
return
|
||
self.cfg["rechnung_jahr"] = stand["jahr_vorher"]
|
||
self.cfg["rechnung_zaehler"] = stand["zaehler_vorher"]
|
||
try:
|
||
config.save_config(self.cfg)
|
||
except OSError as e: # noqa: BLE001
|
||
messagebox.showwarning(
|
||
"Zähler nicht gespeichert",
|
||
f"Die Einstellungen ließen sich nicht schreiben:\n{e}\n\n"
|
||
"Bitte im Menü unter „Rechnungsnummer…“ prüfen.")
|
||
# Die Nummer ist wieder frei - auch im gemeinsamen Buch, sonst
|
||
# ueberspringt das Rechnungstool sie fuer immer.
|
||
if self.buch is not None and stand.get("nummer"):
|
||
try:
|
||
self.buch.entfernen(stand["nummer"])
|
||
except Exception as e: # noqa: BLE001
|
||
print("[nummernbuch] Storno nicht ausgetragen:", e)
|
||
self.storno_stand = None
|
||
self._storno_anzeigen()
|
||
self._neue_nummer_vorschlagen()
|
||
if grund:
|
||
print(f"[storno] {grund}")
|
||
|
||
def dlg_storno(self):
|
||
"""Knopf „Storno“ – setzt den Weg auf Storno und öffnet das Popup."""
|
||
self._dlg_korrektur("storno")
|
||
|
||
def dlg_berichtigen(self):
|
||
"""Knopf „Berichtigen“ – setzt den Weg auf Berichtigung und öffnet das Popup."""
|
||
self._dlg_korrektur("berichtigung")
|
||
|
||
def _dlg_korrektur(self, modus):
|
||
"""Popup für beide Wege. Welcher gilt, steht schon beim Öffnen fest.
|
||
|
||
"storno" -> Storno-PDF schreiben, danach die berichtigte Rechnung
|
||
"berichtigung" -> nur die Kundenangaben, Rechnung behält ihre Nummer
|
||
"""
|
||
storno = modus == "storno"
|
||
if self.storno_stand:
|
||
messagebox.showinfo(
|
||
"Storno liegt schon bereit",
|
||
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
||
f"{self.storno_stand['zu']} ist noch offen.\n\n"
|
||
"Erst die berichtigte Rechnung erstellen oder den Storno dort verwerfen.")
|
||
return
|
||
if self.berichtigung_stand:
|
||
messagebox.showinfo(
|
||
"Berichtigung läuft schon",
|
||
f"Die Rechnung {self.berichtigung_stand['zu']} liegt gerade zum Berichtigen "
|
||
"im Formular.\n\nErst „PDF erstellen“ drücken oder das Formular zurücksetzen.")
|
||
return
|
||
|
||
out_dir = config.ensure_output_dir(self.cfg)
|
||
alle = rechnungen_im_ordner(out_dir)
|
||
stornos = {nummer_normalisieren(d.get("storno_zu")): d
|
||
for d in alle if d.get("art") == "storno"}
|
||
stornierte = set(stornos)
|
||
# Zu welchen Rechnungen gibt es schon eine berichtigte Rechnung?
|
||
ersetzte = {nummer_normalisieren(d.get("ersetzt"))
|
||
for d in alle if d.get("art") == "korrektur"}
|
||
offen = [d for d in alle
|
||
if d.get("art") not in ("storno", "berichtigung")
|
||
and nummer_normalisieren(d.get("rechnungsnummer")) not in stornierte]
|
||
if not offen:
|
||
messagebox.showwarning(
|
||
"Nichts zu korrigieren",
|
||
f"Im Ordner\n{out_dir}\nliegt keine Rechnung, die dafür in Frage kommt.")
|
||
return
|
||
|
||
d = tk.Toplevel(self)
|
||
d.title("Rechnung stornieren" if storno else "Rechnung berichtigen")
|
||
d.transient(self)
|
||
d.grab_set()
|
||
self._style_dialog(d)
|
||
frm = ttk.Frame(d, padding=12)
|
||
frm.pack(fill="both", expand=True)
|
||
|
||
ttk.Label(frm, justify="left", wraplength=470, text=(
|
||
"Für falsche Beträge oder Leistungen. Die alte Rechnung bleibt unverändert "
|
||
"liegen – sie darf weder geändert noch gelöscht werden. Es entsteht eine "
|
||
"Stornorechnung mit eigener neuer Nummer, die sich auf sie bezieht, und danach "
|
||
"die berichtigte Rechnung."
|
||
if storno else
|
||
"Wenn nur die Angaben zum Kunden nicht stimmen. Die Rechnung behält Nummer, "
|
||
"Datum und Beträge und bleibt gültig – es wird nur ein berichtigtes Blatt "
|
||
"nachgereicht (§ 31 Abs. 5 UStDV). Für falsche Beträge ist das der falsche Weg, "
|
||
"dafür gibt es den Storno.")).grid(
|
||
row=0, column=0, columnspan=2, sticky="w", pady=(0, 10))
|
||
|
||
ttk.Label(frm, text="Alte Rechnungsnummer").grid(row=1, column=0, sticky="w", pady=3)
|
||
# Bewusst OHNE textvariable: eine StringVar, die nur an einer lokalen
|
||
# Variable haengt, raeumt Python nach dem Dialogaufbau weg - Tk loescht
|
||
# dann die Tcl-Variable und das Feld steht leer da.
|
||
ent = ttk.Entry(frm, width=20)
|
||
ent.grid(row=1, column=1, sticky="w", pady=3, padx=(8, 0))
|
||
ent.focus_set()
|
||
|
||
v_info = tk.StringVar(value="Nummer eintragen – das Programm sucht die Rechnung.")
|
||
ttk.Label(frm, textvariable=v_info, justify="left", wraplength=430).grid(
|
||
row=2, column=0, columnspan=2, sticky="w", pady=(10, 0))
|
||
|
||
# Wird nur sichtbar, wenn eine Nummer MEHRFACH im Ordner liegt. Genau das
|
||
# ist der Altbestand: solange zwei Fenster gleichzeitig laufen konnten,
|
||
# wurde dieselbe Nummer zweimal vergeben, teils als Windows-Kopie mit
|
||
# "(1)" im Namen. Dann darf das Programm nicht raten, welche gemeint ist.
|
||
self._auswahl = tk.Listbox(frm, height=4, exportselection=False,
|
||
bg=self.col_field, fg=self.col_fg,
|
||
selectbackground=self.col_sel, selectforeground=self.col_fg,
|
||
relief="flat", font=("Consolas", 9))
|
||
self._auswahl.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(8, 0))
|
||
self._auswahl.grid_remove()
|
||
self._kandidaten = []
|
||
|
||
def kandidaten_zur_nummer(nr):
|
||
return [x for x in alle
|
||
if nummer_normalisieren(x.get("rechnungsnummer")) == nr
|
||
and x.get("art") != "berichtigung"]
|
||
|
||
def suche():
|
||
"""(Rechnung, Meldung) zur eingetippten Nummer. Rechnung=None -> Meldung sagt warum."""
|
||
nr = nummer_normalisieren(ent.get())
|
||
if not nr:
|
||
self._kandidaten = []
|
||
self._auswahl.grid_remove()
|
||
return None, "Nummer eintragen – das Programm sucht die Rechnung."
|
||
gefunden = kandidaten_zur_nummer(nr)
|
||
if not gefunden:
|
||
self._kandidaten = []
|
||
self._auswahl.grid_remove()
|
||
return None, f"Zu {nr} liegt in\n{out_dir}\nkeine Rechnung."
|
||
|
||
# Mehrere Belege unter derselben Nummer -> auswählen lassen.
|
||
if len(gefunden) > 1:
|
||
if gefunden != self._kandidaten:
|
||
self._kandidaten = gefunden
|
||
self._auswahl.delete(0, "end")
|
||
for x in gefunden:
|
||
self._auswahl.insert("end",
|
||
f"{x.get('datum',''):11s} {str(x.get('nachname') or '?'):16s} "
|
||
f"{berechnung.eur(x.get('gesamt') or 0):>11s} "
|
||
f"{os.path.basename(x.get('_pfad',''))}")
|
||
self._auswahl.grid()
|
||
self._auswahl.selection_clear(0, "end")
|
||
auswahl = self._auswahl.curselection()
|
||
if not auswahl:
|
||
return None, (f"⚠ Die Nummer {nr} liegt {len(gefunden)}× im Ordner – "
|
||
"das darf es nicht geben.\nBitte unten auswählen, "
|
||
"welcher Beleg gemeint ist.")
|
||
treffer = gefunden[auswahl[0]]
|
||
else:
|
||
self._kandidaten = []
|
||
self._auswahl.grid_remove()
|
||
treffer = gefunden[0]
|
||
|
||
if treffer.get("art") == "storno":
|
||
return None, f"{nr} ist selbst eine Stornorechnung."
|
||
nr_norm = nummer_normalisieren(treffer.get("rechnungsnummer"))
|
||
if nr_norm in stornierte:
|
||
st = stornos.get(nr_norm) or {}
|
||
folgt = st.get("korrektur_nummer") or ""
|
||
if nr_norm in ersetzte or not folgt:
|
||
return None, (f"{nr} wurde bereits mit {st.get('rechnungsnummer','?')} "
|
||
"storniert – ein zweites Mal geht nicht.")
|
||
# Auf dem Storno steht, welche Nummer die berichtigte Rechnung
|
||
# bekommt. Damit braucht es keinen gemerkten Zustand: fehlt sie
|
||
# noch, wird sie einfach unter dieser Nummer geschrieben.
|
||
return None, (f"{nr} ist mit {st.get('rechnungsnummer','?')} storniert. "
|
||
f"Die berichtigte Rechnung gehört unter die Nummer {folgt} – "
|
||
"die steht auch auf dem Storno-Blatt.")
|
||
gesamt = float(treffer.get("gesamt") or 0)
|
||
mehrfach = (f"⚠ Achtung: die Nummer liegt {len(gefunden)}× im Ordner.\n"
|
||
if len(gefunden) > 1 else "")
|
||
return treffer, (
|
||
mehrfach +
|
||
f"Rechnung {treffer.get('rechnungsnummer')} vom {treffer.get('datum','')}"
|
||
f" {treffer.get('nachname','')}"
|
||
+ (f" ({os.path.basename(treffer.get('_pfad',''))})" if mehrfach else "") + "\n"
|
||
f"Rechnungsbetrag {berechnung.eur(treffer.get('zwischensumme') or 0)}, "
|
||
f"Gesamt {berechnung.eur(gesamt)}\n"
|
||
f"Der Storno lautet über {berechnung.eur(-abs(gesamt))}.")
|
||
|
||
def vorschau(*_a):
|
||
v_info.set(suche()[1])
|
||
ent.bind("<KeyRelease>", vorschau)
|
||
self._auswahl.bind("<<ListboxSelect>>", vorschau)
|
||
|
||
def anlegen(*_a):
|
||
meta, meldung = suche()
|
||
if not meta:
|
||
v_info.set(meldung)
|
||
return
|
||
d.destroy()
|
||
if storno:
|
||
self._storno_anlegen(meta, out_dir)
|
||
else:
|
||
self._berichtigung_starten(meta, out_dir)
|
||
ent.bind("<Return>", anlegen)
|
||
|
||
btns = ttk.Frame(frm)
|
||
btns.grid(row=4, column=0, columnspan=2, pady=(14, 0), sticky="e")
|
||
ttk.Button(btns, text="Storno erstellen" if storno else "Rechnung laden",
|
||
command=anlegen).pack(side="right", padx=4)
|
||
ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right")
|
||
frm.columnconfigure(1, weight=1)
|
||
|
||
|
||
def _storno_anlegen(self, meta, out_dir):
|
||
"""Schreibt die Storno-PDF und füllt das Formular für die berichtigte Rechnung."""
|
||
alt_nummer = str(meta.get("rechnungsnummer", ""))
|
||
nummer = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
||
if not nummer:
|
||
messagebox.showerror("Keine freie Nummer",
|
||
"Es wurde keine freie Rechnungsnummer gefunden.")
|
||
return
|
||
|
||
r = storno_rechnung_bauen(meta, self.cfg)
|
||
heute = datetime.date.today().strftime("%d.%m.%Y")
|
||
# Welche Nummer die berichtigte Rechnung bekommt, steht schon jetzt fest -
|
||
# die naechste freie NACH dem Storno. Sie kommt aufs Storno-Blatt, damit
|
||
# die Kette aus dem Beleg allein lesbar ist und kein Zustand gemerkt
|
||
# werden muss: alte Nummer -> Storno -> neue Nummer, alles auf einem Blatt.
|
||
folge_nummer = naechste_freie_nummer_nach(out_dir, self.cfg, nummer, self._belegte_nummern())
|
||
# Der Storno geht an denselben Empfaenger wie die alte Rechnung - was in
|
||
# deren Kenndaten fehlt, wird aus ihrem Adressblock geholt.
|
||
kunde, woher = kundendaten_ermitteln(meta)
|
||
namenszeile = " ".join(t for t in (kunde.get("vorname", ""),
|
||
kunde.get("nachname", "")) if t)
|
||
kunde_zeilen = [kunde.get("anrede", ""), namenszeile, kunde.get("zusatz", ""),
|
||
kunde.get("strasse", ""), kunde.get("plzort", "")]
|
||
|
||
# Der Storno ist der Beleg, der die Rechnung aufhebt - er geht an den Gast
|
||
# und braucht dieselben Angaben wie sie. Steht in der alten Rechnung nichts,
|
||
# kann das Programm nichts erfinden; dann muss der Nutzer entscheiden.
|
||
maengel = kunde_maengel(kunde)
|
||
if maengel:
|
||
if not messagebox.askyesno(
|
||
"Angaben zum Kunden unvollständig",
|
||
f"In Rechnung {alt_nummer} steht nicht alles:\n\n- "
|
||
+ "\n- ".join(maengel) +
|
||
"\n\nDas Storno geht an den Gast und trägt dieselben Angaben. "
|
||
"Was fehlt, fehlt auch dort.\n\nTrotzdem erstellen?",
|
||
icon="warning", default="no"):
|
||
return
|
||
|
||
# Der Storno ist das Spiegelbild der Rechnung - auch bei den
|
||
# UEBERNACHTUNGEN. Stand hier vorher nichts, blieben die Naechte der
|
||
# aufgehobenen Rechnung im Steuerjournal stehen: die Betraege kuerzten
|
||
# sich, die Zahl der Uebernachtungen nicht, und das Amt bekam sie
|
||
# doppelt gemeldet (4 - 0 + 4 = 8 statt 4).
|
||
alt_naechte = int(parse_num(str(meta.get("naechte") or 0)) or 0)
|
||
kopf = {
|
||
"rechnungsnummer": nummer,
|
||
"datum": heute,
|
||
"anreise": "", "abreise": "",
|
||
"naechte": -abs(alt_naechte) if alt_naechte else "",
|
||
"kunde_zeilen": kunde_zeilen,
|
||
"nachname": kunde.get("nachname", ""),
|
||
"vorname": kunde.get("vorname", ""),
|
||
"art": "storno",
|
||
# Vorgang der alten Rechnung uebernehmen, nicht deren Nummer: wird eine
|
||
# schon berichtigte Rechnung noch einmal storniert, bleibt die ganze
|
||
# Kette unter EINEM Schluessel.
|
||
"vorgang": str(meta.get("vorgang") or alt_nummer),
|
||
"storno_zu": alt_nummer,
|
||
# alte Nummer -> diese Nummer. Steht auf dem Blatt, damit die Kette
|
||
# aus dem Storno allein lesbar ist.
|
||
"korrektur_nummer": folge_nummer or "",
|
||
"storno_datum": str(meta.get("datum") or ""),
|
||
"kunde": kunde,
|
||
}
|
||
name = f"Storno_{safe_filename(nummer)}"
|
||
if kopf["nachname"]:
|
||
name += "_" + safe_filename(kopf["nachname"])
|
||
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"Storno-PDF konnte nicht erstellt werden:\n{e}")
|
||
print("[storno] FEHLER:", e)
|
||
return
|
||
|
||
self.storno_stand = {
|
||
"pfad": pfad, "nummer": nummer, "zu": alt_nummer,
|
||
"vorgang": str(meta.get("vorgang") or alt_nummer),
|
||
"zu_datum": str(meta.get("datum") or ""),
|
||
"jahr_vorher": self.cfg.get("rechnung_jahr", datetime.date.today().year),
|
||
"zaehler_vorher": self.cfg.get("rechnung_zaehler", 0),
|
||
}
|
||
self._zaehler_aus_nummer_speichern(nummer)
|
||
print(f"[storno] erstellt -> {pfad}")
|
||
# Der Storno hat eine eigene Nummer - die gehoert ins gemeinsame Buch,
|
||
# sonst vergibt sie das andere Programm noch einmal, und das Journal
|
||
# erfaehrt nicht, dass die Rechnung aufgehoben ist.
|
||
self._ins_buch(nummer, kopf, r, pfad)
|
||
self._buch_anzeigen()
|
||
self._journal_benachrichtigen(f"Storno {nummer} zu Rechnung {alt_nummer} erstellt")
|
||
|
||
fehlend = self._formular_aus_meta(meta)
|
||
self.r_nummer.set(folge_nummer or "")
|
||
self._storno_anzeigen()
|
||
|
||
hinweis = ""
|
||
if fehlend:
|
||
hinweis = (f"\n\nZum Kunden: {fehlend}.\nBitte oben prüfen und ergänzen – "
|
||
"ohne vollständige Anschrift wird die Rechnung nicht geschrieben.")
|
||
messagebox.showinfo(
|
||
"Storno erstellt",
|
||
f"Stornorechnung {nummer} zu Rechnung {alt_nummer} liegt hier:\n{pfad}\n\n"
|
||
f"Jetzt die berichtigte Rechnung {self.r_nummer.get()} anpassen und "
|
||
f"\"PDF erstellen\" drücken.{hinweis}")
|
||
|
||
def _formular_aus_meta(self, meta):
|
||
"""Formular mit den Daten der alten Rechnung füllen.
|
||
|
||
Gibt zurück, was NICHT übernommen werden konnte - aeltere PDFs (v1/v2)
|
||
tragen weder Anschrift noch Positionen in den Kenndaten.
|
||
"""
|
||
kunde, woher = kundendaten_ermitteln(meta)
|
||
self.k_vorname.set(kunde.get("vorname", ""))
|
||
self.k_name.set(kunde.get("nachname", ""))
|
||
self.k_anrede.set(str(kunde.get("anrede", "") or ""))
|
||
self.k_zusatz.set(str(kunde.get("zusatz", "") or ""))
|
||
self.k_strasse.set(str(kunde.get("strasse", "") or ""))
|
||
self.k_plzort.set(str(kunde.get("plzort", "") or ""))
|
||
self.r_anreise.set(str(meta.get("anreise", "") or ""))
|
||
self.r_abreise.set(str(meta.get("abreise", "") or ""))
|
||
self.r_datum.set(datetime.date.today().strftime("%d.%m.%Y"))
|
||
|
||
# Eine Montage-Rechnung gehoert in ihren Reiter: ihre Position 1 ist die
|
||
# Monatsmiete, im Campingformular waere es Position 1 des Katalogs
|
||
# (Wohnwagen pro Nacht) - der Storno haette stillschweigend etwas
|
||
# anderes berichtigt.
|
||
montage = str(meta.get("vorlage", "") or "") == "montage"
|
||
try:
|
||
self.blaetter.select(1 if montage else 0)
|
||
except (AttributeError, tk.TclError):
|
||
pass
|
||
|
||
for nr, var in self.anzahl_vars.items():
|
||
var.set("")
|
||
self.personen_vars[nr].set("")
|
||
if montage:
|
||
erste = (meta.get("positionen") or [{}])[0]
|
||
self.m_monate.set(str(erste.get("anzahl", "1") or "1"))
|
||
else:
|
||
for p in meta.get("positionen") or []:
|
||
nr = p.get("nr")
|
||
if nr in self.anzahl_vars:
|
||
self.anzahl_vars[nr].set(str(p.get("anzahl", "") or ""))
|
||
self.personen_vars[nr].set(str(p.get("personen", "") or ""))
|
||
self.recalc()
|
||
|
||
hinweise = []
|
||
if woher:
|
||
hinweise.append("aus dem Rechnungstext gelesen: " + ", ".join(woher))
|
||
fehlt = [feld for feld, wert in (("Straße", self.k_strasse.get()),
|
||
("PLZ / Ort", self.k_plzort.get()),
|
||
("Nachname", self.k_name.get()))
|
||
if not wert]
|
||
if fehlt:
|
||
hinweise.append("steht in der alten Rechnung nicht drin: " + ", ".join(fehlt))
|
||
if not (meta.get("positionen") or []):
|
||
hinweise.append("Leistungen konnten nicht übernommen werden")
|
||
return "; ".join(hinweise)
|