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
144 lines
5.7 KiB
Python
144 lines
5.7 KiB
Python
# -*- 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()
|