Im Journal wird nichts mehr von Hand geloescht

Sein Einwand: 'wenn man in der beherbergungstool ein datensatz loescht, bleibt
die in der sqlite - oder du patchst es raus, dass man das garnicht mehr loeschen
kann'.

Beides stimmt. Im gemeinsamen Nummernbuch BLEIBT die Nummer stehen, und das ist
richtig: vergeben ist vergeben, die Rechnung liegt beim Gast. Das Loeschen im
Journal gehoert dagegen weg - eine Zeile aus einer PDF kaeme beim naechsten
Einlesen ohnehin zurueck, eine Zeile aus dem Excel-Altbestand (quelle='xlsx')
gibt es dagegen nirgends sonst; die waere fuer immer weg, samt Uebernachtung im
Amtsbericht. § 146 Abs. 4 AO verbietet Aenderungen, bei denen der urspruengliche
Inhalt nicht mehr feststellbar ist.

Raus sind: Papierkorb-Knopf, Entf-Taste, Menuepunkt und die Haken-Spalte, die
nur dafuer da war. Damit ist das Monatsblatt reine Anzeige.

Der Weg, der bleibt: die PDF aus dem Rechnungsordner nehmen - dann raeumt der
Scan die Zeile von selbst weg, und nur dann, wenn das Laufwerk erreichbar ist
(ein abgezogenes NAS darf die Buchhaltung nicht leeren).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
TheMockTv 2026-09-03 22:43:23 +02:00
parent c230a27cff
commit 0218abaebe
3 changed files with 25 additions and 114 deletions

78
app.py
View file

@ -102,7 +102,6 @@ class App(tk.Tk):
self.bind("<F5>", lambda _e: self.einlesen())
self.bind("<Control-f>", lambda _e: self.suchfeld.focus_set())
self.bind("<Delete>", lambda _e: self.buchung_loeschen())
self.protocol("WM_DELETE_WINDOW", self._beenden)
self.aktualisieren()
@ -137,7 +136,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="Markierte Buchung löschen (Entf)", command=self.buchung_loeschen)
daten.add_separator()
daten.add_command(label="Steuersatz ändern…", command=self.satz_aendern)
daten.add_command(label="Firmendaten des Rechnungstools wählen…",
@ -160,9 +158,11 @@ class App(tk.Tk):
ttk.Button(leiste, text="Rechnungen einlesen", command=self.einlesen).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)
self.papierkorb.pack(side="left", padx=(6, 0))
# Kein Papierkorb mehr: im Journal wird nichts von Hand geloescht. Eine
# Zeile aus einer PDF kaeme beim naechsten Einlesen ohnehin zurueck, eine
# Zeile aus dem Excel-Altbestand waere fuer immer weg - samt Uebernachtung
# im Amtsbericht (§ 146 Abs. 4 AO). Soll eine Buchung raus, wandert die PDF
# aus dem Rechnungsordner; den Rest macht der Scan von selbst.
ttk.Label(leiste, text="Suche:").pack(side="left", padx=(20, 4))
self.suchfeld = ttk.Entry(leiste, textvariable=self.suche, width=22)
@ -175,7 +175,7 @@ class App(tk.Tk):
self.blaetter.pack(fill="both", expand=True, padx=10, pady=(0, 6))
self.monate = {}
for m in range(1, 13):
tab = MonatsTab(self.blaetter, m, bei_haken=self.papierkorb_auffrischen)
tab = MonatsTab(self.blaetter, m)
self.blaetter.add(tab, text=MONATE[m - 1])
self.monate[m] = tab
self.blaetter.select(min(date.today().month, 12) - 1)
@ -262,7 +262,6 @@ class App(tk.Tk):
self.jahr_summe.setzen(summiere(sichtbar), sichtbar)
self.jahr_wahl.config(values=self._jahre())
self.papierkorb_auffrischen()
self.ordner_anzeige.config(
text=f"Rechnungen: {self.pdf_ordner}" if self.pdf_ordner
else "kein Rechnungs-Ordner gewählt (Datei → Rechnungs-Ordner wählen…)")
@ -588,71 +587,6 @@ class App(tk.Tk):
"Nicht eingelesen",
f"{len(fehler)} Datei(en) sind keine lesbare Rechnung:\n\n{namen}",
parent=self)
# Im Journal wird NICHTS von Hand geaendert - weder angelegt noch bearbeitet.
# Jede Zeile gehoert zu einer Rechnung aus dem Rechnungstool; wer hier Betrag
# oder Nummer verstellt, meldet dem Amt etwas anderes, als auf dem Beleg
# steht, und beim naechsten Einlesen kaeme die Aenderung ohnehin zurueck.
# Stimmt eine Rechnung nicht, wird sie im Rechnungstool storniert oder
# berichtigt. Loeschen bleibt moeglich (Entf / Papierkorb) - es entfernt nur
# die Zeile aus dem Zwischenspeicher, die PDF bleibt liegen.
def _markierte(self):
return self.monate[self._monat].markierte()
def buchung_loeschen(self):
b = self._markierte()
if not b:
return
if not messagebox.askyesno(
"Buchung löschen",
f"Rechnung {b.voll_nummer} ({b.nachname}) aus dem Journal löschen?\n\n"
"Die PDF-Datei bleibt erhalten – beim nächsten Einlesen "
"taucht die Buchung wieder auf.", parent=self):
return
self.journal.loeschen(b.id)
self.aktualisieren()
self.melde(f"Buchung {b.voll_nummer} gelöscht")
# ---- Anhaken + Papierkorb ----------------------------------------------
def _angehakte(self):
"""Alle angehakten Buchungen – über alle Monate hinweg."""
return [b for m in range(1, 13) for b in self.monate[m].angehakte()]
def papierkorb_auffrischen(self):
anzahl = len(self._angehakte())
self.papierkorb.config(
text=f"🗑 Löschen ({anzahl})" if anzahl else "🗑 Löschen",
state="normal" if anzahl else "disabled")
def angehakte_loeschen(self):
angehakt = self._angehakte()
if not angehakt:
return
aus_pdf = [b for b in angehakt if b.quelle == "pdf"]
namen = "\n".join(f"• {b.datum.strftime('%d.%m.%Y')} {b.voll_nummer} "
f"{b.nachname or 'ohne Namen'}" for b in angehakt[:15])
if len(angehakt) > 15:
namen += f"\n… und {len(angehakt) - 15} weitere"
hinweis = ""
if aus_pdf:
hinweis = (f"\n\n{len(aus_pdf)} davon stammen aus einer Rechnungs-PDF – die Dateien "
"bleiben liegen und tauchen beim nächsten Einlesen wieder auf.")
if not messagebox.askyesno(
"Angehakte Buchungen löschen",
f"{len(angehakt)} Buchung(en) aus dem Journal löschen?\n\n{namen}{hinweis}",
parent=self):
return
for b in angehakt:
self.journal.loeschen(b.id)
for m in range(1, 13):
self.monate[m].haken_loesen()
self.aktualisieren()
self.papierkorb_auffrischen()
self.melde(f"{len(angehakt)} Buchung(en) gelöscht")
def satz_aendern(self):
fenster = tk.Toplevel(self)
fenster.title("Steuersatz")

