rechnungstool/ordnerwahl.py
TheMockTv 2e3a191f19 Eigene Meldungsfenster, aktiver Reiter hervorgehoben, Pruefschalter als Funktion
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
2026-09-05 19:17:13 +02:00

210 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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