diff --git a/app.py b/app.py index b42c7db..960fe55 100644 --- a/app.py +++ b/app.py @@ -33,8 +33,7 @@ import gemeinsam import ordnerwahl import pdf_renderer from helfer import ANREDEN, kunde_maengel, parse_date, parse_num, saeubern, safe_filename, ziffern -from bestand import (hoechster_zaehler_im_ordner, naechste_freie_nummer, - rechnungen_im_ordner, +from bestand import (naechste_freie_nummer, rechnungen_im_ordner, vorhandene_rechnungen) from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn, laufende_pid_lesen) @@ -94,8 +93,6 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk): self.after(250, self._frage_speicherort_falls_noetig) # ... und danach nach dem gemeinsamen Ordner fuer das Nummernbuch. self.after(400, self._frage_gemeinsam_falls_noetig) - # ... und danach einmalig nach dem Stand der Rechnungsnummer. - self.after(600, self._frage_nummer_falls_noetig) self.after(500, self._push_abholen) @@ -598,135 +595,6 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk): self.buch_lbl.configure(text=f"Nummernbuch: {anzahl} Nummern", foreground=self.col_muted) - def _frage_nummer_falls_noetig(self): - """Beim ersten Start einmal nach dem Stand der Rechnungsnummer fragen. - - Er kommt aus einer Excel-Tabelle mit laufender Nummer - die muss er - einmal nennen koennen, sonst faengt das Programm bei 001 an und vergibt - Nummern ein zweites Mal. Danach ist der Zaehler zu. - """ - if TESTLAUF or self.cfg.get("nummer_eingerichtet"): - return - jahr = int(self.cfg.get("rechnung_jahr", 0) or datetime.date.today().year) - gefunden = self.hoechster_stand(jahr) - vorschlag = max(gefunden, int(self.cfg.get("rechnung_zaehler", 0) or 0)) - - d = tk.Toplevel(self) - d.title("Rechnungsnummer einrichten") - d.transient(self) - d.resizable(False, False) - self._style_dialog(d) - wert = tk.StringVar(value=str(vorschlag)) - fertig = {"ok": False} - - rahmen = ttk.Frame(d, padding=18) - rahmen.pack(fill="both", expand=True) - ttk.Label(rahmen, text="Wo soll die Rechnungsnummer weiterlaufen?", - font=("Segoe UI", 13, "bold")).grid(row=0, column=0, columnspan=2, sticky="w") - ttk.Label(rahmen, wraplength=620, justify="left", text=( - "Trag die zuletzt vergebene Nummer ein – die aus deiner bisherigen Liste. " - "Die nächste Rechnung bekommt dann die Nummer danach.\n\n" - "Das wird EINMAL eingerichtet. Danach steht der Zähler fest: eine " - "ausgestellte Rechnung wird nur noch über Storno oder Berichtigung " - "korrigiert, und jede Nummer gibt es nur einmal " - "(§ 14 Abs. 4 Nr. 4 UStG).")).grid( - row=1, column=0, columnspan=2, sticky="w", pady=(8, 12)) - - if gefunden: - tk.Label(rahmen, wraplength=620, justify="left", fg="#1f5c3d", - font=("Segoe UI", 9, "bold"), - text=f"Im Rechnungsordner und im Nummernbuch steht schon " - f"{jahr}-{gefunden:03d} – niedriger geht es nicht.").grid( - row=2, column=0, columnspan=2, sticky="w", pady=(0, 10)) - - ttk.Label(rahmen, text=f"Zuletzt vergeben: {jahr} –").grid(row=3, column=0, sticky="e") - ttk.Entry(rahmen, textvariable=wert, width=10, justify="center").grid( - row=3, column=1, sticky="w", padx=(8, 0)) - hinweis = tk.Label(rahmen, text="", wraplength=620, justify="left", fg="#a00000", - font=("Segoe UI", 9)) - hinweis.grid(row=4, column=0, columnspan=2, sticky="w", pady=(8, 0)) - - def uebernehmen(): - try: - zahl = int(parse_num(wert.get())) - except (TypeError, ValueError): - zahl = -1 - if zahl < gefunden: - hinweis.configure(text=f"Mindestens {gefunden} – so weit ist schon vergeben.") - return - if zahl < 0: - hinweis.configure(text="Bitte eine Zahl eintragen (0 = es geht bei 001 los).") - return - if not messagebox.askyesno( - "Sind Sie sicher?", - f"Die nächste Rechnung bekommt die Nummer {jahr}-{zahl + 1:03d}.\n\n" - "Das lässt sich danach nicht mehr ändern.\n\n" - "Ja = so übernehmen\n" - "Nein = zurück zur Eingabe", icon="question", default="yes", parent=d): - return - self.cfg["rechnung_jahr"] = jahr - self.cfg["rechnung_zaehler"] = zahl - self.cfg["nummer_eingerichtet"] = True - try: - config.save_config(self.cfg) - except OSError as e: # noqa: BLE001 - messagebox.showwarning("Nicht gespeichert", - f"Der Stand ließ sich nicht speichern:\n{e}", parent=d) - return - fertig["ok"] = True - d.destroy() - - knoepfe = ttk.Frame(rahmen) - knoepfe.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(16, 0)) - ttk.Button(knoepfe, text="Übernehmen", width=16, command=uebernehmen).pack(side="right") - ttk.Button(knoepfe, text="Später", width=12, command=d.destroy).pack(side="left") - - d.protocol("WM_DELETE_WINDOW", d.destroy) - d.bind("", lambda _e: d.destroy()) - d.update_idletasks() - x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 110}") - d.grab_set() - self.wait_window(d) - if fertig["ok"]: - self._neue_nummer_vorschlagen() - - def hoechster_stand(self, jahr=None): - """Hoechster JE VERGEBENER Zaehler dieses Jahres - aus beiden Quellen. - - Der Zaehler in der config allein taugt nicht als Wahrheit: er laesst - sich in den Einstellungen zuruecksetzen. Deshalb zaehlt, was WIRKLICH - vergeben ist - die PDFs im Ordner und das gemeinsame Nummernbuch (das - kennt auch die Nummern aus dem Altbestand, zu denen es keine PDF gibt). - """ - jahr = int(jahr or self.cfg.get("rechnung_jahr", 0) or datetime.date.today().year) - hoch = hoechster_zaehler_im_ordner(config.ensure_output_dir(self.cfg), jahr) - if self.buch is not None: - try: - hoch = max(hoch, self.buch.hoechster_zaehler(jahr)) - except Exception as e: # noqa: BLE001 - ohne Buch bleibt der Ordner - print("[nummernbuch] Hoechststand nicht lesbar:", e) - return hoch - - def _zaehler_absichern(self): - """Einen zurueckgedrehten Zaehler stillschweigend wieder anheben. - - Nicht als Vorwurf - es reicht ein alter Rechner, eine wiederhergestellte - config oder ein Jahreswechsel von Hand. Vergeben ist vergeben. - """ - jahr = int(self.cfg.get("rechnung_jahr", 0) or datetime.date.today().year) - if jahr != datetime.date.today().year: - return - hoch = self.hoechster_stand(jahr) - if hoch > int(self.cfg.get("rechnung_zaehler", 0) or 0): - print(f"[nummern] Zaehler stand auf {self.cfg.get('rechnung_zaehler')}, " - f"vergeben ist bis {hoch} - angehoben") - self.cfg["rechnung_zaehler"] = hoch - try: - config.save_config(self.cfg) - except OSError as e: # noqa: BLE001 - print("[nummern] Zaehler nicht gespeichert:", e) - def _belegte_nummern(self): """Alle im Buch vergebenen Nummern. Faellt das Buch aus, ist die Menge leer - eine Rechnung muss auch dann geschrieben werden koennen.""" @@ -935,10 +803,6 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk): """ if self.storno_stand or self.berichtigung_stand: return - # Erst sicherstellen, dass der Zaehler nicht hinter dem steht, was - # schon vergeben ist - sonst schlaegt das Programm eine Nummer vor, - # die es ein zweites Mal vergeben wuerde. - self._zaehler_absichern() frei = naechste_freie_nummer(config.ensure_output_dir(self.cfg), self.cfg, self._belegte_nummern()) if frei: diff --git a/bestand.py b/bestand.py index abfd31e..3333712 100644 --- a/bestand.py +++ b/bestand.py @@ -8,7 +8,6 @@ Rechnung steht, welche Nummer als nächste frei ist. Hier wird nur gelesen. import datetime import json import os -import re import config from helfer import ANREDEN, name_aufteilen, saeubern, safe_filename @@ -169,29 +168,6 @@ def rechnungen_im_ordner(ordner): gefunden.append(daten) gefunden.sort(key=lambda d: str(d.get("rechnungsnummer", "")), reverse=True) return gefunden -def hoechster_zaehler_im_ordner(ordner, jahr): - """Groesster Zaehler dieses Jahres, den die PDFs im Ordner hergeben. - - Gelesen wird der DATEINAME (Rechnung_2026-442_..., Storno_2026-443_...) - - das geht ohne jede PDF zu oeffnen und reicht: das Programm schreibt die - Nummer immer in den Namen. Was jemand von Hand umbenannt hat, faellt hier - durch; dafuer gibt es das gemeinsame Nummernbuch als zweite Quelle. - """ - hoch = 0 - muster = re.compile(rf"(?:Rechnung|Storno)_{int(jahr)}-(\d+)", re.IGNORECASE) - try: - namen = os.listdir(ordner) - except OSError: - return 0 - for name in namen: - if not name.lower().endswith(".pdf"): - continue - m = muster.search(name) - if m: - hoch = max(hoch, int(m.group(1))) - return hoch - - def naechste_freie_nummer_nach(ordner, cfg, vergeben, belegt=None): """Erste freie Nummer NACH - fuer die Rechnung, die auf ein Storno folgt. diff --git a/config.py b/config.py index 90f6d5e..0b2a5fb 100644 --- a/config.py +++ b/config.py @@ -142,10 +142,6 @@ DEFAULT_CONFIG = { "montage": DEFAULT_MONTAGE, # Das Angebot hat eine EIGENE Reihe (2026-A001): es darf keine # Rechnungsnummer verbrauchen - die gehoert zu einer echten Rechnung. - # Einmal beim ersten Start gesetzt (er kommt aus einer Excel-Tabelle mit - # laufender Nummer). Danach steht der Zaehler fest - geaendert wird eine - # Rechnung nur noch ueber Storno oder Berichtigung. - "nummer_eingerichtet": False, "angebot_jahr": 0, "angebot_zaehler": 0, # Alle Steuern als EINE benannte Liste. diff --git a/einstellungen.py b/einstellungen.py index ddc2b54..190a226 100644 --- a/einstellungen.py +++ b/einstellungen.py @@ -399,26 +399,6 @@ class EinstellungenMixin: ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right") def dlg_nummer(self): - # Einmal eingerichtet, dann zu: eine ausgestellte Rechnung wird nur noch - # ueber Storno oder Berichtigung korrigiert (§ 14 Abs. 4 Nr. 4 UStG, - # § 146 Abs. 4 AO). Angezeigt wird der Stand trotzdem - man will ja - # nachsehen koennen. - if self.cfg.get("nummer_eingerichtet"): - jahr = self.cfg.get("rechnung_jahr") - zaehler = int(self.cfg.get("rechnung_zaehler", 0) or 0) - hoch = self.hoechster_stand(jahr) - messagebox.showinfo( - "Rechnungsnummer", - f"Zuletzt vergeben: {jahr}-{max(zaehler, hoch):03d}\n" - f"Die nächste Rechnung bekommt {jahr}-{max(zaehler, hoch) + 1:03d}.\n\n" - "Der Zähler wurde einmal eingerichtet und lässt sich nicht mehr ändern. " - "Eine ausgestellte Rechnung wird über „Storno“ oder „Berichtigen“ " - "korrigiert – jede Nummer darf es nur einmal geben " - "(§ 14 Abs. 4 Nr. 4 UStG).") - return - return self._dlg_nummer_einrichten() - - def _dlg_nummer_einrichten(self): d = tk.Toplevel(self) d.title("Rechnungsnummer") d.transient(self) @@ -432,39 +412,14 @@ class EinstellungenMixin: ttk.Label(frm, text="Format: JJJJ-NNN (z. B. 2026-013). Der Zähler ist der zuletzt\n" "vergebene Wert – beim nächsten Beleg wird +1 vorgeschlagen.", justify="left").grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8)) - hoch_jetzt = self.hoechster_stand() - ttk.Label(frm, foreground=self.col_muted, justify="left", - text=f"Schon vergeben ist bis {self.cfg.get('rechnung_jahr')}-" - f"{hoch_jetzt:03d} (aus den PDFs im Ordner und dem gemeinsamen\n" - "Nummernbuch). Dahinter zurück geht nicht – nach vorn schon.").grid( - row=4, column=0, columnspan=2, sticky="w", pady=(10, 0)) ttk.Label(frm, text="Jahr").grid(row=1, column=0, sticky="w", pady=3) ttk.Entry(frm, textvariable=v_jahr, width=10).grid(row=1, column=1, sticky="w", pady=3) ttk.Label(frm, text="Zuletzt vergebener Zähler").grid(row=2, column=0, sticky="w", pady=3) ttk.Entry(frm, textvariable=v_z, width=10).grid(row=2, column=1, sticky="w", pady=3) def speichern(): - jahr = int(parse_num(v_jahr.get())) - zaehler = int(parse_num(v_z.get())) - # Zurueck geht nicht: was vergeben ist, ist vergeben. Geprueft wird - # gegen die PDFs im Ordner UND das gemeinsame Nummernbuch - der - # Zaehler in der config allein waere ja gerade das, was hier - # geaendert wird. Vorwaerts ist erlaubt: eine lueckenlose Folge - # verlangt das Gesetz nicht, jede Nummer aber nur einmal - # (§ 14 Abs. 4 Nr. 4 UStG). - hoch = self.hoechster_stand(jahr) - if jahr == int(self.cfg.get("rechnung_jahr", jahr) or jahr) and zaehler < hoch: - messagebox.showwarning( - "Nummer schon vergeben", - f"Für {jahr} ist bereits bis {jahr}-{hoch:03d} vergeben.\n\n" - f"Der Zähler kann nicht auf {zaehler} zurückgesetzt werden – sonst " - "gäbe es eine Nummer zweimal, und jede Rechnungsnummer darf nur " - "einmal vergeben werden (§ 14 Abs. 4 Nr. 4 UStG).\n\n" - "Nach vorn ist er frei: Lücken sind erlaubt.", parent=d) - v_z.set(str(hoch)) - return - self.cfg["rechnung_jahr"] = jahr - self.cfg["rechnung_zaehler"] = zaehler + self.cfg["rechnung_jahr"] = int(parse_num(v_jahr.get())) + self.cfg["rechnung_zaehler"] = int(parse_num(v_z.get())) config.save_config(self.cfg) self._neue_nummer_vorschlagen() d.destroy() diff --git a/gemeinsam.py b/gemeinsam.py index 6c2872d..fced977 100644 --- a/gemeinsam.py +++ b/gemeinsam.py @@ -209,19 +209,6 @@ class Nummernbuch: zeilen = con.execute("SELECT nummer FROM nummern") return {z["nummer"] for z in zeilen} - def hoechster_zaehler(self, jahr): - """Groesster Zaehler dieses Jahres im Buch (0, wenn nichts drinsteht). - - Aus '2026-442' wird 442. Nummern anderer Reihen (Angebote 2026-A001) - zaehlen NICHT mit - die haben mit der Rechnungsnummer nichts zu tun. - """ - hoch = 0 - for nummer in self.nummern(jahr): - teil = str(nummer).split("-")[-1] - if teil.isdigit(): - hoch = max(hoch, int(teil)) - return hoch - def eintrag(self, nummer): with self._verbindung() as con: z = con.execute("SELECT * FROM nummern WHERE nummer=?", (str(nummer),)).fetchone() diff --git a/pruef_angebot.py b/pruef_angebot.py index e7229f2..d4f8e54 100644 --- a/pruef_angebot.py +++ b/pruef_angebot.py @@ -226,18 +226,6 @@ 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") - -# ---- 9. Zweite Sicherung: auch ohne Kenndaten wird nichts gebucht ----- -from pdf_parser import keine_buchung # noqa: E402 - -nur_name = os.path.join(ordner, "Angebot_2026-A099_OhneKenndaten.pdf") -shutil.copy(os.path.join(angebote, "Angebot_2026-A001_Vorab.pdf"), nur_name) -pruefe(keine_buchung(nur_name), - "ein Angebot wird auch am Dateinamen erkannt (falls die Kenndaten fehlen)") -pruefe(keine_buchung(os.path.join(angebote, "Angebot_2026-A001_Vorab.pdf")), - "und alles im Unterordner Angebote wird uebersprungen") -pruefe(not keine_buchung(os.path.join(ordner, "Rechnung_2026-001_Vorab.pdf")), - "eine echte Rechnung wird davon NICHT erfasst") journal.schliessen() if not fehler: diff --git a/pruef_nummernsperre.py b/pruef_nummernsperre.py deleted file mode 100644 index 6893504..0000000 --- a/pruef_nummernsperre.py +++ /dev/null @@ -1,305 +0,0 @@ -# -*- coding: utf-8 -*- -"""Pruefstand: der Rechnungszaehler kann nicht mehr zurueck. - -Sein Einwand: *"er kann jetzt bescheissen, indem er das in den einstellungen -aendert"* - und der Vorschlag dazu: *"dann koennte man noch die pdfs nutzen als -referenz, die haben auch die nummern"*. - -Geprueft wird beides als Quelle: - * die PDFs im Rechnungsordner (Dateiname traegt die Nummer) - * das gemeinsame Nummernbuch (kennt auch Nummern OHNE PDF - Altbestand) - -Vorwaerts darf der Zaehler: eine lueckenlose Folge verlangt das Gesetz nicht. -Zurueck nicht: jede Nummer darf es nur einmal geben (§ 14 Abs. 4 Nr. 4 UStG). -""" -import json -import os -import shutil -import sys -import tempfile -import threading - -QUELLE = r"C:\claude\rechnungstool" -sys.path.insert(0, QUELLE) - -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - - -def _reissleine(): - print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") - os._exit(2) - - -_wecker = threading.Timer(150, _reissleine) -_wecker.daemon = True -_wecker.start() - -os.environ["CAMPINGHOF_TEST"] = "1" -tmp = tempfile.mkdtemp(prefix="sperre_pruef_") -ordner = os.path.join(tmp, "rechnungen") -gemeinsam_ordner = os.path.join(tmp, "install") -os.makedirs(ordner) -os.makedirs(gemeinsam_ordner) - -import config # noqa: E402 - -shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json")) -config.CONFIG_PATH = os.path.join(tmp, "config.json") -roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) -roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 -roh["output_dir"] = ordner -json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) - -import app as A # noqa: E402 -import gemeinsam # noqa: E402 -from tkinter import messagebox # noqa: E402 - -warnungen = [] -messagebox.askyesno = lambda t, x, **k: ("Fertig" not in t) -messagebox.showinfo = lambda *a, **k: None -messagebox.showwarning = lambda *a, **k: warnungen.append(a[0] if a else "") -messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "") - -f = A.RechnungsApp() -f.cfg["output_dir"] = ordner - -JAHR = 2026 -if int(f.cfg.get("rechnung_jahr", 0)) != JAHR: - print("HINWEIS | Der Pruefstand rechnet mit 2026 - heute ist ein anderes Jahr.") - - -def rechnung(name): - f.reset_formular() - f.k_vorname.set("Anna") - f.k_name.set(name) - f.k_strasse.set("Musterweg 1") - f.k_plzort.set("12345 Musterstadt") - f.r_anreise.set("01.08.2026") - f.r_abreise.set("03.08.2026") - f.anzahl_vars[sorted(f.anzahl_vars)[0]].set("2") - f._testwahl = "rechnung" - f.update() - f.pdf_erstellen() - - -# ---- 1. Drei Rechnungen schreiben ---------------------------------------- -for name in ("Eins", "Zwei", "Drei"): - rechnung(name) -dateien = sorted(d for d in os.listdir(ordner) if d.endswith(".pdf")) -pruefe(len(dateien) == 3, f"drei Rechnungen geschrieben ({dateien})") -pruefe(f.cfg.get("rechnung_zaehler") == 3, f"Zaehler steht auf 3 ({f.cfg.get('rechnung_zaehler')})") -pruefe(f.hoechster_stand(JAHR) == 3, f"hoechster Stand aus den PDFs: 3 ({f.hoechster_stand(JAHR)})") - -# ---- 2. Zaehler von Hand zurueckdrehen (der Betrugsfall) ----------------- -f.cfg["rechnung_zaehler"] = 0 -config.save_config(f.cfg) -f._neue_nummer_vorschlagen() -pruefe(f.r_nummer.get() == "2026-004", - f"trotz zurueckgedrehtem Zaehler wird 2026-004 vorgeschlagen ({f.r_nummer.get()})") -pruefe(f.cfg.get("rechnung_zaehler") == 3, - f"und der Zaehler ist wieder angehoben ({f.cfg.get('rechnung_zaehler')})") - -# ---- 3. Auch die Einstellungen lassen ihn nicht zurueck ------------------ -def knoepfe_von(w, aus=None): - aus = [] if aus is None else aus - for k in w.winfo_children(): - if k.winfo_class() in ("TButton", "Button"): - aus.append(k) - knoepfe_von(k, aus) - return aus - - -def felder_von(w, aus=None): - aus = [] if aus is None else aus - for k in w.winfo_children(): - if k.winfo_class() in ("TEntry", "Entry"): - aus.append(k) - felder_von(k, aus) - return aus - - -def nummerndialog(neuer_zaehler): - """Dialog oeffnen, Zaehler eintragen, Speichern druecken.""" - f.dlg_nummer() - f.update() - top = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"][-1] - felder = felder_von(top) - felder[1].delete(0, "end") - felder[1].insert(0, str(neuer_zaehler)) - f.update() - for k in knoepfe_von(top): - if "Speichern" in str(k.cget("text")): - k.invoke() - break - f.update() - noch_offen = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"] - for w in noch_offen: - w.destroy() - return bool(noch_offen) - - -warnungen.clear() -blieb_offen = nummerndialog(1) -pruefe(f.cfg.get("rechnung_zaehler") == 3, - f"die Einstellungen setzen den Zaehler NICHT auf 1 ({f.cfg.get('rechnung_zaehler')})") -pruefe(any("vergeben" in w for w in warnungen), f"mit Meldung an den Benutzer ({warnungen})") -pruefe(blieb_offen, "und der Dialog bleibt offen, statt still zu schlucken") - -warnungen.clear() -nummerndialog(50) -pruefe(f.cfg.get("rechnung_zaehler") == 50, - f"nach VORN darf er (Luecken sind erlaubt) ({f.cfg.get('rechnung_zaehler')})") -pruefe(not warnungen, f"und dabei wird nicht gewarnt ({warnungen})") - -# ---- 4. Das Nummernbuch kennt Nummern OHNE PDF -------------------------- -f.cfg["gemeinsam_ordner"] = gemeinsam_ordner -f._buch_oeffnen() -pruefe(f.buch is not None, "das gemeinsame Nummernbuch ist da") -f.buch.eintragen("2026-090", jahr=2026, nachname="AltbestandOhnePDF", - quelle="journal", herkunft="xlsx") -pruefe(f.hoechster_stand(JAHR) == 90, - f"der hoechste Stand kommt jetzt aus dem Buch: 90 ({f.hoechster_stand(JAHR)})") - -f.cfg["rechnung_zaehler"] = 5 -config.save_config(f.cfg) -f._neue_nummer_vorschlagen() -pruefe(f.r_nummer.get() == "2026-091", - f"vorgeschlagen wird 2026-091 - hinter dem Altbestand ({f.r_nummer.get()})") - -warnungen.clear() -nummerndialog(10) -pruefe(f.cfg.get("rechnung_zaehler") == 90, - f"und die Einstellungen kommen auch nicht hinter das Buch zurueck " - f"({f.cfg.get('rechnung_zaehler')})") - -# ---- 5. Ohne PDFs im Ordner: das Buch allein haelt die Sperre ----------- -for d in os.listdir(ordner): - if d.endswith(".pdf"): - os.remove(os.path.join(ordner, d)) -pruefe(f.hoechster_stand(JAHR) == 90, - f"auch ohne PDFs bleibt der Stand bei 90 ({f.hoechster_stand(JAHR)})") - -# ---- 6. Und ohne Buch: die PDFs allein halten sie ---------------------- -rechnung("Nachher") # schreibt 2026-091 -f.buch = None -f.cfg["gemeinsam_ordner"] = "" -pruefe(f.hoechster_stand(JAHR) == 91, - f"ohne Buch zaehlen die PDFs: 91 ({f.hoechster_stand(JAHR)})") -warnungen.clear() -nummerndialog(2) -pruefe(f.cfg.get("rechnung_zaehler") == 91, - f"und auch dann geht es nicht zurueck ({f.cfg.get('rechnung_zaehler')})") - -# ---- 7. Der Erststart: EINMAL setzen duerfen --------------------------- -# Er kommt aus einer Excel-Tabelle mit laufender Nummer - die muss er einmal -# nennen koennen. Danach ist der Zaehler zu. -f.cfg["nummer_eingerichtet"] = False -f.cfg["rechnung_jahr"] = JAHR -config.save_config(f.cfg) -A.TESTLAUF = False # den Erststart-Dialog wirklich zeigen - - -def erststart(zahl, erwartet_ja=True): - """Popup oeffnen, Zahl eintragen, Uebernehmen druecken.""" - antwort = {"gefragt": False} - - def sicher(titel, text, **kw): - antwort["gefragt"] = True - return erwartet_ja - - messagebox.askyesno = sicher - - def klick(rest=40): - tops = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"] - if tops: - top = tops[-1] - felder = felder_von(top) - if felder: - felder[0].delete(0, "end") - felder[0].insert(0, str(zahl)) - for k in knoepfe_von(top): - if "bernehmen" in str(k.cget("text")): - k.invoke() - return - if rest: - f.after(100, lambda: klick(rest - 1)) - - f.after(200, klick) - f._frage_nummer_falls_noetig() - for w in [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"]: - w.destroy() - return antwort["gefragt"] - - -hoch_jetzt = f.hoechster_stand(JAHR) -erststart(hoch_jetzt - 5, True) -pruefe(not f.cfg.get("nummer_eingerichtet"), - "eine Zahl UNTER dem Vergebenen wird beim Erststart nicht angenommen") - -gefragt = erststart(500, True) -pruefe(gefragt, "vor dem Uebernehmen wird noch einmal gefragt") -pruefe(f.cfg.get("nummer_eingerichtet") is True, "danach ist der Zaehler eingerichtet") -pruefe(f.cfg.get("rechnung_zaehler") == 500, f"und steht auf 500 ({f.cfg.get('rechnung_zaehler')})") - -# ---- 8. Ab jetzt ist er zu ------------------------------------------- -meldungen = [] -messagebox.showinfo = lambda *a, **k: meldungen.append(a[1] if len(a) > 1 else "") -vorher = f.cfg.get("rechnung_zaehler") -f.dlg_nummer() -f.update() -offen = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"] -pruefe(not offen, "die Einstellungen oeffnen kein Eingabefenster mehr") -pruefe(f.cfg.get("rechnung_zaehler") == vorher, - f"der Zaehler bleibt, wie er ist ({f.cfg.get('rechnung_zaehler')})") -pruefe(any("Storno" in m for m in meldungen), - f"und es wird auf Storno/Berichtigen verwiesen ({meldungen})") - -# Und der Erststart fragt nicht noch einmal -f.after(200, lambda: None) -f._frage_nummer_falls_noetig() -pruefe(not [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"], - "beim naechsten Start wird nicht wieder gefragt") - -# ---- 9. Jahreswechsel: nach dem Bindestrich faengt es wieder bei 001 an -- -# "beim jahreswechsel sollte die nach dem - zeichen auf 000 gehen, weil neues -# jahr". Der Zaehler des VORjahres darf dabei nichts anheben. -import datetime as _dt - -heute = _dt.date.today().year -f.cfg["rechnung_jahr"] = heute - 1 -f.cfg["rechnung_zaehler"] = 442 -f.cfg["nummer_eingerichtet"] = True -config.save_config(f.cfg) -f._neue_nummer_vorschlagen() -pruefe(f.r_nummer.get() == f"{heute}-001", - f"im neuen Jahr wird {heute}-001 vorgeschlagen ({f.r_nummer.get()})") - -# Auch dann, wenn im Ordner noch die hohen Nummern des Vorjahres liegen. -alt_pdf = os.path.join(ordner, f"Rechnung_{heute - 1}-442_Vorjahr.pdf") -open(alt_pdf, "wb").write(b"%PDF-1.4 kein echtes PDF, nur der Name zaehlt") -vor_dem_anlegen = 91 # so weit ist DIESES Jahr aus den Schritten davor -pruefe(f.hoechster_stand(heute) == vor_dem_anlegen, - f"die Rechnung aus dem Vorjahr hebt dieses Jahr NICHT an " - f"({f.hoechster_stand(heute)}, erwartet {vor_dem_anlegen})") -pruefe(f.hoechster_stand(heute - 1) == 442, - f"fuer ihr eigenes Jahr aber schon ({f.hoechster_stand(heute - 1)})") -f._neue_nummer_vorschlagen() -pruefe(f.r_nummer.get() == f"{heute}-001", - f"und es bleibt bei {heute}-001 ({f.r_nummer.get()})") - -f.destroy() -if not fehler: - shutil.rmtree(tmp, ignore_errors=True) -else: - print(f" (Wegwerf-Ordner bleibt: {tmp})") -print("---") -print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}") -sys.exit(1 if fehler else 0)