rechnungstool/ordnerwahl.py
TheMockTv 9794632ae4 Alles unter ravokk - und ein Deinstallieren laesst nichts liegen
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
2026-09-07 10:43:09 +02:00

225 lines
9.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.
⚠ 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