View file

@ -209,6 +209,19 @@ 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()

View file

@ -9,11 +9,7 @@ from tkinter import ttk, font as tkfont
from modell import summiere, eur, eur_kurz
HAKEN_AN = "☑"
HAKEN_AUS = "☐"
SPALTEN = [
("haken", HAKEN_AUS, 34, "center"), # zum Anhaken fürs Löschen
("datum", "Datum", 90, "center"),
("nummer", "Rechnung", 95, "center"),
("art", "Art", 132, "center"), # Storno zu ... / storniert / Neuausstellung
@ -87,14 +83,16 @@ class SummenLeiste(tk.Frame):
class MonatsTab(ttk.Frame):
"""Tabelle eines Monats. `bei_auswahl` wird mit der markierten Buchung gerufen."""
"""Tabelle eines Monats - reine Anzeige.
def __init__(self, master, monat: int, bei_haken=None):
Hier wird nichts angehakt und nichts geloescht: die Zahlen kommen aus den
Rechnungen, nicht aus diesem Fenster.
"""
def __init__(self, master, monat: int):
super().__init__(master, padding=(8, 8, 8, 6))
self.monat = monat
self.bei_haken = bei_haken # meldet dem Fenster, dass sich die Auswahl änderte
self._buchungen = []
self._angehakt = set() # ids der angehakten Buchungen
self.baum = ttk.Treeview(self, columns=[s[0] for s in SPALTEN],
show="headings", selectmode="browse")
@ -121,37 +119,6 @@ class MonatsTab(ttk.Frame):
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.baum.bind("<Button-1>", self._klick)
# Kein Doppelklick zum Bearbeiten: die Zahlen kommen aus der Rechnung,
# nicht aus diesem Fenster.
# ---- Anhaken -----------------------------------------------------------
def _klick(self, ereignis):
"""Klick in die Haken-Spalte schaltet die Zeile an/aus."""
if self.baum.identify_region(ereignis.x, ereignis.y) != "cell":
return
if self.baum.identify_column(ereignis.x) != "#1":
return
zeile = self.baum.identify_row(ereignis.y)
if not zeile:
return
b = self._buchungen[int(zeile)]
if b.id in self._angehakt:
self._angehakt.discard(b.id)
else:
self._angehakt.add(b.id)
self.baum.set(zeile, "haken", HAKEN_AN if b.id in self._angehakt else HAKEN_AUS)
if self.bei_haken:
self.bei_haken()
return "break" # kein Zeilen-Markieren durch den Haken-Klick
def angehakte(self):
return [b for b in self._buchungen if b.id in self._angehakt]
def haken_loesen(self):
self._angehakt.clear()
def markierte(self):
"""Aktuell markierte Buchung oder None."""
sel = self.baum.selection()
@ -174,8 +141,6 @@ class MonatsTab(ttk.Frame):
doppelte = set(doppelte)
aufgehoben = set(aufgehoben)
self._buchungen = sorted(buchungen, key=lambda b: (b.datum, b.rechnungsnummer))
# Haken nur für Zeilen behalten, die es noch gibt
self._angehakt &= {b.id for b in self._buchungen}
self._namensspalte_anpassen()
self.baum.delete(*self.baum.get_children())
for i, b in enumerate(self._buchungen):
@ -196,7 +161,6 @@ class MonatsTab(ttk.Frame):
else:
art_text = ""
self.baum.insert("", "end", iid=str(i), tags=marke, values=(
HAKEN_AN if b.id in self._angehakt else HAKEN_AUS,
b.datum.strftime("%d.%m.%Y"),
("⚠ " + b.voll_nummer) if zweimal else b.voll_nummer,
art_text,