REGRESSION aus der Modulaufteilung (Code-Review-Agent): - einstellungen.py fehlten "berechnung" und "datetime". Menue > Leistungskatalog und > Rechnungsnummer stuerzten mit NameError ab und liessen ein leeres, modales Fenster stehen. Preise und Nummernkreis waren nicht mehr einstellbar. KEIN Pruefstand hat das gemerkt - keiner oeffnete je einen Dialog. Neu: pruef_dialoge.py oeffnet ALLE Dialoge und schliesst sie wieder. HAENGENDE ZWISCHENSPEICHER (derselbe Agent, gemessen): - "Zuruecksetzen" liess storno_stand/berichtigung_stand stehen. Danach gehoerten die Daten eines FREMDEN Gastes zum Storno der alten Rechnung, und die Berichtigung schrieb ein Blatt mit Nummer A im Kenndatensatz und Nummer B auf dem Blatt. reset_formular fragt jetzt nach und verwirft sauber. - _neue_nummer_vorschlagen fasst die Nummer nicht mehr an, solange ein Vorgang laeuft (traf auch dlg_nummer). - Fenster schliessen mit offenem Storno fragt jetzt nach (WM_DELETE_WINDOW). - _storno_verwerfen bricht ab, wenn das PDF nicht geloescht werden kann, statt den Zaehler trotzdem zurueckzudrehen. - Der Storno prueft die Kundenangaben und fragt, wenn die alte Rechnung sie nicht hergibt. - Zaehler nicht speicherbar -> sichtbare Meldung statt Traceback ins Nichts. - storno.py: v1-Rueckfall nimmt den ersten Satz OHNE extra_blatt. - EINE Quelle fuer die naechste Nummer: _neue_nummer_vorschlagen benutzt jetzt naechste_freie_nummer, der Jahreswechsel steht nur noch dort. Vorher schlug das Formular 2026-001 vor, waehrend der Storno-Weg 2025-088 nahm. PRUEFSTAENDE (Audit-Agent: 16 von 27 eingebauten Fehlern blieben unbemerkt): - Neu pruef_kernregeln.py (27 Pruefungen) fuer die zwei Regeln, um die es geht: Nummer von HAND auf eine vergebene setzen, Zaehler hinter dem Ordner, Jahreswechsel, Zaehler nur vorwaerts, Storno ueber den DIALOGKNOPF statt der internen Methode, zweite Berichtigung am selben Tag. Der Bestand wird ueber SHA256 verglichen - "Datei ist noch da" heisst nicht "unveraendert". - Der Text des Storno-PDFs wird gelesen: Positionszeile negativ, nicht nur der Summenblock. - Tautologien raus: Selbstvergleich beim storno_datum, all() ueber eine leere Liste, "nicht" in einem deutschen Text. - Unangemeldeter Dialog laesst den Lauf scheitern, statt still "Nein" zu sagen. - pruef_nummern.py: .PDF gross geschrieben, Rechnung_*.txt, echte Praefix-Falle. - Gegenprobe: sechs Mutationen eingebaut, die vorher gruen blieben - alle sechs werden jetzt rot. RECHT (Gegenpruefungs-Agent): Zitate praezisiert. § 146 Abs. 4 AO verbietet nur Aenderungen, bei denen der urspruengliche Inhalt nicht mehr feststellbar ist - ein protokollierter Storno-Vermerk waere erlaubt, wir verzichten trotzdem bewusst darauf. Der § 14c-Hinweis bleibt, gilt aber gegenueber Endverbrauchern nicht (EuGH C-378/21, BMF v. 27.02.2024) - er zielt auf Firmengaeste. 142 Pruefungen gruen (100 + 27 + 8 + 7). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
366 lines
17 KiB
Python
366 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Die Dialoge aus dem Menue "Einstellungen" - Steuersaetze, Katalog, Firma, Nummer.
|
||
|
||
Alles, was hier eingestellt wird, landet in der config.json. Nichts davon steht
|
||
hart im Code: Steuersaetze, Preise, Firmendaten, Rechnungsnummernkreis.
|
||
"""
|
||
|
||
import datetime
|
||
import os
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, filedialog
|
||
|
||
import berechnung
|
||
import config
|
||
from helfer import parse_num
|
||
|
||
|
||
class EinstellungenMixin:
|
||
"""Einstellungsdialoge. Erwartet die Felder der RechnungsApp."""
|
||
|
||
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 _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")
|