Desktop-Programm (Python/Tkinter): Kundendaten + Leistungen eingeben, druckfertige PDF-Rechnung im Briefkopf des Betriebs. Optionale Beherbergungssteuer wird eigen ausgewiesen und bekommt ein zweites Blatt fuers Amt. Firmendaten, Preise und Steuersaetze stehen in der config.json neben dem Programm (nicht im Repo) und werden im Menue "Einstellungen" gepflegt. Im Code stehen nur Platzhalter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
925 lines
43 KiB
Python
925 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Rechnungstool Campinghof Bartl - Desktop-GUI (Tkinter).
|
||
|
||
Bedienung:
|
||
1. Kundenadresse + An-/Abreise eingeben
|
||
2. bei den passenden Leistungen Anzahl (und ggf. Personen) eintragen
|
||
3. "PDF erstellen" -> druckfertige Rechnung im Ordner rechnungen/
|
||
|
||
Steuersaetze, Bettensteuer, Preise und Firmendaten -> Menue "Einstellungen".
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import datetime
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, filedialog
|
||
|
||
import config
|
||
import berechnung
|
||
import pdf_renderer
|
||
|
||
|
||
def parse_num(s):
|
||
"""'12', '3,5' -> float; leer/ungueltig -> 0.0"""
|
||
s = (s or "").strip().replace(",", ".")
|
||
if not s:
|
||
return 0.0
|
||
try:
|
||
return float(s)
|
||
except ValueError:
|
||
return 0.0
|
||
|
||
|
||
def parse_date(s):
|
||
"""'28.01.2026' -> date; sonst None"""
|
||
s = (s or "").strip()
|
||
for fmt in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d"):
|
||
try:
|
||
return datetime.datetime.strptime(s, fmt).date()
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
|
||
|
||
def safe_filename(s):
|
||
keep = "-_.() "
|
||
return "".join(c for c in s if c.isalnum() or c in keep).strip() or "Rechnung"
|
||
|
||
|
||
def windows_dark_mode():
|
||
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
|
||
if not sys.platform.startswith("win"):
|
||
return False
|
||
try:
|
||
import winreg
|
||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") as k:
|
||
return winreg.QueryValueEx(k, "AppsUseLightTheme")[0] == 0
|
||
except OSError:
|
||
return False
|
||
|
||
|
||
class RechnungsApp(tk.Tk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.cfg = config.load_config()
|
||
self.title("Rechnungstool - " + self.cfg["firma"].get("name", "Campinghof"))
|
||
# Fensterhoehe an den Bildschirm anpassen, damit nichts unter den Rand rutscht
|
||
hoehe = max(560, min(840, self.winfo_screenheight() - 90))
|
||
self.geometry(f"1060x{hoehe}")
|
||
self.minsize(900, 540)
|
||
|
||
self.anzahl_vars = {} # nr -> StringVar
|
||
self.personen_vars = {} # nr -> StringVar
|
||
self.gesamt_lbls = {} # nr -> Label
|
||
|
||
self._apply_theme() # Farben nach Windows hell/dunkel
|
||
self._build_menu()
|
||
self._build_kopf()
|
||
self._build_summen() # untere Leiste (Summen + Buttons) zuerst -> bleibt immer sichtbar
|
||
self._build_positionen() # Positionsliste fuellt den Rest und scrollt
|
||
|
||
self._neue_nummer_vorschlagen()
|
||
self.recalc()
|
||
|
||
# Titelleiste dunkel (nach dem Mappen des Fensters)
|
||
self.after(60, lambda: self._dark_titlebar(self))
|
||
# Beim ersten Start nach dem Speicherort fragen (wenn noch keiner gesetzt ist)
|
||
self.after(250, self._frage_speicherort_falls_noetig)
|
||
|
||
# ----------------------------------------------------------------- Theme
|
||
def _apply_theme(self):
|
||
"""Farben passend zum Windows-Theme (hell/dunkel) setzen."""
|
||
dark = windows_dark_mode()
|
||
self._dark_mode = dark
|
||
if dark:
|
||
self.col_bg = "#23272e" # Fenster-Hintergrund
|
||
self.col_fg = "#e6e6e6" # Schrift
|
||
self.col_field = "#2f343c" # Eingabefelder
|
||
self.col_sel = "#3a4a44" # aktiv/hover
|
||
self.col_accent = "#5fb98f" # Akzent (Gruen, hell)
|
||
self.col_line = "#3a3f47"
|
||
self.col_scroll = "#4a505a" # Scroll-Griff
|
||
self.col_muted = "#8a929c" # gedaempfte Schrift (z.B. "powered by")
|
||
else:
|
||
self.col_bg = "#f3f3f3"
|
||
self.col_fg = "#1a1a1a"
|
||
self.col_field = "#ffffff"
|
||
self.col_sel = "#cfe3d8"
|
||
self.col_accent = "#1f5c3d"
|
||
self.col_line = "#cccccc"
|
||
self.col_scroll = "#b8b8b8"
|
||
self.col_muted = "#777777" # gedaempfte Schrift (z.B. "powered by")
|
||
|
||
self.configure(bg=self.col_bg)
|
||
style = ttk.Style(self)
|
||
try:
|
||
style.theme_use("clam")
|
||
except tk.TclError:
|
||
pass
|
||
bg, fg, field, sel, accent = (self.col_bg, self.col_fg, self.col_field,
|
||
self.col_sel, self.col_accent)
|
||
style.configure(".", background=bg, foreground=fg, fieldbackground=field,
|
||
bordercolor=self.col_line, lightcolor=bg, darkcolor=bg)
|
||
style.configure("TFrame", background=bg)
|
||
style.configure("TLabel", background=bg, foreground=fg)
|
||
style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
|
||
style.configure("TLabelframe.Label", background=bg, foreground=accent)
|
||
style.configure("TButton", background=field, foreground=fg, bordercolor=self.col_line)
|
||
style.map("TButton", background=[("active", sel)], foreground=[("active", fg)])
|
||
style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat",
|
||
arrowcolor=bg, padding=(10, 3))
|
||
style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)])
|
||
style.configure("TCheckbutton", background=bg, foreground=fg)
|
||
style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])
|
||
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg)
|
||
style.configure("TCombobox", fieldbackground=field, foreground=fg,
|
||
background=field, arrowcolor=fg)
|
||
style.map("TCombobox", fieldbackground=[("readonly", field)],
|
||
foreground=[("readonly", fg)], background=[("readonly", field)])
|
||
for sbs in ("Vertical.TScrollbar", "Horizontal.TScrollbar", "TScrollbar"):
|
||
style.configure(sbs, troughcolor=bg, background=self.col_scroll,
|
||
bordercolor=bg, lightcolor=bg, darkcolor=bg,
|
||
arrowcolor=fg, relief="flat", gripcount=0)
|
||
style.map(sbs, background=[("active", accent)],
|
||
troughcolor=[("disabled", bg)])
|
||
style.configure("TSeparator", background=self.col_line)
|
||
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
|
||
self.option_add("*background", bg)
|
||
self.option_add("*foreground", fg)
|
||
|
||
def _style_dialog(self, d):
|
||
"""Toplevel-Dialog an das Theme angleichen."""
|
||
d.configure(bg=self.col_bg)
|
||
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
|
||
d.after(20, lambda: self._dark_titlebar(d))
|
||
|
||
def _dark_titlebar(self, win):
|
||
"""Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode)."""
|
||
if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)):
|
||
return
|
||
try:
|
||
import ctypes
|
||
win.update_idletasks()
|
||
hwnd = ctypes.windll.user32.GetParent(win.winfo_id())
|
||
val = ctypes.c_int(1)
|
||
for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19)
|
||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
||
hwnd, attr, ctypes.byref(val), ctypes.sizeof(val))
|
||
# Frame neu zeichnen lassen (ohne Flackern/Fokusverlust)
|
||
SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED
|
||
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
|
||
except Exception as e: # noqa: BLE001
|
||
print("[theme] Titelleiste:", e)
|
||
|
||
# ---------------------------------------------------------------- Menue
|
||
def _build_menu(self):
|
||
# Vollstaendig eigene Menueleiste + eigene Dropdowns -> komplett theme-konform
|
||
# (native tk-Menubar/Popups lassen sich unter Windows nicht einfaerben).
|
||
self._open_pop = None
|
||
self._menus = {
|
||
"Datei": [
|
||
("PDF erstellen", self.pdf_erstellen),
|
||
("Formular zurücksetzen", self.reset_formular),
|
||
("Ausgabeordner öffnen", self._ordner_oeffnen),
|
||
("-", None),
|
||
("Beenden", self.destroy),
|
||
],
|
||
"Einstellungen": [
|
||
("Steuersätze…", self.dlg_steuern),
|
||
("Leistungskatalog / Preise…", self.dlg_katalog),
|
||
("Firmendaten, Logo & Texte…", self.dlg_firma),
|
||
("Rechnungsnummer…", self.dlg_nummer),
|
||
("Speicherort der Rechnungen…", self.dlg_speicherort),
|
||
],
|
||
"Hilfe": [("Info", self._info)],
|
||
}
|
||
bar = tk.Frame(self, bg=self.col_bg)
|
||
bar.pack(side="top", fill="x")
|
||
for text, items in self._menus.items():
|
||
lbl = tk.Label(bar, text=text, bg=self.col_bg, fg=self.col_fg, padx=8, pady=3)
|
||
lbl.pack(side="left")
|
||
lbl.bind("<Enter>", lambda e, w=lbl: w.configure(bg=self.col_sel))
|
||
lbl.bind("<Leave>", lambda e, w=lbl: w.configure(bg=self.col_bg))
|
||
lbl.bind("<Button-1>", lambda e, b=lbl, it=items: self._menu_popup(b, it))
|
||
ttk.Separator(self, orient="horizontal").pack(side="top", fill="x")
|
||
self.bind_all("<Control-p>", lambda e: self.pdf_erstellen())
|
||
|
||
def _menu_popup(self, anchor, items):
|
||
if self._open_pop: # offenes Menu zu -> Toggle
|
||
self._close_pop()
|
||
return
|
||
pop = tk.Toplevel(self)
|
||
pop.overrideredirect(True)
|
||
pop.configure(bg=self.col_line) # 1px Rahmen = col_line statt weiss
|
||
inner = tk.Frame(pop, bg=self.col_field)
|
||
inner.pack(padx=1, pady=1)
|
||
for label, cmd in items:
|
||
if label == "-":
|
||
tk.Frame(inner, bg=self.col_line, height=1).pack(fill="x", pady=3)
|
||
continue
|
||
it = tk.Label(inner, text=label, bg=self.col_field, fg=self.col_fg,
|
||
anchor="w", padx=16, pady=5)
|
||
it.pack(fill="x")
|
||
it.bind("<Enter>", lambda e, w=it: w.configure(bg=self.col_sel))
|
||
it.bind("<Leave>", lambda e, w=it: w.configure(bg=self.col_field))
|
||
it.bind("<Button-1>", lambda e, c=cmd: self._run_menu(c))
|
||
pop.update_idletasks()
|
||
pop.geometry(f"+{anchor.winfo_rootx()}+{anchor.winfo_rooty() + anchor.winfo_height()}")
|
||
self._open_pop = pop
|
||
pop.grab_set()
|
||
pop.bind("<Button-1>", self._maybe_close_pop, add="+")
|
||
pop.bind("<Escape>", lambda e: self._close_pop())
|
||
self._dark_titlebar(pop)
|
||
|
||
def _maybe_close_pop(self, event):
|
||
pop = self._open_pop
|
||
if not pop:
|
||
return
|
||
if (event.x_root < pop.winfo_rootx() or event.x_root > pop.winfo_rootx() + pop.winfo_width()
|
||
or event.y_root < pop.winfo_rooty() or event.y_root > pop.winfo_rooty() + pop.winfo_height()):
|
||
self._close_pop()
|
||
|
||
def _run_menu(self, cmd):
|
||
self._close_pop()
|
||
if cmd:
|
||
cmd()
|
||
|
||
def _close_pop(self):
|
||
if getattr(self, "_open_pop", None):
|
||
try:
|
||
self._open_pop.grab_release()
|
||
self._open_pop.destroy()
|
||
except tk.TclError:
|
||
pass
|
||
self._open_pop = None
|
||
|
||
# ---------------------------------------------------------------- Kopf
|
||
def _build_kopf(self):
|
||
top = ttk.Frame(self, padding=(12, 10, 12, 4))
|
||
top.pack(fill="x")
|
||
|
||
# Kunde (links)
|
||
kunde = ttk.LabelFrame(top, text="Rechnung an (Kunde)", padding=8)
|
||
kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
||
self.k_anrede = tk.StringVar()
|
||
self.k_name = tk.StringVar()
|
||
self.k_zusatz = tk.StringVar()
|
||
self.k_strasse = tk.StringVar()
|
||
self.k_plzort = tk.StringVar()
|
||
felder = [
|
||
("Anrede (optional)", self.k_anrede),
|
||
("Name / Firma", self.k_name),
|
||
("Zusatz (optional)", self.k_zusatz),
|
||
("Straße / Nr.", self.k_strasse),
|
||
("PLZ / Ort", self.k_plzort),
|
||
]
|
||
for i, (lbl, var) in enumerate(felder):
|
||
ttk.Label(kunde, text=lbl).grid(row=i, column=0, sticky="w", pady=2)
|
||
ttk.Entry(kunde, textvariable=var, width=38).grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
kunde.columnconfigure(1, weight=1)
|
||
|
||
# Rechnungsinfos (rechts)
|
||
info = ttk.LabelFrame(top, text="Rechnungsdaten", padding=8)
|
||
info.grid(row=0, column=1, sticky="nsew")
|
||
self.r_nummer = tk.StringVar()
|
||
self.r_datum = tk.StringVar(value=datetime.date.today().strftime("%d.%m.%Y"))
|
||
self.r_anreise = tk.StringVar()
|
||
self.r_abreise = tk.StringVar()
|
||
self.r_naechte = tk.StringVar(value="–")
|
||
rows = [
|
||
("Rechnungsnummer", self.r_nummer),
|
||
("Rechnungsdatum", self.r_datum),
|
||
("Anreise (TT.MM.JJJJ)", self.r_anreise),
|
||
("Abreise (TT.MM.JJJJ)", self.r_abreise),
|
||
]
|
||
date_vars = (self.r_datum, self.r_anreise, self.r_abreise)
|
||
for i, (lbl, var) in enumerate(rows):
|
||
ttk.Label(info, text=lbl).grid(row=i, column=0, sticky="w", pady=2)
|
||
ent = ttk.Entry(info, textvariable=var, width=18)
|
||
ent.grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
if var in date_vars: # Datum: Punkte automatisch, Cursor ans Ende
|
||
ent.bind("<KeyRelease>", self._auto_datum_evt)
|
||
ttk.Label(info, text="Nächte").grid(row=4, column=0, sticky="w", pady=2)
|
||
ttk.Label(info, textvariable=self.r_naechte, font=("Segoe UI", 10, "bold")).grid(
|
||
row=4, column=1, sticky="w", padx=(8, 0))
|
||
info.columnconfigure(1, weight=1)
|
||
self.r_anreise.trace_add("write", lambda *a: self._update_naechte())
|
||
self.r_abreise.trace_add("write", lambda *a: self._update_naechte())
|
||
|
||
top.columnconfigure(0, weight=1)
|
||
top.columnconfigure(1, weight=0)
|
||
|
||
def _auto_datum_evt(self, event):
|
||
"""Punkte automatisch setzen, Cursor ans Ende. Nach Tag+Monat das aktuelle
|
||
Jahr ergänzen (1206 -> 12.06.2026, Jahr aus der Systemuhr). Backspace/Pfeile
|
||
lassen normales Editieren zu."""
|
||
if event.keysym in ("BackSpace", "Delete", "Left", "Right", "Home", "End", "Tab"):
|
||
return
|
||
e = event.widget
|
||
s = e.get()
|
||
d = "".join(c for c in s if c.isdigit())[:8]
|
||
if len(d) < 2:
|
||
out = d
|
||
elif len(d) == 2:
|
||
out = d + "." # Tag fertig -> Punkt
|
||
elif len(d) < 4:
|
||
out = f"{d[:2]}.{d[2:]}"
|
||
elif len(d) == 4:
|
||
out = f"{d[:2]}.{d[2:4]}.{datetime.date.today().year}" # Monat fertig -> Jahr automatisch
|
||
else:
|
||
out = f"{d[:2]}.{d[2:4]}.{d[4:]}" # eigenes Jahr getippt
|
||
if out != s:
|
||
e.delete(0, "end")
|
||
e.insert(0, out)
|
||
e.icursor("end")
|
||
if len(d) == 4: # Tag+Monat fertig, Jahr automatisch -> direkt ins nächste Feld
|
||
e.tk_focusNext().focus()
|
||
|
||
def _update_naechte(self):
|
||
a = parse_date(self.r_anreise.get())
|
||
b = parse_date(self.r_abreise.get())
|
||
if a and b and (b - a).days > 0:
|
||
self.r_naechte.set(str((b - a).days))
|
||
else:
|
||
self.r_naechte.set("–")
|
||
|
||
# ---------------------------------------------------------- Positionen
|
||
# Spaltendefinition – Reihenfolge wie in der Excel:
|
||
# (Titel, Ausrichtung, Mindestbreite px). col 1 (Leistung) bekommt das Gewicht.
|
||
COLS = [
|
||
("Pos.", "center", 38),
|
||
("Leistung", "w", 330),
|
||
("USt %", "center", 50),
|
||
("Anzahl\nTage/Menge", "center", 80),
|
||
("Pers.", "center", 56),
|
||
("E.-Preis", "e", 72),
|
||
("Gesamt", "e", 92),
|
||
]
|
||
|
||
def _build_positionen(self):
|
||
wrap = ttk.LabelFrame(self, text="Leistungen – Anzahl (und ggf. Personen) eintragen", padding=(6, 4))
|
||
wrap.pack(side="top", fill="both", expand=True, padx=12, pady=4)
|
||
|
||
canvas = tk.Canvas(wrap, highlightthickness=0, bg=self.col_bg)
|
||
sb = ttk.Scrollbar(wrap, orient="vertical", command=canvas.yview)
|
||
self.pos_frame = ttk.Frame(canvas)
|
||
self._pos_win = canvas.create_window((0, 0), window=self.pos_frame, anchor="nw")
|
||
self.pos_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||
# pos_frame auf Canvas-Breite ziehen (minus Luft rechts zum Scrollbalken)
|
||
canvas.bind("<Configure>", lambda e: canvas.itemconfig(self._pos_win, width=e.width - 18))
|
||
canvas.configure(yscrollcommand=sb.set)
|
||
canvas.pack(side="left", fill="both", expand=True, pady=(4, 0))
|
||
sb.pack(side="right", fill="y")
|
||
canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(int(-e.delta / 120), "units"))
|
||
|
||
# Feste Spaltenbreiten -> Kopf und Daten teilen sich EIN Grid und fluchten
|
||
for i, (_, _, msize) in enumerate(self.COLS):
|
||
self.pos_frame.columnconfigure(i, minsize=msize, weight=(1 if i == 1 else 0))
|
||
|
||
self._render_positionszeilen()
|
||
|
||
def _render_positionszeilen(self):
|
||
for w in self.pos_frame.winfo_children():
|
||
w.destroy()
|
||
self.anzahl_vars.clear()
|
||
self.personen_vars.clear()
|
||
self.gesamt_lbls.clear()
|
||
|
||
# Kopfzeile (gleiches Grid wie die Daten)
|
||
for i, (title, anchor, _) in enumerate(self.COLS):
|
||
ttk.Label(self.pos_frame, text=title, anchor=anchor,
|
||
font=("Segoe UI", 9, "bold")).grid(row=0, column=i, padx=3, pady=(0, 3), sticky="ew")
|
||
ttk.Separator(self.pos_frame, orient="horizontal").grid(
|
||
row=1, column=0, columnspan=len(self.COLS), sticky="ew", pady=(0, 2))
|
||
|
||
for idx, pos in enumerate(self.cfg["katalog"]):
|
||
r = idx + 2
|
||
nr = pos["nr"]
|
||
av = tk.StringVar()
|
||
pv = tk.StringVar()
|
||
self.anzahl_vars[nr] = av
|
||
self.personen_vars[nr] = pv
|
||
av.trace_add("write", lambda *a: self.recalc())
|
||
pv.trace_add("write", lambda *a: self.recalc())
|
||
|
||
ttk.Label(self.pos_frame, text=str(nr), anchor="center").grid(row=r, column=0, padx=3, pady=2, sticky="ew")
|
||
ttk.Label(self.pos_frame, text=pos["leistung"], anchor="w", wraplength=320,
|
||
justify="left").grid(row=r, column=1, padx=3, pady=2, sticky="ew")
|
||
ttk.Label(self.pos_frame, text=str(pos["satz"]), anchor="center").grid(row=r, column=2, padx=3, pady=2, sticky="ew")
|
||
ttk.Entry(self.pos_frame, textvariable=av, width=8, justify="center").grid(row=r, column=3, padx=3, pady=2)
|
||
# Personen-Feld NUR bei "pro Person"-Positionen anzeigen
|
||
if pos["pro_person"]:
|
||
ttk.Entry(self.pos_frame, textvariable=pv, width=6, justify="center").grid(row=r, column=4, padx=3, pady=2)
|
||
ttk.Label(self.pos_frame, text=berechnung.eur_kurz(pos["preis"]), anchor="e").grid(row=r, column=5, padx=3, pady=2, sticky="ew")
|
||
g = ttk.Label(self.pos_frame, text="0,00", anchor="e")
|
||
g.grid(row=r, column=6, padx=3, pady=2, sticky="ew")
|
||
self.gesamt_lbls[nr] = g
|
||
|
||
# ------------------------------------------------------------- Summen
|
||
def _build_summen(self):
|
||
# Statuszeile ganz unten
|
||
status = ttk.Frame(self, padding=(12, 2))
|
||
status.pack(side="bottom", fill="x")
|
||
ttk.Label(status, text="powered by ravokk", foreground=self.col_muted,
|
||
font=("Segoe UI", 8)).pack(side="right")
|
||
|
||
f = ttk.Frame(self, padding=(12, 6, 12, 10))
|
||
f.pack(side="bottom", fill="x")
|
||
ttk.Separator(self, orient="horizontal").pack(side="bottom", fill="x")
|
||
|
||
# Summen werden in recalc() dynamisch befuellt (Netto -> USt je Satz -> Gesamt)
|
||
self.summen_box = ttk.Frame(f)
|
||
self.summen_box.pack(side="right")
|
||
|
||
links = ttk.Frame(f)
|
||
links.pack(side="left")
|
||
ttk.Button(links, text="PDF erstellen", command=self.pdf_erstellen, width=18).pack(side="left", padx=4)
|
||
ttk.Button(links, text="Zurücksetzen", command=self.reset_formular, width=14).pack(side="left", padx=4)
|
||
|
||
# --------------------------------------------------------- Berechnung
|
||
def _eingaben(self):
|
||
e = {}
|
||
for nr, av in self.anzahl_vars.items():
|
||
e[nr] = {"anzahl": parse_num(av.get()), "personen": parse_num(self.personen_vars[nr].get())}
|
||
return e
|
||
|
||
def recalc(self):
|
||
r = berechnung.berechne(self.cfg["katalog"], self._eingaben(), self.cfg)
|
||
aktiv = {z.nr: z.brutto for z in r.zeilen}
|
||
for nr, lbl in self.gesamt_lbls.items():
|
||
lbl.configure(text=berechnung.eur_kurz(aktiv.get(nr, 0.0)))
|
||
|
||
# Summenzeilen dynamisch aufbauen: Rechnungsbetrag (brutto) -> darin enthaltene
|
||
# USt je Satz -> [Beherbergungssteuer obendrauf] -> Gesamtbetrag.
|
||
for w in self.summen_box.winfo_children():
|
||
w.destroy()
|
||
rows = [("Nettobetrag:", berechnung.eur(r.netto_gesamt), False)]
|
||
for b in r.bloecke:
|
||
name = b.bezeichnung or "USt"
|
||
rows.append((f"{name} {b.satz} %:", berechnung.eur(b.ust), False))
|
||
if r.aufschlaege:
|
||
rows.append(("Zwischensumme:", berechnung.eur(r.brutto), False))
|
||
for a in r.aufschlaege:
|
||
satz = berechnung.eur_kurz(a.satz).replace(",00", "")
|
||
rows.append((f"zzgl. {a.bezeichnung or 'Aufschlag'} ({satz} %):", berechnung.eur(a.betrag), False))
|
||
rows.append(("Gesamtbetrag:", berechnung.eur(r.gesamt), True))
|
||
|
||
for i, (lbl, val, fett) in enumerate(rows):
|
||
font = ("Segoe UI", 12, "bold") if fett else ("Segoe UI", 10)
|
||
pady = (5, 0) if fett else (1, 0)
|
||
ttk.Label(self.summen_box, text=lbl, font=font).grid(row=i, column=0, sticky="e", padx=6, pady=pady)
|
||
vkw = {"font": font, "width": 14, "anchor": "e"}
|
||
if fett:
|
||
vkw["foreground"] = self.col_accent
|
||
ttk.Label(self.summen_box, text=val, **vkw).grid(row=i, column=1, sticky="e", pady=pady)
|
||
|
||
self._letzte_rechnung = r
|
||
return r
|
||
|
||
# ----------------------------------------------------------- Aktionen
|
||
def _neue_nummer_vorschlagen(self):
|
||
jahr = self.cfg.get("rechnung_jahr", datetime.date.today().year)
|
||
zaehler = self.cfg.get("rechnung_zaehler", 0)
|
||
heute_jahr = datetime.date.today().year
|
||
if heute_jahr != jahr:
|
||
jahr, zaehler = heute_jahr, 0
|
||
self.r_nummer.set(config.format_rechnungsnummer(jahr, zaehler + 1))
|
||
|
||
def pdf_erstellen(self):
|
||
r = self.recalc()
|
||
if not r.zeilen:
|
||
messagebox.showwarning("Keine Positionen",
|
||
"Es wurde keine Leistung mit Anzahl > 0 eingetragen.")
|
||
return
|
||
nummer = self.r_nummer.get().strip()
|
||
if not nummer:
|
||
messagebox.showwarning("Rechnungsnummer fehlt", "Bitte eine Rechnungsnummer eingeben.")
|
||
return
|
||
if not self.k_name.get().strip():
|
||
if not messagebox.askyesno("Kein Kundenname",
|
||
"Es ist kein Kundenname eingetragen. Trotzdem PDF erstellen?"):
|
||
return
|
||
|
||
kunde_zeilen = [self.k_anrede.get(), self.k_name.get(), self.k_zusatz.get(),
|
||
self.k_strasse.get(), self.k_plzort.get()]
|
||
kopf = {
|
||
"rechnungsnummer": nummer,
|
||
"datum": self.r_datum.get().strip() or datetime.date.today().strftime("%d.%m.%Y"),
|
||
"anreise": self.r_anreise.get().strip(),
|
||
"abreise": self.r_abreise.get().strip(),
|
||
"naechte": self.r_naechte.get() if self.r_naechte.get() != "–" else "",
|
||
"kunde_zeilen": kunde_zeilen,
|
||
}
|
||
out_dir = config.ensure_output_dir(self.cfg)
|
||
fname = f"Rechnung_{safe_filename(nummer)}"
|
||
if self.k_name.get().strip():
|
||
fname += "_" + safe_filename(self.k_name.get().strip())
|
||
pfad = os.path.join(out_dir, fname + ".pdf")
|
||
|
||
try:
|
||
# Ein PDF: Seite 1 = Kunden-Rechnung, Seite 2 = Beherbergungssteuer fuers Amt
|
||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||
messagebox.showerror("Fehler beim Erstellen", f"PDF konnte nicht erstellt werden:\n{e}")
|
||
print("[pdf] FEHLER:", e)
|
||
return
|
||
|
||
# Rechnungsnummer-Zaehler hochsetzen, falls Format JJJJ-NNN
|
||
self._zaehler_aus_nummer_speichern(nummer)
|
||
|
||
print(f"[pdf] erstellt -> {pfad}")
|
||
hinweis = "\n\n(Seite 1 = Kunde, Seite 2 = Beherbergungssteuer fürs Amt)" if r.aufschlaege else ""
|
||
if messagebox.askyesno("Fertig", f"Rechnung erstellt:\n{pfad}{hinweis}\n\nJetzt öffnen (zum Drucken)?"):
|
||
self._oeffne_datei(pfad)
|
||
|
||
def _zaehler_aus_nummer_speichern(self, nummer):
|
||
try:
|
||
jahr_s, z_s = nummer.split("-")
|
||
jahr, z = int(jahr_s), int(z_s)
|
||
except (ValueError, AttributeError):
|
||
return
|
||
self.cfg["rechnung_jahr"] = jahr
|
||
self.cfg["rechnung_zaehler"] = z
|
||
config.save_config(self.cfg)
|
||
self._neue_nummer_vorschlagen()
|
||
|
||
def reset_formular(self):
|
||
for v in (self.k_anrede, self.k_name, self.k_zusatz, self.k_strasse, self.k_plzort,
|
||
self.r_anreise, self.r_abreise):
|
||
v.set("")
|
||
self.r_datum.set(datetime.date.today().strftime("%d.%m.%Y"))
|
||
for av in self.anzahl_vars.values():
|
||
av.set("")
|
||
for pv in self.personen_vars.values():
|
||
pv.set("")
|
||
self._neue_nummer_vorschlagen()
|
||
self.recalc()
|
||
|
||
def _ordner_oeffnen(self):
|
||
self._oeffne_datei(config.ensure_output_dir(self.cfg))
|
||
|
||
def _frage_speicherort_falls_noetig(self):
|
||
if (self.cfg.get("output_dir") or "").strip():
|
||
return
|
||
vorschlag = config.default_output_dir()
|
||
messagebox.showinfo(
|
||
"Speicherort der Rechnungen",
|
||
"Bitte wähle einen Ordner, in dem die fertigen Rechnungen (PDF) gespeichert werden.\n\n"
|
||
f"Vorschlag: {vorschlag}\n\n(Lässt sich später unter Einstellungen ändern.)")
|
||
d = filedialog.askdirectory(title="Ordner für die Rechnungen wählen",
|
||
initialdir=os.path.expanduser("~"))
|
||
self.cfg["output_dir"] = d if d else vorschlag
|
||
config.save_config(self.cfg)
|
||
config.ensure_output_dir(self.cfg)
|
||
|
||
def dlg_speicherort(self):
|
||
aktuell = config.ensure_output_dir(self.cfg)
|
||
d = filedialog.askdirectory(title="Ordner für die Rechnungen wählen", initialdir=aktuell)
|
||
if d:
|
||
self.cfg["output_dir"] = d
|
||
config.save_config(self.cfg)
|
||
config.ensure_output_dir(self.cfg)
|
||
messagebox.showinfo("Speicherort", f"Rechnungen werden gespeichert in:\n{d}")
|
||
|
||
def _oeffne_datei(self, pfad):
|
||
try:
|
||
if sys.platform.startswith("win"):
|
||
os.startfile(pfad) # noqa: S606
|
||
elif sys.platform == "darwin":
|
||
os.system(f'open "{pfad}"')
|
||
else:
|
||
os.system(f'xdg-open "{pfad}"')
|
||
except Exception as e: # noqa: BLE001
|
||
messagebox.showinfo("Pfad", f"{pfad}\n\n(Konnte nicht automatisch geöffnet werden: {e})")
|
||
|
||
def _info(self):
|
||
d = tk.Toplevel(self)
|
||
d.title("Info")
|
||
d.transient(self)
|
||
d.resizable(False, False)
|
||
self._style_dialog(d)
|
||
frm = ttk.Frame(d, padding=18)
|
||
frm.pack(fill="both", expand=True)
|
||
ttk.Label(frm, text="Rechnungstool Campinghof Bartl", font=("Segoe UI", 11, "bold"),
|
||
foreground=self.col_accent).pack(anchor="w")
|
||
ttk.Label(frm, justify="left",
|
||
text="Daten eingeben, PDF erstellen, drucken.\n"
|
||
"Steuersätze, Preise und Firmendaten lassen sich\n"
|
||
"im Menü Einstellungen anpassen.").pack(anchor="w", pady=(8, 12))
|
||
ttk.Label(frm, text="powered by ravokk", foreground=self.col_muted).pack(anchor="w")
|
||
ttk.Button(frm, text="OK", command=d.destroy, width=10).pack(anchor="e", pady=(14, 0))
|
||
d.bind("<Escape>", lambda e: d.destroy())
|
||
d.bind("<Return>", lambda e: d.destroy())
|
||
d.grab_set()
|
||
|
||
# ====================================================== Einstellungen
|
||
def dlg_steuern(self):
|
||
d = tk.Toplevel(self)
|
||
d.title("Steuersätze")
|
||
d.transient(self)
|
||
d.grab_set()
|
||
self._style_dialog(d)
|
||
d.geometry("620x470")
|
||
|
||
frm = ttk.Frame(d, padding=12)
|
||
frm.pack(fill="both", expand=True)
|
||
|
||
ttk.Label(frm, wraplength=580, justify="left", font=("Segoe UI", 9, "bold"),
|
||
text="Steuersätze: Satz % + Name. Die Katalog-Preise sind BRUTTO; die USt dieses "
|
||
"Satzes wird herausgerechnet und als 'enthalten' ausgewiesen. Haken bei "
|
||
"'Aufschlag': diese Steuer (z.B. Beherbergungssteuer) wird stattdessen auf den "
|
||
"Rechnungsbetrag aufgeschlagen und direkt darunter ausgewiesen.").pack(anchor="w", pady=(0, 8))
|
||
|
||
liste = ttk.Frame(frm)
|
||
liste.pack(fill="x")
|
||
arbeit = []
|
||
for s in self.cfg.get("steuersaetze", []):
|
||
arbeit.append({"satz": tk.StringVar(value=str(s.get("satz", ""))),
|
||
"bez": tk.StringVar(value=s.get("bezeichnung", "")),
|
||
"extra": tk.BooleanVar(value=bool(s.get("extra_blatt", False)))})
|
||
|
||
def render_saetze():
|
||
for w in liste.winfo_children():
|
||
w.destroy()
|
||
for c, t in enumerate(["Satz %", "Bezeichnung (Name der Steuer)", "Aufschlag", ""]):
|
||
ttk.Label(liste, text=t, font=("Segoe UI", 9, "bold")).grid(
|
||
row=0, column=c, padx=4, pady=(0, 3), sticky="w")
|
||
for i, row in enumerate(arbeit, start=1):
|
||
ttk.Entry(liste, textvariable=row["satz"], width=8, justify="center").grid(row=i, column=0, padx=4, pady=2)
|
||
ttk.Entry(liste, textvariable=row["bez"], width=32).grid(row=i, column=1, padx=4, pady=2, sticky="w")
|
||
ttk.Checkbutton(liste, variable=row["extra"]).grid(row=i, column=2, padx=14, pady=2)
|
||
ttk.Button(liste, text="✕", width=3,
|
||
command=lambda r=row: (arbeit.remove(r), render_saetze())).grid(row=i, column=3, padx=4)
|
||
|
||
def satz_hinzufuegen():
|
||
arbeit.append({"satz": tk.StringVar(value="0"),
|
||
"bez": tk.StringVar(value="Mehrwertsteuer"),
|
||
"extra": tk.BooleanVar(value=False)})
|
||
render_saetze()
|
||
|
||
render_saetze()
|
||
ttk.Button(frm, text="+ Steuersatz hinzufügen", command=satz_hinzufuegen).pack(anchor="w", pady=(8, 0))
|
||
|
||
def speichern():
|
||
neu = []
|
||
for row in arbeit:
|
||
s = row["satz"].get().strip()
|
||
if s == "":
|
||
continue
|
||
neu.append({"satz": int(parse_num(s)),
|
||
"bezeichnung": row["bez"].get().strip() or "USt",
|
||
"extra_blatt": bool(row["extra"].get())})
|
||
if not neu:
|
||
messagebox.showwarning("Leer", "Mindestens ein Steuersatz ist nötig.", parent=d)
|
||
return
|
||
self.cfg["steuersaetze"] = neu
|
||
config.save_config(self.cfg)
|
||
self._render_positionszeilen()
|
||
self.recalc()
|
||
d.destroy()
|
||
|
||
btns = ttk.Frame(frm)
|
||
btns.pack(side="bottom", fill="x", pady=(12, 0))
|
||
ttk.Button(btns, text="Speichern", command=speichern).pack(side="right", padx=4)
|
||
ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right")
|
||
|
||
def dlg_katalog(self):
|
||
d = tk.Toplevel(self)
|
||
d.title("Leistungskatalog / Preise")
|
||
d.transient(self)
|
||
d.grab_set()
|
||
self._style_dialog(d)
|
||
|
||
info_lbl = ttk.Label(d, padding=(10, 8), wraplength=900, justify="left",
|
||
text="Leistungen bearbeiten: Bezeichnung, Steuersatz (Auswahl), Preis in € und ob "
|
||
"pro Person gerechnet wird. Weitere Steuersätze legst du unter "
|
||
"Einstellungen > Steuersätze an. Unten neue Zeilen anlegen oder mit dem X "
|
||
"rechts eine Leistung entfernen.")
|
||
info_lbl.pack(side="top", fill="x")
|
||
|
||
# alle definierten Steuersaetze sind je Position waehlbar
|
||
saetze_cfg = self.cfg.get("steuersaetze", [])
|
||
|
||
def satz_label(satz):
|
||
for s in saetze_cfg:
|
||
if int(s.get("satz", -1)) == int(satz or 0):
|
||
return f"{s['satz']} % – {s.get('bezeichnung', '')}".rstrip(" –")
|
||
return f"{satz} %"
|
||
|
||
def combo_werte(aktuell):
|
||
opts = [f"{s['satz']} % – {s.get('bezeichnung', '')}".rstrip(" –") for s in saetze_cfg]
|
||
al = satz_label(aktuell)
|
||
if al not in opts:
|
||
opts.append(al)
|
||
return opts
|
||
|
||
def satz_aus_label(lbl):
|
||
try:
|
||
return int(parse_num(str(lbl).split("%")[0]))
|
||
except (ValueError, IndexError):
|
||
return 0
|
||
|
||
# Arbeitskopie als Liste von Variablen-Dicts (erlaubt Hinzufuegen/Loeschen)
|
||
arbeit = []
|
||
for pos in self.cfg["katalog"]:
|
||
arbeit.append({
|
||
"leistung": tk.StringVar(value=pos["leistung"]),
|
||
"satz": tk.StringVar(value=str(pos["satz"])),
|
||
"preis": tk.StringVar(value=berechnung.eur_kurz(pos["preis"])),
|
||
"pro_person": tk.BooleanVar(value=bool(pos["pro_person"])),
|
||
})
|
||
|
||
def render():
|
||
for w in inner.winfo_children():
|
||
w.destroy()
|
||
for c, t in enumerate(["Pos.", "Bezeichnung", "Steuersatz", "Preis €", "pro Person", ""]):
|
||
ttk.Label(inner, text=t, font=("Segoe UI", 9, "bold")).grid(
|
||
row=0, column=c, padx=4, pady=(0, 4), sticky="w")
|
||
for i, row in enumerate(arbeit, start=1):
|
||
ttk.Label(inner, text=str(i), width=4).grid(row=i, column=0, padx=4, pady=1)
|
||
ttk.Entry(inner, textvariable=row["leistung"], width=64).grid(row=i, column=1, padx=4, pady=1, sticky="we")
|
||
cb = ttk.Combobox(inner, width=22, state="readonly", values=combo_werte(row["satz"].get()))
|
||
cb.set(satz_label(row["satz"].get()))
|
||
cb.bind("<<ComboboxSelected>>",
|
||
lambda ev, rr=row, cc=cb: rr["satz"].set(str(satz_aus_label(cc.get()))))
|
||
cb.grid(row=i, column=2, padx=4, pady=1, sticky="w")
|
||
ttk.Entry(inner, textvariable=row["preis"], width=10, justify="right").grid(row=i, column=3, padx=4, pady=1)
|
||
ttk.Checkbutton(inner, variable=row["pro_person"]).grid(row=i, column=4, padx=4, pady=1)
|
||
ttk.Button(inner, text="✕", width=3,
|
||
command=lambda r=row: (arbeit.remove(r), render())).grid(row=i, column=5, padx=4, pady=1)
|
||
|
||
def hinzufuegen():
|
||
arbeit.append({
|
||
"leistung": tk.StringVar(value="Neue Leistung"),
|
||
"satz": tk.StringVar(value=str(saetze_cfg[0]["satz"]) if saetze_cfg else "19"),
|
||
"preis": tk.StringVar(value="0,00"),
|
||
"pro_person": tk.BooleanVar(value=False),
|
||
})
|
||
render()
|
||
canvas.update_idletasks()
|
||
canvas.configure(scrollregion=canvas.bbox("all"))
|
||
canvas.yview_moveto(1.0)
|
||
|
||
def speichern():
|
||
neu = []
|
||
for i, row in enumerate(arbeit, start=1):
|
||
name = row["leistung"].get().strip()
|
||
if not name:
|
||
continue # leere Zeilen verwerfen
|
||
neu.append({
|
||
"nr": i,
|
||
"leistung": name,
|
||
"satz": int(parse_num(row["satz"].get())),
|
||
"preis": parse_num(row["preis"].get()),
|
||
"pro_person": bool(row["pro_person"].get()),
|
||
})
|
||
if not neu:
|
||
messagebox.showwarning("Leer", "Es muss mindestens eine Leistung übrig bleiben.", parent=d)
|
||
return
|
||
self.cfg["katalog"] = neu
|
||
config.save_config(self.cfg)
|
||
self._render_positionszeilen()
|
||
self.recalc()
|
||
d.destroy()
|
||
|
||
# Buttonleiste unten zuerst -> bleibt immer sichtbar
|
||
btns = ttk.Frame(d, padding=8)
|
||
btns.pack(side="bottom", fill="x")
|
||
ttk.Button(btns, text="+ Position hinzufügen", command=hinzufuegen).pack(side="left", padx=4)
|
||
ttk.Button(btns, text="Speichern", command=speichern).pack(side="right", padx=4)
|
||
ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right", padx=4)
|
||
|
||
# Scrollbereich
|
||
body = ttk.Frame(d)
|
||
body.pack(side="top", fill="both", expand=True)
|
||
canvas = tk.Canvas(body, highlightthickness=0, bg=self.col_bg)
|
||
sb = ttk.Scrollbar(body, orient="vertical", command=canvas.yview)
|
||
inner = ttk.Frame(canvas, padding=8)
|
||
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||
canvas.create_window((0, 0), window=inner, anchor="nw")
|
||
canvas.configure(yscrollcommand=sb.set)
|
||
canvas.pack(side="left", fill="both", expand=True)
|
||
sb.pack(side="right", fill="y")
|
||
|
||
render()
|
||
|
||
# Fenstergröße an den Inhalt anpassen (Breite = breiteste Zeile)
|
||
d.update_idletasks()
|
||
inhalt_w = inner.winfo_reqwidth()
|
||
canvas.configure(width=inhalt_w)
|
||
win_w = min(inhalt_w + sb.winfo_reqwidth() + 26, d.winfo_screenwidth() - 80)
|
||
win_h = min(inner.winfo_reqheight() + btns.winfo_reqheight() + 90,
|
||
d.winfo_screenheight() - 120)
|
||
d.geometry(f"{win_w}x{int(win_h)}")
|
||
d.minsize(min(win_w, 820), 420)
|
||
info_lbl.configure(wraplength=win_w - 24)
|
||
|
||
def dlg_firma(self):
|
||
d = tk.Toplevel(self)
|
||
d.title("Firmendaten & Texte")
|
||
d.transient(self)
|
||
d.grab_set()
|
||
self._style_dialog(d)
|
||
d.geometry("640x640")
|
||
|
||
frm = ttk.Frame(d, padding=12)
|
||
frm.pack(fill="both", expand=True)
|
||
felder = [
|
||
("name", "Firmenname"), ("untertitel", "Untertitel"),
|
||
("absender", "Absenderzeile"), ("inhaber", "Inhaber"),
|
||
("bank", "Bank"), ("iban", "IBAN"), ("bic", "SWIFT/BIC"),
|
||
("steuernummer", "Steuernummer"), ("homepage", "Homepage"),
|
||
("email", "E-Mail"), ("telefon", "Telefon"),
|
||
]
|
||
vars_ = {}
|
||
for i, (key, lbl) in enumerate(felder):
|
||
ttk.Label(frm, text=lbl).grid(row=i, column=0, sticky="w", pady=2)
|
||
v = tk.StringVar(value=self.cfg["firma"].get(key, ""))
|
||
ttk.Entry(frm, textvariable=v, width=58).grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
vars_[key] = v
|
||
frm.columnconfigure(1, weight=1)
|
||
base = len(felder)
|
||
|
||
# Logo (Bilddatei) mit Durchsuchen / Entfernen
|
||
ttk.Label(frm, text="Logo (oben links)").grid(row=base, column=0, sticky="w", pady=2)
|
||
logo_row = ttk.Frame(frm)
|
||
logo_row.grid(row=base, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
v_logo = tk.StringVar(value=self.cfg["firma"].get("logo", ""))
|
||
ttk.Entry(logo_row, textvariable=v_logo).pack(side="left", fill="x", expand=True)
|
||
|
||
def logo_waehlen():
|
||
p = filedialog.askopenfilename(
|
||
title="Logo auswählen",
|
||
filetypes=[("Bilder", "*.png *.jpg *.jpeg *.gif"), ("Alle Dateien", "*.*")])
|
||
if p:
|
||
v_logo.set(p)
|
||
|
||
ttk.Button(logo_row, text="Durchsuchen…", command=logo_waehlen).pack(side="left", padx=(6, 0))
|
||
ttk.Button(logo_row, text="Entfernen", command=lambda: v_logo.set("")).pack(side="left", padx=(4, 0))
|
||
|
||
ttk.Label(frm, text="Danktext 1").grid(row=base + 1, column=0, sticky="nw", pady=2)
|
||
t1 = tk.Text(frm, width=58, height=3, wrap="word", bg=self.col_field, fg=self.col_fg, insertbackground=self.col_fg)
|
||
t1.insert("1.0", self.cfg["texte"].get("dank1", ""))
|
||
t1.grid(row=base + 1, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
ttk.Label(frm, text="Danktext 2").grid(row=base + 2, column=0, sticky="nw", pady=2)
|
||
t2 = tk.Text(frm, width=58, height=3, wrap="word", bg=self.col_field, fg=self.col_fg, insertbackground=self.col_fg)
|
||
t2.insert("1.0", self.cfg["texte"].get("dank2", ""))
|
||
t2.grid(row=base + 2, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
ttk.Label(frm, text="Hinweiszeile").grid(row=base + 3, column=0, sticky="w", pady=2)
|
||
vh = tk.StringVar(value=self.cfg["texte"].get("hinweis", ""))
|
||
ttk.Entry(frm, textvariable=vh, width=58).grid(row=base + 3, column=1, sticky="ew", padx=(8, 0), pady=2)
|
||
|
||
def speichern():
|
||
for key, v in vars_.items():
|
||
self.cfg["firma"][key] = v.get()
|
||
self.cfg["firma"]["logo"] = v_logo.get().strip()
|
||
self.cfg["texte"]["dank1"] = t1.get("1.0", "end").strip()
|
||
self.cfg["texte"]["dank2"] = t2.get("1.0", "end").strip()
|
||
self.cfg["texte"]["hinweis"] = vh.get().strip()
|
||
config.save_config(self.cfg)
|
||
self.title("Rechnungstool - " + self.cfg["firma"].get("name", ""))
|
||
d.destroy()
|
||
|
||
btns = ttk.Frame(frm)
|
||
btns.grid(row=base + 4, column=0, columnspan=2, pady=(12, 0), sticky="e")
|
||
ttk.Button(btns, text="Speichern", command=speichern).pack(side="right", padx=4)
|
||
ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right")
|
||
|
||
def dlg_nummer(self):
|
||
d = tk.Toplevel(self)
|
||
d.title("Rechnungsnummer")
|
||
d.transient(self)
|
||
d.grab_set()
|
||
self._style_dialog(d)
|
||
frm = ttk.Frame(d, padding=12)
|
||
frm.pack(fill="both", expand=True)
|
||
|
||
v_jahr = tk.StringVar(value=str(self.cfg.get("rechnung_jahr", datetime.date.today().year)))
|
||
v_z = tk.StringVar(value=str(self.cfg.get("rechnung_zaehler", 0)))
|
||
ttk.Label(frm, text="Format: JJJJ-NNN (z. B. 2026-013). Der Zähler ist der zuletzt\n"
|
||
"vergebene Wert – beim nächsten Beleg wird +1 vorgeschlagen.",
|
||
justify="left").grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8))
|
||
ttk.Label(frm, text="Jahr").grid(row=1, column=0, sticky="w", pady=3)
|
||
ttk.Entry(frm, textvariable=v_jahr, width=10).grid(row=1, column=1, sticky="w", pady=3)
|
||
ttk.Label(frm, text="Zuletzt vergebener Zähler").grid(row=2, column=0, sticky="w", pady=3)
|
||
ttk.Entry(frm, textvariable=v_z, width=10).grid(row=2, column=1, sticky="w", pady=3)
|
||
|
||
def speichern():
|
||
self.cfg["rechnung_jahr"] = int(parse_num(v_jahr.get()))
|
||
self.cfg["rechnung_zaehler"] = int(parse_num(v_z.get()))
|
||
config.save_config(self.cfg)
|
||
self._neue_nummer_vorschlagen()
|
||
d.destroy()
|
||
|
||
btns = ttk.Frame(frm)
|
||
btns.grid(row=3, column=0, columnspan=2, pady=(12, 0), sticky="e")
|
||
ttk.Button(btns, text="Speichern", command=speichern).pack(side="right", padx=4)
|
||
ttk.Button(btns, text="Abbrechen", command=d.destroy).pack(side="right")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = RechnungsApp()
|
||
app.mainloop()
|