Gemeinsames Nummernbuch, Storno kuerzt die Naechte, Testlauf-Schalter
Das Rechnungstool kannte nur die Nummern seiner eigenen PDFs. Im Steuerjournal stehen aber Buchungen ohne PDF - der Altbestand aus der Excel-Mappe - und deren Nummern hat es ein zweites Mal vergeben. § 14 Abs. 4 Nr. 4 UStG laesst jede Nummer nur einmal zu. Deshalb fuehren beide Programme jetzt EIN gemeinsames Buch (gemeinsam.py): eine SQLite in einem Ordner, den beide kennen. Es ist ein Register, keine zweite Buchhaltung - die Wahrheit bleiben die PDFs. Was hineinkommt: jede geschriebene Rechnung, jeder Storno (mit storno_zu), jede Neuausstellung. Die Nummernvergabe und die Kollisionspruefung beim Erstellen fragen es mit ab; ein verworfener Storno gibt seine Nummer wieder frei. Faellt der Ordner aus (Netzlaufwerk weg), laeuft alles weiter - dann steht es in der Fussleiste. ordnerwahl.py fragt den Ordner beim ersten Start ab: auswaehlen, OK, Sicherheitsfrage, bei Nein zurueck ins Feld. Zwei Fehler daran gleich behoben, bevor sie jemand treffen konnte: das Fenster war zu schmal (der Auswahl-Knopf lag ausserhalb) und der Griff (grab_set) muss waehrend des Windows-Ordner- dialogs los sein, sonst nimmt der keine Eingabe an. Storno: er ist das Spiegelbild der Rechnung - auch bei den UEBERNACHTUNGEN. Vorher stand dort nichts, und das Steuerjournal meldete dem Amt die Naechte der aufgehobenen Rechnung weiter mit. Auf dem Blatt steht "4 Naechte (aufgehoben)", in den Kenndaten -4. "Neue Rechnung:" auf dem Storno-Blatt heisst jetzt "Berichtigte Rechnung:" - die alte Beschriftung las sich wie die Nummer DIESES Blattes. --test / CAMPINGHOF_TEST=1 unterdrueckt den Erststart-Dialog. In der EXE kommt man ohnehin nicht daran, und die Pruefstaende bleiben sonst im Dialog stehen. Pruefstand pruef_nummernbuch.py (neu): beide Programme auf einem Ordner - das Journal traegt drei Altbestands-Nummern ein, das Rechnungstool schlaegt danach 2026-004 statt 2026-001 vor. Alle uebrigen Pruefstaende gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
parent
5ef4e0a769
commit
ecad4b00ea
12 changed files with 790 additions and 16 deletions
206
app.py
206
app.py
|
|
@ -27,9 +27,12 @@ from tkinter import ttk, messagebox
|
|||
|
||||
import config
|
||||
import berechnung
|
||||
import gemeinsam
|
||||
import ordnerwahl
|
||||
import pdf_renderer
|
||||
from helfer import ANREDEN, kunde_maengel, parse_date, parse_num, saeubern, safe_filename, ziffern
|
||||
from bestand import naechste_freie_nummer, vorhandene_rechnungen
|
||||
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)
|
||||
from theme import ThemeMixin
|
||||
|
|
@ -37,6 +40,13 @@ from korrektur import KorrekturMixin
|
|||
from einstellungen import EinstellungenMixin
|
||||
|
||||
|
||||
# Testlauf: kein Erststart-Dialog. Pruefstaende fahren das Fenster fern, ein
|
||||
# modaler Dialog wuerde sie haengen lassen - und in der EXE kommt man an den
|
||||
# Schalter ohnehin nicht heran (Doppelklick uebergibt keine Argumente).
|
||||
# python app.py --test oder CAMPINGHOF_TEST=1
|
||||
TESTLAUF = ("--test" in sys.argv) or os.environ.get("CAMPINGHOF_TEST") == "1"
|
||||
|
||||
|
||||
class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
@ -53,6 +63,9 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
# Zwischenspeicher: geschriebenes Storno, das noch bestaetigt werden muss.
|
||||
# Solange es hier liegt, gehoert zur naechsten PDF die Storno-Kette.
|
||||
self.storno_stand = None
|
||||
# Das gemeinsame Nummernbuch (siehe gemeinsam.py). None = noch keiner
|
||||
# gesetzt; das Programm laeuft dann weiter, warnt aber sichtbar.
|
||||
self.buch = None
|
||||
# Zwischenspeicher fuer eine reine Berichtigung (§ 31 Abs. 5 UStDV):
|
||||
# die naechste PDF ist dann ein berichtigtes Blatt zu dieser Rechnung.
|
||||
self.berichtigung_stand = None
|
||||
|
|
@ -74,6 +87,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
self.after(60, lambda: self._dark_titlebar(self))
|
||||
# Beim ersten Start nach dem Speicherort fragen (wenn noch keiner gesetzt ist)
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Menue
|
||||
|
|
@ -96,6 +111,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
("Firmendaten, Logo & Texte…", self.dlg_firma),
|
||||
("Rechnungsnummer…", self.dlg_nummer),
|
||||
("Speicherort der Rechnungen…", self.dlg_speicherort),
|
||||
("Gemeinsamer Ordner (Nummernbuch)…", self.dlg_gemeinsam),
|
||||
],
|
||||
"Hilfe": [("Info", self._info)],
|
||||
}
|
||||
|
|
@ -484,6 +500,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
status.pack(side="bottom", fill="x")
|
||||
ttk.Label(status, text="powered by ravokk", foreground=self.col_muted,
|
||||
font=("Segoe UI", 8)).pack(side="right")
|
||||
# Steht der gemeinsame Ordner? Das gehoert sichtbar hin - ohne ihn kennt
|
||||
# das Programm die Nummern des Steuerjournals nicht.
|
||||
self.buch_lbl = ttk.Label(status, text="", foreground=self.col_muted,
|
||||
font=("Segoe UI", 8))
|
||||
self.buch_lbl.pack(side="left")
|
||||
|
||||
f = ttk.Frame(self, padding=(12, 6, 12, 10))
|
||||
f.pack(side="bottom", fill="x")
|
||||
|
|
@ -502,6 +523,161 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
width=14).pack(side="left", padx=4)
|
||||
ttk.Button(links, text="Zurücksetzen", command=self.reset_formular, width=14).pack(side="left", padx=4)
|
||||
|
||||
# --------------------------------------------------- Gemeinsames Buch
|
||||
def _buch_oeffnen(self):
|
||||
"""Nummernbuch aus dem eingestellten Ordner - oder None."""
|
||||
ordner = str(self.cfg.get("gemeinsam_ordner", "") or "")
|
||||
if not ordner:
|
||||
self.buch = None
|
||||
return
|
||||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||||
if not taugt:
|
||||
self.buch = None
|
||||
print("[nummernbuch] nicht erreichbar:", grund)
|
||||
return
|
||||
self.buch = gemeinsam.Nummernbuch(ordner)
|
||||
|
||||
def _frage_gemeinsam_falls_noetig(self):
|
||||
"""Beim ersten Start nach dem gemeinsamen Ordner fragen.
|
||||
|
||||
Wer abbricht, kann weiterarbeiten - dann steht aber in der Fussleiste,
|
||||
dass die Nummern des Steuerjournals unbekannt sind. Beim naechsten Start
|
||||
wird wieder gefragt.
|
||||
"""
|
||||
self._buch_oeffnen()
|
||||
if self.buch is None and not TESTLAUF:
|
||||
self.dlg_gemeinsam(erststart=True)
|
||||
self._buch_anzeigen()
|
||||
|
||||
def dlg_gemeinsam(self, erststart=False):
|
||||
ordner = ordnerwahl.ordner_abfragen(
|
||||
self, "Rechnungstool", str(self.cfg.get("gemeinsam_ordner", "") or ""))
|
||||
if not ordner:
|
||||
if erststart:
|
||||
messagebox.showwarning(
|
||||
"Ohne gemeinsamen Ordner",
|
||||
"Es wurde kein gemeinsamer Ordner gewählt.\n\n"
|
||||
"Das Rechnungstool kennt jetzt nur die Nummern seiner eigenen "
|
||||
"PDFs - eine Buchung, die nur im Steuerjournal steht, kann eine "
|
||||
"Nummer ein zweites Mal bekommen.\n\n"
|
||||
"Nachholen: Einstellungen -> Gemeinsamer Ordner (Nummernbuch).")
|
||||
return
|
||||
self.cfg["gemeinsam_ordner"] = ordner
|
||||
config.save_config(self.cfg)
|
||||
self._buch_oeffnen()
|
||||
self._nachtragen()
|
||||
self._buch_anzeigen()
|
||||
self._neue_nummer_vorschlagen()
|
||||
|
||||
def _buch_anzeigen(self):
|
||||
"""Fussleiste: welcher Ordner, wie viele Nummern - oder die Warnung."""
|
||||
if not hasattr(self, "buch_lbl"):
|
||||
return
|
||||
if self.buch is None:
|
||||
self.buch_lbl.configure(
|
||||
text="kein gemeinsamer Ordner - Nummern des Steuerjournals unbekannt",
|
||||
foreground="#c04040")
|
||||
return
|
||||
try:
|
||||
anzahl = len(self.buch.nummern())
|
||||
except Exception as e: # noqa: BLE001
|
||||
self.buch_lbl.configure(text=f"Nummernbuch nicht lesbar: {e}",
|
||||
foreground="#c04040")
|
||||
return
|
||||
self.buch_lbl.configure(text=f"Nummernbuch: {anzahl} Nummern",
|
||||
foreground=self.col_muted)
|
||||
|
||||
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."""
|
||||
if self.buch is None:
|
||||
return set()
|
||||
try:
|
||||
return self.buch.nummern()
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("[nummernbuch] Lesen fehlgeschlagen:", e)
|
||||
return set()
|
||||
|
||||
def _ins_buch(self, nummer, kopf, rechnung, pdf_pfad=""):
|
||||
"""Eine geschriebene Rechnung ins gemeinsame Buch eintragen."""
|
||||
if self.buch is None or not nummer:
|
||||
return
|
||||
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
|
||||
datum = parse_date(str(kopf.get("datum", "")))
|
||||
try:
|
||||
jahr = int(str(nummer).split("-")[0])
|
||||
except (ValueError, IndexError):
|
||||
jahr = datum.year if datum else datetime.date.today().year
|
||||
try:
|
||||
self.buch.eintragen(
|
||||
nummer,
|
||||
jahr=jahr,
|
||||
datum=datum.isoformat() if datum else "",
|
||||
nachname=str(kopf.get("nachname", "") or ""),
|
||||
vorname=str(kopf.get("vorname", "") or ""),
|
||||
naechte=int(parse_num(str(kopf.get("naechte") or 0)) or 0),
|
||||
zwischensumme=berechnung.runde(rechnung.brutto),
|
||||
steuer_satz=float(aufschlag.satz) if aufschlag else 0.0,
|
||||
steuer_betrag=berechnung.runde(aufschlag.betrag) if aufschlag else 0.0,
|
||||
gesamt=berechnung.runde(rechnung.gesamt),
|
||||
art=str(kopf.get("art", "rechnung") or "rechnung"),
|
||||
vorgang=str(kopf.get("vorgang", "") or ""),
|
||||
storno_zu=str(kopf.get("storno_zu", "") or ""),
|
||||
ersetzt=str(kopf.get("ersetzt", "") or ""),
|
||||
berichtigt_zu=str(kopf.get("berichtigt_zu", "") or ""),
|
||||
vorlage=str(kopf.get("vorlage", "") or ""),
|
||||
quelle="rechnungstool", herkunft="pdf", pdf_pfad=pdf_pfad)
|
||||
except Exception as e: # noqa: BLE001 - die PDF ist schon geschrieben
|
||||
print("[nummernbuch] Eintragen fehlgeschlagen:", e)
|
||||
messagebox.showwarning(
|
||||
"Nummernbuch nicht erreichbar",
|
||||
"Die Rechnung ist geschrieben, konnte aber nicht ins gemeinsame "
|
||||
f"Nummernbuch eingetragen werden:\n{e}\n\n"
|
||||
"Das Steuerjournal sieht sie trotzdem - es liest die PDFs.")
|
||||
|
||||
def _nachtragen(self):
|
||||
"""Alles, was schon im Rechnungsordner liegt, ins Buch schreiben.
|
||||
|
||||
Laeuft beim Einrichten des Ordners einmal durch - sonst faengt das Buch
|
||||
bei null an und kennt die bisherigen Nummern nicht.
|
||||
"""
|
||||
if self.buch is None:
|
||||
return
|
||||
anzahl = 0
|
||||
for meta in rechnungen_im_ordner(config.ensure_output_dir(self.cfg)):
|
||||
nummer = str(meta.get("rechnungsnummer") or "")
|
||||
if not nummer:
|
||||
continue
|
||||
datum = parse_date(str(meta.get("datum", "")))
|
||||
try:
|
||||
jahr = int(nummer.split("-")[0])
|
||||
except (ValueError, IndexError):
|
||||
jahr = datum.year if datum else 0
|
||||
try:
|
||||
self.buch.eintragen(
|
||||
nummer, jahr=jahr,
|
||||
datum=datum.isoformat() if datum else "",
|
||||
nachname=str(meta.get("nachname", "") or ""),
|
||||
vorname=str(meta.get("vorname", "") or ""),
|
||||
naechte=int(parse_num(str(meta.get("naechte") or 0)) or 0),
|
||||
zwischensumme=float(meta.get("zwischensumme") or 0),
|
||||
steuer_satz=float(meta.get("steuer_satz") or 0),
|
||||
steuer_betrag=float(meta.get("steuer_betrag") or 0),
|
||||
gesamt=float(meta.get("gesamt") or 0),
|
||||
art=str(meta.get("art", "rechnung") or "rechnung"),
|
||||
vorgang=str(meta.get("vorgang", "") or ""),
|
||||
storno_zu=str(meta.get("storno_zu", "") or ""),
|
||||
ersetzt=str(meta.get("ersetzt", "") or ""),
|
||||
berichtigt_zu=str(meta.get("berichtigt_zu", "") or ""),
|
||||
vorlage=str(meta.get("vorlage", "") or ""),
|
||||
quelle="rechnungstool", herkunft="pdf")
|
||||
anzahl += 1
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("[nummernbuch] Nachtragen fehlgeschlagen:", e)
|
||||
break
|
||||
if anzahl:
|
||||
print(f"[nummernbuch] {anzahl} vorhandene Rechnungen nachgetragen")
|
||||
|
||||
# --------------------------------------------------------- Berechnung
|
||||
def _eingaben(self):
|
||||
e = {}
|
||||
|
|
@ -569,7 +745,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
"""
|
||||
if self.storno_stand or self.berichtigung_stand:
|
||||
return
|
||||
frei = naechste_freie_nummer(config.ensure_output_dir(self.cfg), self.cfg)
|
||||
frei = naechste_freie_nummer(config.ensure_output_dir(self.cfg), self.cfg,
|
||||
self._belegte_nummern())
|
||||
if frei:
|
||||
self.r_nummer.set(frei)
|
||||
|
||||
|
|
@ -770,6 +947,17 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
print("[berichtigung] FEHLER:", e)
|
||||
return
|
||||
print(f"[berichtigung] erstellt -> {pfad}")
|
||||
# Das Blatt verbraucht keine Nummer und bucht nichts - im Buch wird
|
||||
# nur der berichtigte Name der Rechnung nachgezogen. Wuerde man hier
|
||||
# den ganzen Eintrag schreiben, stuenden dort die Nullen des Blattes.
|
||||
if self.buch is not None:
|
||||
try:
|
||||
self.buch.eintragen(ber["zu"],
|
||||
nachname=kopf.get("nachname", ""),
|
||||
vorname=kopf.get("vorname", ""),
|
||||
berichtigt_zu=ber["zu"])
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("[nummernbuch] Berichtigung nicht eingetragen:", e)
|
||||
self.berichtigung_stand = None
|
||||
self._storno_anzeigen()
|
||||
if messagebox.askyesno("Fertig",
|
||||
|
|
@ -784,9 +972,14 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
# Beleg ist im Pruefungsfall die einfachere Geschichte. Und jede
|
||||
# Nummer darf es nur einmal geben (§ 14 Abs. 4 Nr. 4 UStG). Also neue Nummer.
|
||||
vorhanden = vorhandene_rechnungen(out_dir, nummer)
|
||||
if vorhanden:
|
||||
# Auch das gemeinsame Nummernbuch zaehlt als vergeben: dort stehen die
|
||||
# Nummern des Steuerjournals, zu denen es hier gar keine PDF gibt.
|
||||
im_buch = nummer in self._belegte_nummern()
|
||||
if vorhanden or im_buch:
|
||||
liste = "\n".join("- " + os.path.basename(x) for x in vorhanden)
|
||||
frei = naechste_freie_nummer(out_dir, self.cfg)
|
||||
if im_buch and not vorhanden:
|
||||
liste = "- steht im gemeinsamen Nummernbuch (Steuerjournal)"
|
||||
frei = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
||||
if not frei:
|
||||
messagebox.showerror(
|
||||
"Rechnungsnummer schon vergeben",
|
||||
|
|
@ -823,6 +1016,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
|||
print("[pdf] FEHLER:", e)
|
||||
return
|
||||
|
||||
# Ins gemeinsame Nummernbuch, damit das Steuerjournal die Nummer kennt -
|
||||
# auch wenn es die PDF (noch) nicht eingelesen hat.
|
||||
self._ins_buch(nummer, kopf, r, pfad)
|
||||
self._buch_anzeigen()
|
||||
|
||||
# Rechnungsnummer-Zaehler hochsetzen, falls Format JJJJ-NNN
|
||||
self._zaehler_aus_nummer_speichern(nummer)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue