Sein Fund: "der ohne gemeinsamen ordner popup ist noch hell" und "er hebt den aktiven tab nicht hervor, der inaktive ist der hervorgehobene". - Meldungen und Ja/Nein-Fragen kommen jetzt aus eigenen Fenstern im Theme (MeldungMixin). messagebox sind Fenster von WINDOWS - sie bleiben hell, egal was das Programm einstellt. Dieselbe Erfahrung steckt schon in der eigenen Menueleiste. 49 Aufrufe umgestellt; das Ordner-Fenster meldet ueber das Hauptfenster mit. ⚠️ Im Pruflauf laeuft beides weiter ueber messagebox - dort haengen die Pruefstaende ihre Antworten ein. Sonst warten sie auf einen Klick. - Reiter: der OFFENE ist jetzt der hellere (Kachelfarbe), die ruhenden liegen auf dem dunklen Grund. Vorher war es andersherum. - config.TESTLAUF ist eine FUNKTION geworden: Pruefstaende importieren config oft, bevor sie CAMPINGHOF_TEST setzen - der einmal berechnete Wert stand dann den ganzen Lauf auf False, und das Programm oeffnete echte Dialoge, auf die niemand klickt. Genau daran hing pruef_api. - Der Erststart-Dialog hat einen eigenen Schalter (_erststart_zeigen), statt am ganzen Pruefschalter zu haengen: ein Pruefstand kann IHN zeigen, ohne dass alle anderen Meldungen wieder echte Fenster aufmachen. - Pruefstand-Fenster stehen nicht mehr in der Taskleiste (-toolwindow): sie liegen neben dem Bildschirm, und ein Eintrag, den man anklickt, ohne dass etwas erscheint, ist schlimmer als gar keiner. - pruef_nummernsperre drueckt jetzt den Radioknopf "Meine Zaehlung weiterfuehren", statt nur ins Zahlenfeld zu schreiben - das Feld ist ohne diesen Klick gesperrt, wie beim Benutzer auch. Alle 14 Pruefstaende gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
210 lines
8.8 KiB
Python
210 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Den gemeinsamen Ordner abfragen - in beiden Programmen gleich.
|
||
|
||
Rechnungstool und Steuerjournal fuehren ein gemeinsames Nummernbuch
|
||
(siehe gemeinsam.py). Damit sie sich finden, muss BEIDEN derselbe Ordner
|
||
genannt werden - deshalb fragt jedes Programm beim ersten Start danach und
|
||
sagt ausdruecklich dazu, dass es derselbe sein muss.
|
||
|
||
Der Weg ist bewusst umstaendlich: auswaehlen, dann OK, dann noch einmal
|
||
bestaetigen. Ein falscher Ordner faellt sonst erst auf, wenn eine
|
||
Rechnungsnummer zum zweiten Mal vergeben wurde.
|
||
|
||
Wer jetzt keinen Ordner hat, kommt mit "Spaeter einrichten" weiter - das
|
||
Programm laeuft dann ohne das gemeinsame Buch und sagt das in der Fussleiste.
|
||
|
||
⚠ Das Fenster bekommt seine Groesse ausdruecklich gesetzt. Ohne das war es zu
|
||
schmal, der Knopf zum Auswaehlen lag ausserhalb - und der Dialog war nicht zu
|
||
bedienen (gefunden am 03.09.2026, bevor es jemand benutzen musste).
|
||
|
||
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
|
||
hinueber.
|
||
"""
|
||
|
||
import os
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
|
||
import theme
|
||
|
||
import gemeinsam
|
||
|
||
BREITE = 780
|
||
|
||
|
||
class OrdnerWahl(tk.Toplevel):
|
||
"""Modaler Dialog. Nach dem Schliessen steht das Ergebnis in `.ergebnis`
|
||
(gewaehlter Ordner) oder es ist None (spaeter/abgebrochen)."""
|
||
|
||
def __init__(self, master, programm: str, vorgabe: str = ""):
|
||
super().__init__(master)
|
||
self.ergebnis = None
|
||
self.title("Gemeinsamer Ordner für das Nummernbuch")
|
||
self.transient(master)
|
||
# Dieses Fenster stand als einziges noch in Weiss da: es kam nie durch
|
||
# _style_dialog. Farben holt es sich jetzt vom Hauptfenster, damit es
|
||
# nicht wieder auseinanderlaeuft, wenn dort etwas geaendert wird.
|
||
self._bg = getattr(master, "col_bg", "#0d0e10")
|
||
self._fg = getattr(master, "col_fg", "#e8e8ea")
|
||
self._warn = getattr(master, "col_gefahr", "#e85a5a")
|
||
self._gut = getattr(master, "col_accent", "#06C6A4")
|
||
self._muted = getattr(master, "col_muted", "#8a8e96")
|
||
if hasattr(master, "_style_dialog"):
|
||
master._style_dialog(self)
|
||
else:
|
||
self.configure(bg=self._bg)
|
||
self.pfad = tk.StringVar(value=vorgabe or "")
|
||
|
||
rahmen = ttk.Frame(self, padding=18)
|
||
rahmen.pack(fill="both", expand=True)
|
||
rahmen.columnconfigure(0, weight=1)
|
||
breite = BREITE - 60
|
||
|
||
ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen",
|
||
font=(theme.SCHRIFT_NAME, 13, "bold")).grid(row=0, column=0, sticky="w")
|
||
ttk.Label(rahmen, wraplength=breite, justify="left", text=(
|
||
"Rechnungstool und Steuerjournal führen zusammen ein Nummernbuch. Darin steht "
|
||
"jede vergebene Rechnungsnummer – auch die aus der alten Excel-Tabelle, zu der "
|
||
"es gar keine PDF gibt.\n\n"
|
||
"Ohne dieses Buch kennt jedes Programm nur seine eigenen Nummern und vergibt "
|
||
"eine Nummer ein zweites Mal. Jede Nummer darf es aber nur einmal geben "
|
||
"(§ 14 Abs. 4 Nr. 4 UStG).")).grid(row=1, column=0, sticky="w", pady=(8, 12))
|
||
|
||
tk.Label(rahmen, wraplength=breite, justify="left", fg=self._warn, bg=self._bg,
|
||
font=(theme.SCHRIFT_NAME, 10, "bold"),
|
||
text="WICHTIG: In BEIDEN Programmen muss derselbe Ordner stehen – am besten "
|
||
"der Ordner, in dem die Programme liegen.").grid(
|
||
row=2, column=0, sticky="w", pady=(0, 14))
|
||
|
||
wahl = ttk.Frame(rahmen)
|
||
wahl.grid(row=3, column=0, sticky="ew")
|
||
wahl.columnconfigure(0, weight=1)
|
||
self.feld = ttk.Entry(wahl, textvariable=self.pfad, state="readonly")
|
||
self.feld.grid(row=0, column=0, sticky="ew")
|
||
ttk.Button(wahl, text="Install-Ordner auswählen…", command=self.waehlen,
|
||
width=26).grid(row=0, column=1, sticky="w", padx=(10, 0))
|
||
|
||
self.stand = tk.Label(rahmen, text="", wraplength=breite, justify="left",
|
||
bg=self._bg,
|
||
font=(theme.SCHRIFT_NAME, 9))
|
||
self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0))
|
||
|
||
knoepfe = ttk.Frame(rahmen)
|
||
knoepfe.grid(row=5, column=0, sticky="ew", pady=(18, 0))
|
||
self.ok = ttk.Button(knoepfe, text="OK", command=self.uebernehmen, width=14)
|
||
self.ok.pack(side="right")
|
||
ttk.Button(knoepfe, text="Später einrichten", command=self.spaeter,
|
||
width=18).pack(side="left")
|
||
|
||
self._stand_zeigen()
|
||
self.protocol("WM_DELETE_WINDOW", self.spaeter)
|
||
self.bind("<Escape>", lambda _e: self.spaeter())
|
||
|
||
# Groesse ausdruecklich setzen: sonst schneidet Tk das Fenster an der
|
||
# zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar.
|
||
self.update_idletasks()
|
||
hoehe = max(self.winfo_reqheight(), 300)
|
||
x = master.winfo_rootx() + max(0, (master.winfo_width() - BREITE) // 2)
|
||
y = master.winfo_rooty() + 90
|
||
self.geometry(f"{BREITE}x{hoehe}+{max(0, x)}+{max(0, y)}")
|
||
self.minsize(BREITE, hoehe)
|
||
self.grab_set()
|
||
self.lift()
|
||
self.focus_force()
|
||
|
||
# ------------------------------------------------------------------ Weg
|
||
def waehlen(self):
|
||
# Der Griff (grab_set) muss waehrend des Windows-Ordnerdialogs los sein:
|
||
# sonst kann der native Dialog hinter dem gesperrten Fenster landen und
|
||
# nimmt keine Eingabe an - man klickt und es passiert nichts.
|
||
try:
|
||
self.grab_release()
|
||
except tk.TclError:
|
||
pass
|
||
try:
|
||
ordner = filedialog.askdirectory(
|
||
title="Gemeinsamen Ordner wählen (in beiden Programmen derselbe!)",
|
||
initialdir=self.pfad.get() or os.path.expanduser("~"), parent=self)
|
||
finally:
|
||
try:
|
||
self.grab_set()
|
||
except tk.TclError:
|
||
pass
|
||
if ordner:
|
||
self.pfad.set(os.path.abspath(ordner))
|
||
self._stand_zeigen()
|
||
|
||
def _stand_zeigen(self):
|
||
"""Sagt, was in dem gewaehlten Ordner schon liegt.
|
||
|
||
Ein bereits vorhandenes Nummernbuch ist das beste Zeichen, dass es der
|
||
richtige Ordner ist - dann hat das andere Programm ihn schon.
|
||
"""
|
||
ordner = self.pfad.get().strip()
|
||
if not ordner:
|
||
self.stand.configure(text="Noch kein Ordner gewählt.", fg=self._warn)
|
||
self.ok.state(["disabled"])
|
||
return
|
||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||
if not taugt:
|
||
self.stand.configure(text=grund, fg=self._warn)
|
||
self.ok.state(["disabled"])
|
||
return
|
||
self.ok.state(["!disabled"])
|
||
if os.path.exists(gemeinsam.pfad_zur_datei(ordner)):
|
||
try:
|
||
anzahl = len(gemeinsam.Nummernbuch(ordner).nummern())
|
||
except Exception: # noqa: BLE001
|
||
anzahl = "?"
|
||
self.stand.configure(
|
||
text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – "
|
||
"das ist der Ordner des anderen Programms. Genau richtig.",
|
||
fg=self._gut)
|
||
else:
|
||
self.stand.configure(
|
||
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
|
||
"anderen Programm denselben Ordner.", fg=self._muted)
|
||
|
||
def _melden(self, titel, text):
|
||
"""Meldung im Theme des Hauptfensters - sonst wie bisher."""
|
||
eltern = self.master
|
||
if hasattr(eltern, "melden_warnung"):
|
||
return eltern.melden_warnung(titel, text)
|
||
return messagebox.showwarning(titel, text, parent=self)
|
||
|
||
def _frage(self, titel, text):
|
||
eltern = self.master
|
||
if hasattr(eltern, "frage"):
|
||
return eltern.frage(titel, text)
|
||
return messagebox.askyesno(titel, text, parent=self)
|
||
|
||
|
||
def uebernehmen(self):
|
||
ordner = self.pfad.get().strip()
|
||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||
if not taugt:
|
||
self._melden("Ordner geht nicht", grund or "Bitte einen Ordner wählen.")
|
||
return
|
||
if not self._frage(
|
||
"Sind Sie sicher?",
|
||
f"Gemeinsamer Ordner:\n\n{ordner}\n\n"
|
||
"Steht im anderen Programm derselbe Ordner? Nur dann kennen beide "
|
||
"dieselben Rechnungsnummern.\n\n"
|
||
"Ja = Ordner übernehmen\n"
|
||
"Nein = zurück zur Auswahl"):
|
||
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
|
||
self.feld.focus_set()
|
||
return
|
||
self.ergebnis = ordner
|
||
self.destroy()
|
||
|
||
def spaeter(self):
|
||
self.ergebnis = None
|
||
self.destroy()
|
||
|
||
|
||
def ordner_abfragen(fenster, programm, vorgabe=""):
|
||
"""Dialog zeigen und den gewaehlten Ordner zurueckgeben (oder None)."""
|
||
d = OrdnerWahl(fenster, programm, vorgabe)
|
||
fenster.wait_window(d)
|
||
return d.ergebnis
|