Seine Ansagen
"gibt es noch das popup install ordner auswaehlen, das stimmt ja nicht"
"wie soll das rechnungstool und steuertool wissen wo es ist, das muss in
ravokk liegen und dann zusammen, sonst kommt das eine nicht an das andere
ran - die sind doch nur ueber die gemeinsame sqlite verbunden und mit api"
"es gibt ravokk als main verzeichnis, da liegt config und sqlite drin, und
dann gibt es den ordner rechnungstool und steuertool"
"ravokk ist meine firma und die programme gehoeren zu der firma"
"du solltest die auch mal deinstallieren und neu installieren ... dass der
ordner dann leer ist und sauber - will keine tools die unsauber sind"
Die Struktur
%LOCALAPPDATA%/ravokk/
config.json, journal.sqlite3, Nummernbuch, berichte/ <- Daten
Rechnungstool/ <- Programm
Steuerrechnungstool/ <- Programm
Die Daten liegen NEBEN dem Programm, nicht darin. Nur dadurch koennen
beide Forderungen gleichzeitig gelten: der Programmordner wird beim
Deinstallieren restlos geleert, und die Belege bleiben zehn Jahre stehen
(§ 147 AO).
Kein Ordner-Dialog mehr beim ersten Start
gemeinsam.standard_ordner() nennt den festen Ort, beide Programme legen
ihn selbst an. Gefragt wird nur noch, wer ihn ausdruecklich verschieben
will (Menue -> Gemeinsamer Ordner) - etwa auf ein Netzlaufwerk.
Der Knopf hiess "Install-Ordner auswaehlen..." und der Text riet zum
"Ordner, in dem die Programme liegen". Beides war falsch und seit dem
Installer erst recht: die beiden liegen in getrennten Unterordnern, und
ein Deinstallieren haette das Nummernbuch mitgenommen.
⚠ Dabei gefunden: das Journal lag im TEMP-Ordner
Der Merkzettel der installierten Fassung zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09. Windows raeumt den Temp-Ordner automatisch auf; das Journal waere
ohne Vorwarnung verschwunden. ablage.py verwirft einen solchen Merkzettel
jetzt, statt ihm zu folgen.
Uninstaller
Er raeumt jetzt auch, was der UPDATER angelegt hat - jede geladene Fassung
und installiert.json. Die kannte der Installer nicht; mit jeder Fassung
waere eine EXE mehr liegen geblieben. Gemessen vorher: zwei EXEs und der
halbe Datenordner.
pruef_installation.py
Installiert wirklich, legt eine Datei an wie der Updater es taete,
deinstalliert und sieht nach: Programmordner leer, Daten unangetastet.
Vorhandene Daten werden vorher gesichert und danach zurueckgelegt - ein
Pruefstand fasst niemandem seine Buchhaltung an.
pruef_gemeinsam_automatisch.py
Prueft, dass beide Programme denselben Ordner nennen und dass findet, was
das andere eintraegt - ohne dass jemand einen Pfad eingegeben hat.
Symbol
Das Windows-Symbol ist jetzt eine helle Kachel mit runden Ecken und dem
Zeichen darauf, in allen Groessen von 16 bis 256 - "so wie das rustdesk,
nur halt alle ecken in der rundung". Ein freigestelltes Logo ging auf
einem dunklen Desktop unter. Die zweite, aufgehellte ICO ist damit
ueberfluessig; das aufgehellte Logo IM Programm (Kopfzeile, Wasserzeichen)
bleibt unveraendert.
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
|