Gleiche Umstellung wie im Rechnungstool, gleiche gemeinsame Dateien
(gemeinsam.py, ordnerwahl.py, wasserzeichen.py, bauen.py).
%LOCALAPPDATA%/ravokk/
journal.sqlite3, berichte/, Nummernbuch, config.json <- Daten
Rechnungstool/ <- Programm
Steuerrechnungstool/ <- Programm
⚠ Hier gefunden und behoben: das Journal der INSTALLIERTEN Fassung lag im
Temp-Ordner. Der Merkzettel (ablage.py) zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09.2026. Windows raeumt den Temp-Ordner von selbst auf; das Journal
waere ohne Vorwarnung verschwunden. Ein Merkzettel, der dorthin zeigt,
wird jetzt verworfen statt befolgt.
Sein Echtbestand (238 Buchungen) lag davon unberuehrt in der Quelle und
ist gesichert.
Kein Ordner-Dialog mehr beim ersten Start - der gemeinsame Ordner steht
fest. Der Knopf hiess dort "Install-Ordner auswaehlen...", was schon immer
falsch war: es geht um das gemeinsame Nummernbuch, nicht um den
Installationsort.
Uninstaller raeumt jetzt auch die vom Updater geladenen Fassungen weg.
pruef_installation.py prueft beides zusammen: Programmordner leer, Daten
unangetastet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
225 lines
9.8 KiB
Python
225 lines
9.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.
|
||
|
||
⚠ NICHT der Ordner, in dem das Programm liegt. Frueher stand das so im Text
|
||
("am besten der Ordner, in dem die Programme liegen") und der Knopf hiess
|
||
sogar "Install-Ordner auswaehlen". Seit es einen Installer gibt, geht das gar
|
||
nicht mehr: die beiden Programme liegen in ZWEI getrennten Ordnern unter
|
||
%LOCALAPPDATA%/Programs, und ein Deinstallieren wuerde das Nummernbuch
|
||
mitnehmen. Vorgeschlagen wird deshalb der Ordner, in dem auch die Rechnungen
|
||
liegen - den kennen beide Programme ohnehin, und er wird mitgesichert.
|
||
|
||
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 auch die Rechnungen 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")
|
||
# "Install-Ordner" hiess der Knopf frueher - das war schon immer
|
||
# falsch und ist es seit dem Installer erst recht: hier geht es NICHT
|
||
# um den Ordner, in dem das Programm liegt, sondern um den, in dem die
|
||
# beiden Programme ihr gemeinsames Nummernbuch fuehren. Sein Befund am
|
||
# 07.09.2026: "gibt es noch das popup install ordner auswaehlen, das
|
||
# stimmt ja nicht".
|
||
ttk.Button(wahl, text="Ordner auswählen…", command=self.waehlen,
|
||
width=22).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}")
|
||
theme.mittig(self, master)
|
||
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
|