Ersetzt die Excel-Mappe des Campinghofs. Die Rechnungs-PDFs des Rechnungstools sind die Quelle; SQLite ist nur Cache, Excel nur noch Import des Altbestands. Bericht fuers Amt als PDF im Briefkopf der Rechnungen (Firmendaten aus der config.json des Rechnungstools). - pdf_parser: liest Datum, Nummer, Name, Naechte, Zwischensumme, Satz - db: Journal + Scan mit Cache (Pfad/mtime/Groesse) - xlsx_io: Import der alten Tabelle, vereinheitlicht Rechnungsnummern - bericht_pdf: Monats- und Jahresbericht (reportlab) - GUI: Monatsreiter, Suche, Anhaken + Papierkorb, Kennzahlen-Leiste Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
5.9 KiB
Python
142 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Dialog für eine Buchung von Hand (z.B. Barzahlung ohne PDF-Rechnung)
|
||
und zum Nachbessern einer eingelesenen Zeile.
|
||
|
||
Schließen mit ESC, Speichern mit Enter oder Knopf.
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox
|
||
from datetime import datetime, date
|
||
|
||
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, jahr: int, satz: float = STANDARD_SATZ, buchung: Buchung = None):
|
||
super().__init__(master)
|
||
self.ergebnis = None
|
||
self.loeschen = False
|
||
self._vorlage = buchung
|
||
self._jahr = buchung.jahr if buchung else jahr
|
||
|
||
self.title("Buchung bearbeiten" if buchung else "Neue Buchung")
|
||
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") if buchung
|
||
else date.today().strftime("%d.%m.%Y"))),
|
||
("nummer", "Rechnungsnummer", buchung.rechnungsnummer if buchung else ""),
|
||
("nachname", "Nachname", buchung.nachname if buchung else ""),
|
||
("naechte", "Übernachtungen", str(buchung.naechte) if buchung else "1"),
|
||
("gezahlt", "Gezahlt (Zwischensumme)", f"{buchung.gezahlt:.2f}".replace(".", ",") if buchung else ""),
|
||
("satz", "Steuersatz %", f"{(buchung.satz if buchung else 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)
|
||
if buchung: # nur beim Bearbeiten – eine neue Buchung gibt es noch nicht zu löschen
|
||
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" if buchung else "nummer"].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 if self._vorlage else "manuell",
|
||
pdf_pfad=self._vorlage.pdf_pfad if self._vorlage else "",
|
||
id=self._vorlage.id if self._vorlage else 0,
|
||
)
|
||
self.destroy()
|