Steuerrechnungstool/gui_dialoge.py
TheMockTv 894bc81c46 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
2026-09-03 19:47:44 +02:00

144 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Dialog zum Nachbessern einer Zeile, die schon im Journal steht.
Buchungen von Hand ANLEGEN gibt es bewusst nicht mehr: jede Zeile gehört zu
einer Rechnung aus dem Rechnungstool. Wer eine Buchung braucht, schreibt die
Rechnung - sonst meldet das Journal ans Amt, was in keinem Rechnungsordner
steht.
Schließen mit ESC, Speichern mit Enter oder Knopf.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
from modell import (Buchung, STANDARD_SATZ, steuer_betrag, eur, zahl_aus_text,
zerlege_nummer)
class BuchungDialog(tk.Toplevel):
"""
Modal. Nach dem Schließen steht das Ergebnis in
.ergebnis – die gespeicherte Buchung (oder None, wenn abgebrochen)
.loeschen – True, wenn die Buchung gelöscht werden soll
"""
def __init__(self, master, buchung: Buchung, satz: float = STANDARD_SATZ):
super().__init__(master)
self.ergebnis = None
self.loeschen = False
self._vorlage = buchung
self._jahr = buchung.jahr
self.title("Buchung bearbeiten")
self.resizable(False, False)
self.transient(master)
self.grab_set()
rahmen = ttk.Frame(self, padding=14)
rahmen.grid(sticky="nsew")
self.felder = {}
zeilen = [
("datum", "Rechnungsdatum", buchung.datum.strftime("%d.%m.%Y")),
("nummer", "Rechnungsnummer", buchung.rechnungsnummer),
("nachname", "Nachname", buchung.nachname),
("naechte", "Übernachtungen", str(buchung.naechte)),
("gezahlt", "Gezahlt (Zwischensumme)", f"{buchung.gezahlt:.2f}".replace(".", ",")),
("satz", "Steuersatz %", f"{(buchung.satz or satz):g}".replace(".", ",")),
]
for i, (schluessel, beschriftung, wert) in enumerate(zeilen):
ttk.Label(rahmen, text=beschriftung + ":").grid(row=i, column=0, sticky="w", pady=3, padx=(0, 10))
e = ttk.Entry(rahmen, width=26)
e.insert(0, wert)
e.grid(row=i, column=1, sticky="ew", pady=3)
e.bind("<KeyRelease>", lambda _e: self._vorschau())
self.felder[schluessel] = e
self.vorschau = ttk.Label(rahmen, text="", foreground="black",
font=("Segoe UI", 10, "bold"), anchor="e")
self.vorschau.grid(row=len(zeilen), column=0, columnspan=2, sticky="ew", pady=(10, 4))
knoepfe = ttk.Frame(rahmen)
knoepfe.grid(row=len(zeilen) + 1, column=0, columnspan=2, sticky="ew", pady=(6, 0))
ttk.Button(knoepfe, text="Speichern", command=self._speichern) \
.pack(side="left", fill="x", expand=True)
ttk.Button(knoepfe, text="Löschen", command=self._loeschen) \
.pack(side="left", fill="x", expand=True, padx=(8, 0))
rahmen.columnconfigure(1, weight=1)
self.bind("<Escape>", lambda _e: self.destroy())
self.bind("<Return>", lambda _e: self._speichern())
self.felder["datum"].focus_set()
self._vorschau()
self.update_idletasks()
x = master.winfo_rootx() + (master.winfo_width() - self.winfo_width()) // 2
y = master.winfo_rooty() + 120
self.geometry(f"+{max(0, x)}+{max(0, y)}")
# ---- Eingaben lesen ----------------------------------------------------
def _werte(self):
gezahlt = zahl_aus_text(self.felder["gezahlt"].get())
satz = zahl_aus_text(self.felder["satz"].get())
return gezahlt, satz
def _vorschau(self):
gezahlt, satz = self._werte()
self.vorschau.config(text=f"Beherbergungssteuer: {eur(steuer_betrag(gezahlt, satz))}")
def _loeschen(self):
b = self._vorlage
hinweis = ("\n\nDie Rechnungs-PDF bleibt liegen – beim nächsten Einlesen "
"taucht die Buchung wieder auf.") if b.quelle == "pdf" else ""
if not messagebox.askyesno(
"Buchung löschen",
f"Rechnung {b.voll_nummer} ({b.nachname or 'ohne Namen'}) "
f"aus dem Journal löschen?{hinweis}", parent=self):
return
self.loeschen = True
self.destroy()
def _speichern(self):
text_datum = self.felder["datum"].get().strip()
datum = None
for fmt in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d"):
try:
datum = datetime.strptime(text_datum, fmt).date()
break
except ValueError:
continue
if not datum:
messagebox.showerror("Datum", f"Datum nicht lesbar: {text_datum}\n"
"Erwartet wird z.B. 12.07.2026", parent=self)
return
# "238" und "2026-238" werden beide angenommen
jahr, nummer = zerlege_nummer(self.felder["nummer"].get(), datum.year)
if not nummer:
messagebox.showerror("Rechnungsnummer", "Bitte eine Rechnungsnummer eintragen "
"(z.B. 238).", parent=self)
return
gezahlt, satz = self._werte()
if gezahlt <= 0:
messagebox.showerror("Gezahlt", "Bitte den Rechnungsbetrag (Zwischensumme) "
"eintragen.", parent=self)
return
self.ergebnis = Buchung(
datum=datum,
rechnungsnummer=nummer,
jahr=jahr,
nachname=self.felder["nachname"].get().strip(),
naechte=int(zahl_aus_text(self.felder["naechte"].get()) or 0),
gezahlt=gezahlt,
satz=satz,
quelle=self._vorlage.quelle,
pdf_pfad=self._vorlage.pdf_pfad,
id=self._vorlage.id,
)
self.destroy()