Doppelte Rechnungsnummern zeigen, nur ein Fenster, keine Handbuchungen
Doppelte Rechnungsnummern werden ANGEZEIGT statt still mitgeschleppt: rote Zeile mit Warnzeichen, markierter Monatsreiter, rote Zeile unter der Jahressumme und eine Liste aller Belege mit Dateinamen. Gerechnet wird ueber das ganze Jahr - liegt die zweite Rechnung im naechsten Monat, sieht kein Monat fuer sich eine Dublette, das Amt bekommt sie trotzdem doppelt. Die Betraege bleiben in allen Summen und im Amtsbericht; gemeldet wird der Fund, korrigiert wird nichts von allein. Vorgeschichte: bis v1.2 war die Rechnungsnummer der Schluessel der Tabelle - die zweite Rechnung ueberschrieb die erste still und fehlte im Amtsbericht. v1.3 hat den Schluessel auf die PDF-Datei umgestellt, seitdem bleiben beide Zeilen stehen. Erst jetzt sieht man den Fall auch. Einzelinstanz aus dem Steuerrechner uebernommen (nur Mutex- und PID-Name geaendert): zwei Fenster lesen denselben Rechnungsordner in dieselbe journal.sqlite3 - was das eine loescht, steht im anderen noch da, und wer dort "PDF fuers Amt" drueckt, meldet einen Stand, den es nicht mehr gibt. Die Sperre greift vor ablage.daten_ordner() und nur beim echten Start. "Neue Buchung" ist raus: jede Zeile gehoert zu einer Rechnung aus dem Rechnungstool. Wer eine Buchung braucht, schreibt die Rechnung. Bestehende Zeilen lassen sich weiter bearbeiten. Pruefstaende: pruef_doppelte.py (27 Pruefungen am echten Fenster), pruef_einzelinstanz.py (echte Prozesse). Beide gruen, dazu Abnahme mit den Augen an Hauptfenster, Liste und Bearbeiten-Dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
parent
de71fce64f
commit
894bc81c46
8 changed files with 707 additions and 47 deletions
156
app.py
156
app.py
|
|
@ -17,13 +17,38 @@ from datetime import date
|
|||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
|
||||
# Nur EIN Fenster. Zwei Instanzen lesen denselben Rechnungsordner in dieselbe
|
||||
# journal.sqlite3: was das eine loescht, steht im anderen noch da, und der
|
||||
# Bericht fuers Amt kaeme aus dem veralteten Fenster. Die Sperre steht VOR
|
||||
# ablage.daten_ordner() - der legt beim ersten Start den Datenordner an und
|
||||
# zieht alte Dateien um; das darf nicht zweimal gleichzeitig laufen.
|
||||
# Nur beim echten Start: Pruefstaende importieren app als Modul und sollen
|
||||
# nicht an der Sperre des laufenden Programms scheitern.
|
||||
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen,
|
||||
fenster_nach_vorn, laufende_pid_lesen)
|
||||
|
||||
_SPERRE = None
|
||||
if __name__ == "__main__":
|
||||
_SPERRE = einzelinstanz_pruefen()
|
||||
if _SPERRE is False:
|
||||
if not fenster_nach_vorn(laufende_pid_lesen()):
|
||||
_wurzel = tk.Tk()
|
||||
_wurzel.withdraw()
|
||||
_wurzel.attributes("-topmost", True)
|
||||
messagebox.showerror(
|
||||
"Beherbergungssteuer läuft bereits",
|
||||
"Das Steuerjournal ist schon geöffnet, das Fenster ließ sich aber "
|
||||
"nicht nach vorn holen – bitte über die Taskleiste zurückwechseln.")
|
||||
_wurzel.destroy()
|
||||
sys.exit(0)
|
||||
|
||||
import db
|
||||
import xlsx_io
|
||||
import firma as firma_lib
|
||||
import bericht_pdf
|
||||
import ablage
|
||||
from modell import MONATE, STANDARD_SATZ, summiere, eur
|
||||
from gui_monat import MonatsTab, SummenLeiste
|
||||
from modell import MONATE, STANDARD_SATZ, summiere, eur, eur_kurz, doppelte_nummern
|
||||
from gui_monat import MonatsTab, SummenLeiste, WARNUNG
|
||||
from gui_dialoge import BuchungDialog
|
||||
|
||||
# Als EXE liegt alles in einem eigenen Datenordner - siehe ablage.py.
|
||||
|
|
@ -88,7 +113,6 @@ class App(tk.Tk):
|
|||
daten.add_command(label="Rechnungen einlesen (F5)", command=self.einlesen)
|
||||
daten.add_command(label="Alle Rechnungen neu einlesen", command=lambda: self.einlesen(voll=True))
|
||||
daten.add_separator()
|
||||
daten.add_command(label="Buchung von Hand anlegen…", command=self.buchung_anlegen)
|
||||
daten.add_command(label="Markierte Buchung löschen (Entf)", command=self.buchung_loeschen)
|
||||
daten.add_separator()
|
||||
daten.add_command(label="Steuersatz ändern…", command=self.satz_aendern)
|
||||
|
|
@ -108,8 +132,7 @@ class App(tk.Tk):
|
|||
self.jahr_wahl.bind("<<ComboboxSelected>>", lambda _e: self.jahr_gewechselt())
|
||||
|
||||
ttk.Button(leiste, text="Rechnungen einlesen", command=self.einlesen).pack(side="left")
|
||||
ttk.Button(leiste, text="Neue Buchung", command=self.buchung_anlegen).pack(side="left", padx=6)
|
||||
ttk.Button(leiste, text="PDF fürs Amt", command=self.bericht_monat).pack(side="left")
|
||||
ttk.Button(leiste, text="PDF fürs Amt", command=self.bericht_monat).pack(side="left", padx=(6, 0))
|
||||
|
||||
self.papierkorb = ttk.Button(leiste, text="🗑 Löschen", state="disabled",
|
||||
command=self.angehakte_loeschen)
|
||||
|
|
@ -140,13 +163,26 @@ class App(tk.Tk):
|
|||
self.jahr_summe = SummenLeiste(rahmen, mit_schnitt=True)
|
||||
self.jahr_summe.pack(fill="x", pady=(2, 4))
|
||||
|
||||
fuss = ttk.Frame(rahmen)
|
||||
fuss.pack(fill="x")
|
||||
self.status = ttk.Label(fuss, text="", foreground="black")
|
||||
self.fuss = ttk.Frame(rahmen)
|
||||
self.fuss.pack(fill="x")
|
||||
self.status = ttk.Label(self.fuss, text="", foreground="black")
|
||||
self.status.pack(side="left")
|
||||
self.ordner_anzeige = ttk.Label(fuss, text="", foreground="black")
|
||||
self.ordner_anzeige = ttk.Label(self.fuss, text="", foreground="black")
|
||||
self.ordner_anzeige.pack(side="right")
|
||||
|
||||
# Doppelt vergebene Rechnungsnummern. Die Leiste wird erst eingehaengt,
|
||||
# wenn es wirklich welche gibt - im Normalfall soll hier nichts stehen
|
||||
# und auch kein leerer Streifen Platz wegnehmen.
|
||||
self._doppelte = {}
|
||||
self.doppelt_leiste = ttk.Frame(rahmen)
|
||||
self.knopf_doppelte = ttk.Button(self.doppelt_leiste,
|
||||
text="Doppelte Rechnungen anzeigen…",
|
||||
command=self.doppelte_zeigen)
|
||||
self.knopf_doppelte.pack(side="right", padx=(10, 0))
|
||||
self.warnung = tk.Label(self.doppelt_leiste, text="", fg=WARNUNG, anchor="w",
|
||||
justify="left", wraplength=760, font=("Segoe UI", 9))
|
||||
self.warnung.pack(side="left", fill="x", expand=True)
|
||||
|
||||
# ----------------------------------------------------------- Anzeige ----
|
||||
def _jahre(self):
|
||||
vorhanden = set(self.journal.jahre()) | {date.today().year, self.jahr.get()}
|
||||
|
|
@ -167,8 +203,17 @@ class App(tk.Tk):
|
|||
begriff = self.suche.get().strip().lower()
|
||||
sichtbar = [b for b in alle if self._passt(b, begriff)] if begriff else alle
|
||||
|
||||
# Ueber ALLE Buchungen des Jahres, nicht ueber die gefilterten: sonst
|
||||
# versteckt die Suche den zweiten Beleg und die Dublette waere weg.
|
||||
self._doppelte = doppelte_nummern(alle)
|
||||
|
||||
for m in range(1, 13):
|
||||
self.monate[m].fuellen([b for b in sichtbar if b.monat == m])
|
||||
self.monate[m].fuellen([b for b in sichtbar if b.monat == m], self._doppelte)
|
||||
# Reiter des Monats markieren, damit man die Dublette auch dann
|
||||
# sieht, wenn gerade ein anderer Monat offen ist.
|
||||
betroffen = any(b.voll_nummer in self._doppelte for b in alle if b.monat == m)
|
||||
self.blaetter.tab(m - 1, text=("⚠ " if betroffen else "") + MONATE[m - 1])
|
||||
self._doppelte_melden()
|
||||
|
||||
if begriff:
|
||||
self.jahr_titel.config(text=f"Jahr {jahr} – Treffer für „{self.suche.get().strip()}“")
|
||||
|
|
@ -194,6 +239,78 @@ class App(tk.Tk):
|
|||
self.status.config(text=text)
|
||||
log.info(text)
|
||||
|
||||
# ---- Doppelt vergebene Rechnungsnummern --------------------------------
|
||||
def _doppelte_melden(self):
|
||||
"""Warnleiste zeigen, solange es Dubletten gibt - sonst verschwindet sie."""
|
||||
if not self._doppelte:
|
||||
self.doppelt_leiste.pack_forget()
|
||||
self.warnung.config(text="")
|
||||
return
|
||||
teile = []
|
||||
for nr, liste in list(self._doppelte.items())[:3]:
|
||||
namen = ", ".join(sorted({b.nachname or "?" for b in liste}))
|
||||
teile.append(f"{nr} ({len(liste)}× – {namen})")
|
||||
rest = len(self._doppelte) - len(teile)
|
||||
self.warnung.config(text=(
|
||||
f"⚠ {len(self._doppelte)} Rechnungsnummer(n) doppelt vergeben: "
|
||||
+ "; ".join(teile) + (f" … und {rest} weitere" if rest else "")
|
||||
+ ". Die Beträge stehen trotzdem in allen Summen und im Amtsbericht – "
|
||||
"hier wird nichts stillschweigend weggerechnet."))
|
||||
self.doppelt_leiste.pack(fill="x", pady=(0, 4), before=self.fuss)
|
||||
|
||||
def doppelte_zeigen(self):
|
||||
"""Alle Belege auflisten, die sich eine Rechnungsnummer teilen."""
|
||||
fenster = tk.Toplevel(self)
|
||||
fenster.title("Doppelt vergebene Rechnungsnummern")
|
||||
fenster.transient(self)
|
||||
fenster.geometry("940x520")
|
||||
rahmen = ttk.Frame(fenster, padding=12)
|
||||
rahmen.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(rahmen, wraplength=900, justify="left", text=(
|
||||
f"Jahr {self.jahr.get()}: diese Rechnungsnummern kommen mehrfach vor. "
|
||||
"Jede Nummer darf es nur einmal geben (§ 14 Abs. 4 Nr. 4 UStG).\n\n"
|
||||
"Alle Beträge sind in den Summen und im Bericht fürs Amt enthalten – hier wird "
|
||||
"nichts weggerechnet. Zwei Zeilen zur selben Rechnung (z. B. einmal aus dem "
|
||||
"Excel-Altbestand und einmal aus der PDF) melden die Übernachtung doppelt ans "
|
||||
"Amt; zwei verschiedene Gäste unter einer Nummer sind ein Fehler im "
|
||||
"Rechnungstool. Was zu tun ist, entscheidest du – das Programm ändert nichts "
|
||||
"von allein.")).pack(anchor="w", pady=(0, 10))
|
||||
|
||||
innen = ttk.Frame(rahmen)
|
||||
innen.pack(fill="both", expand=True)
|
||||
innen.rowconfigure(0, weight=1)
|
||||
innen.columnconfigure(0, weight=1)
|
||||
senkrecht = ttk.Scrollbar(innen, orient="vertical")
|
||||
senkrecht.grid(row=0, column=1, sticky="ns")
|
||||
# Waagerecht, weil am Zeilenende der Dateiname steht - genau der sagt,
|
||||
# welche der beiden Rechnungen die zweite ist.
|
||||
waagerecht = ttk.Scrollbar(innen, orient="horizontal")
|
||||
waagerecht.grid(row=1, column=0, sticky="ew")
|
||||
text = tk.Text(innen, wrap="none", yscrollcommand=senkrecht.set,
|
||||
xscrollcommand=waagerecht.set, relief="flat",
|
||||
font=("Consolas", 9))
|
||||
text.grid(row=0, column=0, sticky="nsew")
|
||||
senkrecht.configure(command=text.yview)
|
||||
waagerecht.configure(command=text.xview)
|
||||
text.tag_configure("kopf", foreground=WARNUNG, font=("Consolas", 9, "bold"))
|
||||
|
||||
for nr, liste in self._doppelte.items():
|
||||
text.insert("end", f"{nr} – {len(liste)} Belege\n", "kopf")
|
||||
for b in liste:
|
||||
datei = os.path.basename(b.pdf_pfad) if b.pdf_pfad else f"({b.quelle})"
|
||||
text.insert("end",
|
||||
f" {b.datum:%d.%m.%Y} {(b.nachname or '?'):22.22s} "
|
||||
f"{b.naechte:>2} {'Nacht ' if b.naechte == 1 else 'Nächte'} "
|
||||
f"{eur_kurz(b.gezahlt):>10s} "
|
||||
f"Steuer {eur_kurz(b.steuer):>8s} {datei}\n")
|
||||
text.insert("end", "\n")
|
||||
text.configure(state="disabled")
|
||||
|
||||
ttk.Button(rahmen, text="Schließen", command=fenster.destroy).pack(side="right",
|
||||
pady=(10, 0))
|
||||
fenster.bind("<Escape>", lambda _e: fenster.destroy())
|
||||
|
||||
def jahr_gewechselt(self):
|
||||
self.journal.setze("jahr", self.jahr.get())
|
||||
self.aktualisieren()
|
||||
|
|
@ -242,18 +359,12 @@ class App(tk.Tk):
|
|||
f"{len(fehler)} Datei(en) sind keine lesbare Rechnung:\n\n{namen}",
|
||||
parent=self)
|
||||
|
||||
def buchung_anlegen(self):
|
||||
d = BuchungDialog(self, jahr=self.jahr.get(), satz=self.satz)
|
||||
self.wait_window(d)
|
||||
if d.ergebnis:
|
||||
self.journal.speichern(d.ergebnis)
|
||||
self.jahr.set(d.ergebnis.jahr)
|
||||
self.aktualisieren()
|
||||
self.blaetter.select(d.ergebnis.monat - 1)
|
||||
self.melde(f"Buchung {d.ergebnis.voll_nummer} gespeichert")
|
||||
|
||||
# Es gibt bewusst KEIN "Buchung von Hand anlegen" mehr: jede Zeile im Journal
|
||||
# gehoert zu einer Rechnung aus dem Rechnungstool. Wer eine Buchung braucht,
|
||||
# schreibt die Rechnung - sonst meldet das Journal ans Amt, was in keinem
|
||||
# Rechnungsordner steht. Bestehende Zeilen lassen sich weiter bearbeiten.
|
||||
def buchung_bearbeiten(self, buchung):
|
||||
d = BuchungDialog(self, jahr=buchung.jahr, satz=self.satz, buchung=buchung)
|
||||
d = BuchungDialog(self, buchung=buchung, satz=self.satz)
|
||||
self.wait_window(d)
|
||||
if d.loeschen:
|
||||
self.journal.loeschen(buchung.id)
|
||||
|
|
@ -470,4 +581,7 @@ class App(tk.Tk):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.info("Beherbergungssteuer startet (Python %s)", sys.version.split()[0])
|
||||
# Die Sperre selbst greift schon ganz oben, vor dem Datenordner.
|
||||
eigene_pid_merken()
|
||||
App().mainloop()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue