Alles unter ravokk - und ein Deinstallieren laesst nichts liegen

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
This commit is contained in:
TheMockTv 2026-09-07 10:43:24 +02:00
parent 319c680a62
commit 1562388e78
12 changed files with 660 additions and 153 deletions

View file

@ -1,98 +1,125 @@
; Installer fuer das Steuerrechnungstool (Inno Setup 6)
;
; ENTSCHEIDUNGEN, DIE HIER STECKEN - und warum
; --------------------------------------------
; 1. INSTALLATION PRO BENUTZER nach %LOCALAPPDATA%\Programs, NICHT nach
; "Program Files". Grund: der Updater muss die EXE ablegen duerfen. In
; Program Files darf ein normaler Benutzer nicht schreiben - jedes Update
; wuerde an fehlenden Rechten scheitern oder eine Administrator-Abfrage
; erzwingen. Chrome, VS Code, Discord, Teams und Fusion 360 machen es
; genauso, aus demselben Grund.
;
; 2. DIE VERKNUEPFUNG ZEIGT AUF DEN UPDATER, nicht auf das Programm. So schaut
; jeder Start zuerst nach einer neuen Fassung. Der Updater startet danach
; die eigentliche EXE - die ist dabei noch gar nicht in Benutzung und laesst
; sich deshalb problemlos ersetzen.
;
; 3. KEINE DATEN ANFASSEN. Der Installer legt keine Rechnungsordner an, kein
; Journal, kein Nummernbuch - und beim Deinstallieren wird davon nichts
; entfernt. Das Programm schreibt aufbewahrungspflichtige Belege
; (§ 147 AO, zehn Jahre). Ein Deinstallieren darf sie nie mitnehmen.
;
; 4. Der Eintrag unter "Apps & Features" entsteht von selbst - Inno Setup legt
; den Registry-Schluessel unter HKCU an. Da muss niemand von Hand hinein.
#define Name "Steuerrechnungstool"
#define Herausgeber "ravokk"
#define ExeUpdater "Updater.exe"
#define Webseite "https://git.pcore.de/TheMockTv/Steuerrechnungstool"
[Setup]
AppId={{B4D71E08-9C36-42AF-8E5B-13A7F0C29D64}
AppName={#Name}
AppVersion={#Version}
AppVerName={#Name} {#Version}
AppPublisher={#Herausgeber}
AppPublisherURL={#Webseite}
AppSupportURL={#Webseite}/issues
AppUpdatesURL={#Webseite}/releases
; --- pro Benutzer, ohne Adminrechte ---
PrivilegesRequired=lowest
; Der Zielordner ist NICHT waehlbar. Wer hier "Programme" auswaehlen
; koennte, braeche sich den Updater: dort darf ein normaler Benutzer
; nicht schreiben. Chrome, Teams und Discord blenden diese Seite aus
; demselben Grund aus. In "Programme" gehoert hoechstens eine
; Verknuepfung - und die liegt ohnehin im Startmenue.
DisableDirPage=yes
DefaultDirName={localappdata}\Programs\{#Name}
DefaultGroupName={#Name}
DisableProgramGroupPage=yes
UsePreviousAppDir=yes
OutputDir=setup
OutputBaseFilename=Steuerrechnungstool-Setup-{#Version}
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
SetupIconFile=assets\programm.ico
UninstallDisplayIcon={app}\{#ExeProgramm}
ArchitecturesInstallIn64BitMode=x64compatible
; Erprobungsfassung - das steht auch im Programm selbst.
AppComments=Erprobungsfassung. Benutzung auf eigene Gefahr.
[Languages]
Name: "deutsch"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
; Kein "unchecked": der Haken ist vorgesetzt. Er hatte nach dem
; Installieren kein Symbol auf dem Desktop und musste das Programm
; suchen (07.09.2026). Wer es nicht will, nimmt den Haken heraus.
Name: "desktopicon"; Description: "Verknüpfung auf dem Desktop anlegen"; \
GroupDescription: "Zusätzliche Verknüpfungen:"
[Files]
; Der Updater - das ist, was der Benutzer startet.
Source: "dist\{#ExeUpdater}"; DestDir: "{app}"; Flags: ignoreversion
; Das Programm selbst, mit der Nummer im Namen. Der Updater erkennt daran,
; was installiert ist, und legt neue Fassungen daneben statt darueber.
Source: "dist\{#ExeProgramm}"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; Beide Verknuepfungen zeigen auf den UPDATER, nicht auf das Programm.
; IconFilename ausdruecklich: ohne das erbt die Verknuepfung das Symbol vom
; UPDATER, und Windows zeigte an ihrer Stelle das nackte Standardsymbol.
Name: "{group}\{#Name}"; Filename: "{app}\{#ExeUpdater}"; IconFilename: "{app}\{#ExeProgramm}"
Name: "{group}\{#Name} deinstallieren"; Filename: "{uninstallexe}"
Name: "{userdesktop}\{#Name}"; Filename: "{app}\{#ExeUpdater}"; IconFilename: "{app}\{#ExeProgramm}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#ExeUpdater}"; Description: "{#Name} jetzt starten"; \
Flags: nowait postinstall skipifsilent
[UninstallDelete]
; Nur die Buchfuehrung des Updaters.
; NICHTS von den Daten des Betriebs - keine Rechnungen, kein Journal, kein
; Nummernbuch. Die liegen ohnehin woanders und muessen zehn Jahre bleiben.
Type: files; Name: "{app}\installiert.json"
; Installer fuer das Steuerrechnungstool (Inno Setup 6)
;
; ENTSCHEIDUNGEN, DIE HIER STECKEN - und warum
; --------------------------------------------
; 1. INSTALLATION PRO BENUTZER nach %LOCALAPPDATA%\Programs, NICHT nach
; "Program Files". Grund: der Updater muss die EXE ablegen duerfen. In
; Program Files darf ein normaler Benutzer nicht schreiben - jedes Update
; wuerde an fehlenden Rechten scheitern oder eine Administrator-Abfrage
; erzwingen. Chrome, VS Code, Discord, Teams und Fusion 360 machen es
; genauso, aus demselben Grund.
;
; 2. DIE VERKNUEPFUNG ZEIGT AUF DEN UPDATER, nicht auf das Programm. So schaut
; jeder Start zuerst nach einer neuen Fassung. Der Updater startet danach
; die eigentliche EXE - die ist dabei noch gar nicht in Benutzung und laesst
; sich deshalb problemlos ersetzen.
;
; 3. KEINE DATEN ANFASSEN. Der Installer legt keine Rechnungsordner an, kein
; Journal, kein Nummernbuch - und beim Deinstallieren wird davon nichts
; entfernt. Das Programm schreibt aufbewahrungspflichtige Belege
; (§ 147 AO, zehn Jahre). Ein Deinstallieren darf sie nie mitnehmen.
;
; 4. Der Eintrag unter "Apps & Features" entsteht von selbst - Inno Setup legt
; den Registry-Schluessel unter HKCU an. Da muss niemand von Hand hinein.
#define Name "Steuerrechnungstool"
#define Herausgeber "ravokk"
#define ExeUpdater "Updater.exe"
#define Webseite "https://git.pcore.de/TheMockTv/Steuerrechnungstool"
[Setup]
AppId={{B4D71E08-9C36-42AF-8E5B-13A7F0C29D64}
AppName={#Name}
AppVersion={#Version}
AppVerName={#Name} {#Version}
AppPublisher={#Herausgeber}
AppPublisherURL={#Webseite}
AppSupportURL={#Webseite}/issues
AppUpdatesURL={#Webseite}/releases
; --- pro Benutzer, ohne Adminrechte ---
PrivilegesRequired=lowest
; Der Zielordner ist NICHT waehlbar. Wer hier "Programme" auswaehlen
; koennte, braeche sich den Updater: dort darf ein normaler Benutzer
; nicht schreiben. Chrome, Teams und Discord blenden diese Seite aus
; demselben Grund aus. In "Programme" gehoert hoechstens eine
; Verknuepfung - und die liegt ohnehin im Startmenue.
DisableDirPage=yes
; EIN Verzeichnis fuer beides - seine Ansage am 07.09.2026:
; "es gibt ravokk als main verzeichnis, da liegt config und sqlite
; drin, und dann gibt es den ordner rechnungstool und steuertool"
;
; %LOCALAPPDATA%\ravokk\
; campinghof-nummernbuch.sqlite3 <- gemeinsam, beide schauen hierher
; Rechnungstool\ <- Programm + Updater
; Steuerrechnungstool\ <- Programm + Updater
;
; Jedes Programm in seinem eigenen Unterordner: die beiden Updater heissen
; gleich und wuerden sich sonst gegenseitig ueberschreiben.
;
; Das Nummernbuch liegt eine Ebene DARUEBER und gehoert keinem der beiden.
; Ein Deinstallieren nimmt es deshalb nicht mit - und wer nur eines der
; Programme installiert, legt es trotzdem am richtigen Ort an
; (gemeinsam.standard_ordner).
DefaultDirName={localappdata}\ravokk\{#Name}
DefaultGroupName={#Name}
DisableProgramGroupPage=yes
UsePreviousAppDir=yes
OutputDir=setup
OutputBaseFilename=Steuerrechnungstool-Setup-{#Version}
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
SetupIconFile=assets\programm.ico
UninstallDisplayIcon={app}\{#ExeProgramm}
ArchitecturesInstallIn64BitMode=x64compatible
; Erprobungsfassung - das steht auch im Programm selbst.
AppComments=Erprobungsfassung. Benutzung auf eigene Gefahr.
[Languages]
Name: "deutsch"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
; Kein "unchecked": der Haken ist vorgesetzt. Er hatte nach dem
; Installieren kein Symbol auf dem Desktop und musste das Programm
; suchen (07.09.2026). Wer es nicht will, nimmt den Haken heraus.
Name: "desktopicon"; Description: "Verknüpfung auf dem Desktop anlegen"; \
GroupDescription: "Zusätzliche Verknüpfungen:"
[Files]
; Der Updater - das ist, was der Benutzer startet.
Source: "dist\{#ExeUpdater}"; DestDir: "{app}"; Flags: ignoreversion
; Das Programm selbst, mit der Nummer im Namen. Der Updater erkennt daran,
; was installiert ist, und legt neue Fassungen daneben statt darueber.
Source: "dist\{#ExeProgramm}"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; Beide Verknuepfungen zeigen auf den UPDATER, nicht auf das Programm.
; IconFilename ausdruecklich: ohne das erbt die Verknuepfung das Symbol vom
; UPDATER, und Windows zeigte an ihrer Stelle das nackte Standardsymbol.
Name: "{group}\{#Name}"; Filename: "{app}\{#ExeUpdater}"; IconFilename: "{app}\{#ExeProgramm}"
Name: "{group}\{#Name} deinstallieren"; Filename: "{uninstallexe}"
Name: "{userdesktop}\{#Name}"; Filename: "{app}\{#ExeUpdater}"; IconFilename: "{app}\{#ExeProgramm}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#ExeUpdater}"; Description: "{#Name} jetzt starten"; \
Flags: nowait postinstall skipifsilent
[UninstallDelete]
; Was der INSTALLER gelegt hat, raeumt Inno Setup von selbst weg. Hier
; steht, was der UPDATER angelegt hat - davon weiss der Installer nichts:
; <Name>-<Nummer>.exe jede geladene Fassung
; installiert.json die Buchfuehrung des Updaters
; Ohne diese Zeilen bleibt der Ordner voller alter EXEs stehen.
;
; NICHT geloescht werden Rechnungen, Journal und Nummernbuch. Die liegen
; ohnehin woanders (%LOCALAPPDATA%/ravokk) und muessen zehn Jahre bleiben
; (§ 147 AO). Deshalb steht hier auch kein 'Type: filesandordirs' auf
; {app} - das waere die bequeme, aber falsche Zeile.
Type: files; Name: "{app}\installiert.json"
Type: files; Name: "{app}\{#Name}-*.exe"
Type: files; Name: "{app}\Updater.exe"
; Zum Schluss den Ordner selbst - aber NUR, wenn er leer ist.
Type: dirifempty; Name: "{app}"

View file

@ -5,7 +5,7 @@ a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[('assets/programm.ico', 'assets'), ('assets/programm_dunkel.ico', 'assets'), ('assets/programm_logo.png', 'assets'),],
datas=[('assets/programm.ico', 'assets'), ('assets/programm_logo.png', 'assets'),],
hiddenimports=[],
hookspath=[],
hooksconfig={},

View file

@ -44,6 +44,43 @@ def _beschreibbar(ordner):
return False
def _ist_temp(pfad):
"""Liegt dieser Pfad im Temp-Ordner von Windows?
⚠ Am 07.09.2026 gefunden: der Merkzettel zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09. Die INSTALLIERTE Fassung fuehrte ihr Journal seitdem im
Temp-Ordner. Windows raeumt den automatisch auf (Datentraegerbereinigung,
Speicheroptimierung); das Journal waere ohne Vorwarnung verschwunden.
Ein Merkzettel, der dorthin zeigt, wird deshalb verworfen - lieber ein
frischer, richtiger Ordner als ein Journal auf Abruf.
"""
import tempfile
try:
temp = os.path.realpath(tempfile.gettempdir()).lower()
ziel = os.path.realpath(pfad).lower()
return ziel == temp or ziel.startswith(temp + os.sep)
except (OSError, ValueError):
return False
def firmen_ordner():
"""Das gemeinsame Dach: %LOCALAPPDATA%/ravokk.
Seine Ansage am 07.09.2026: *"es gibt ravokk als main verzeichnis, da
liegt config und sqlite drin, und dann gibt es den ordner rechnungstool
und steuertool"* - und dazu, warum es so heisst: *"ravokk ist meine firma
und die programme gehoeren zu der firma"*.
Darunter liegt je Programm ein eigener Ordner, und das gemeinsame
Nummernbuch eine Ebene darueber - so nimmt ein Deinstallieren es nicht mit
(siehe gemeinsam.standard_ordner).
"""
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
return os.path.join(heim, "ravokk")
def _zeiger_datei():
"""Merkzettel, WO der Datenordner liegt - liegt bewusst ausserhalb davon."""
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
@ -106,11 +143,22 @@ def daten_ordner():
log.warning("Datenordner %s ist nicht erreichbar - es wird nichts geändert", gemerkt)
return gemerkt if _beschreibbar(gemerkt) else EXE_DIR
kandidat = os.path.join(EXE_DIR, "Daten")
# Erste Wahl: der Ordner der Firma. Dort liegen beide Programme und das
# gemeinsame Nummernbuch - alles an einer Stelle, und ein Deinstallieren
# raeumt nur den Programmordner.
# NEBEN dem Programm, nicht darin - siehe firmen_ordner(). Journal,
# Berichte und Log liegen damit in ravokk, das Programm in
# ravokk/Steuerrechnungstool. Ein Deinstallieren leert nur den
# Programmordner.
kandidat = firmen_ordner()
if not _beschreibbar(kandidat):
kandidat = os.path.join(os.environ.get("LOCALAPPDATA", EXE_DIR), "Beherbergungssteuer")
# Rueckfall wie frueher: neben der EXE, dann der alte eigene Ordner.
kandidat = os.path.join(EXE_DIR, "Daten")
if not _beschreibbar(kandidat):
return EXE_DIR
kandidat = os.path.join(os.environ.get("LOCALAPPDATA", EXE_DIR),
"Beherbergungssteuer")
if not _beschreibbar(kandidat):
return EXE_DIR
_ordner_merken(kandidat)
for name in EIGENE_DATEN:
alt, neu = os.path.join(EXE_DIR, name), os.path.join(kandidat, name)

18
app.py
View file

@ -691,13 +691,25 @@ class App(theme.ThemeMixin, tk.Tk):
"""
self._buch_oeffnen()
if self.buch is None and not TESTLAUF:
self.dlg_gemeinsam(erststart=True)
# Kein Dialog mehr beim ersten Start: der Ordner steht fest und ist
# in beiden Programmen derselbe (gemeinsam.standard_ordner).
ordner = gemeinsam.standard_anlegen()
if ordner:
self.journal.setze("gemeinsam_ordner", ordner)
self._buch_oeffnen()
if self.buch is None:
self.dlg_gemeinsam(erststart=True)
self._anmelden()
self._buch_fuellen()
def dlg_gemeinsam(self, erststart=False):
ordner = ordnerwahl.ordner_abfragen(self, "Steuerjournal",
self.journal.hole("gemeinsam_ordner", ""))
# Ist noch keiner gesetzt, wird der Rechnungsordner vorgeschlagen:
# den kennen beide Programme, er wird mitgesichert, und er ueberlebt
# ein Deinstallieren. Der Ordner, in dem das Programm liegt, taugt
# dafuer NICHT mehr - seit dem Installer sind das zwei getrennte.
ordner = ordnerwahl.ordner_abfragen(
self, "Steuerjournal",
self.journal.hole("gemeinsam_ordner", "") or self.pdf_ordner)
if not ordner:
if erststart:
messagebox.showwarning(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View file

@ -82,14 +82,15 @@ def main():
for ordner in ("build", "dist", "setup", "release"):
shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True)
# Die aufgehellte Fassung des Symbols fuer die dunkle Darstellung. Sie
# wird HIER erzeugt und nicht erst beim ersten dunklen Start: sonst muesste
# das Programm in seinen eigenen Ordner schreiben duerfen - unter
# "Programme" darf es das nicht.
# Das Windows-Symbol aus dem Logo bauen: helle Kachel mit runden Ecken,
# Zeichen darauf, in allen Groessen von 16 bis 256. Es wird HIER erzeugt
# und nicht zur Laufzeit - ein installiertes Programm soll nicht in seinen
# eigenen Ordner schreiben muessen.
sys.path.insert(0, HIER)
import wasserzeichen
dunkles = wasserzeichen.symbol_datei_dunkel()
print(f"--- Symbol dunkel: {dunkles or 'FEHLT'}")
ico = wasserzeichen.symbol_bauen(
os.path.join(HIER, "assets", "programm.ico"))
print(f"--- Symbol: {ico or 'FEHLT'}")
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"],
f"{stamm} {version.VERSION}")

View file

@ -90,6 +90,49 @@ FELDER = ("jahr", "datum", "nachname", "vorname", "naechte", "zwischensumme",
"freigabe")
def standard_ordner():
"""Wo das Nummernbuch liegt, wenn niemand etwas anderes einstellt.
WARUM ES DIESEN FESTEN ORT GIBT (07.09.2026)
--------------------------------------------
Vorher fragte jedes Programm beim ersten Start nach einem Ordner - und der
Nutzer musste in BEIDEN denselben eintragen. Sein Einwand, wortwoertlich:
"wie soll das rechnungstool und steuertool wissen wo es ist, das muss
im ... ravokk liegen und dann zusammen, sonst kommt das eine nicht an
das andere ran"
Er hat recht: die beiden sind ueber genau zwei Dinge verbunden - diese
SQLite-Datei und die kleine API, die "hier ist etwas Neues" ruft. Wenn
schon der Ort geraten werden muss, ist die Verbindung nur so gut wie die
Aufmerksamkeit dessen, der zweimal denselben Pfad tippt. Ein falscher
Buchstabe faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal
vergeben wurde.
Deshalb: ein fester Ort, den beide von selbst kennen. Gefragt wird nur
noch, wer ihn ausdruecklich verschieben will (Menue -> Gemeinsamer Ordner) -
etwa auf ein Netzlaufwerk, wenn die Programme auf zwei Rechnern laufen.
LOCALAPPDATA und nicht "Dokumente": dort liegen die Programmdaten ohnehin,
und niemand raeumt den Ordner versehentlich auf. Das Buch ist ein
REGISTER, kein Beleg - geht es verloren, fehlt keine Rechnung, es wird beim
naechsten Einlesen neu gefuellt.
"""
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
return os.path.join(heim, "ravokk")
def standard_anlegen():
"""Den festen Ordner anlegen und zurueckgeben - oder '' wenn das nicht geht."""
ordner = standard_ordner()
try:
os.makedirs(ordner, exist_ok=True)
return ordner
except OSError as e:
log.warning("Gemeinsamer Ordner %s liess sich nicht anlegen: %s", ordner, e)
return ""
def pfad_zur_datei(ordner):
"""Vollstaendiger Pfad der gemeinsamen Datei in diesem Ordner."""
return os.path.join(ordner or "", DATEINAME)

View file

@ -6,6 +6,14 @@ Rechnungstool und Steuerjournal fuehren ein gemeinsames Nummernbuch
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.
@ -22,10 +30,11 @@ hinueber.
"""
import os
import theme
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import theme
import gemeinsam
BREITE = 780
@ -40,6 +49,18 @@ class OrdnerWahl(tk.Toplevel):
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)
@ -48,7 +69,7 @@ class OrdnerWahl(tk.Toplevel):
breite = BREITE - 60
ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen",
font=("Segoe UI", 13, "bold")).grid(row=0, column=0, sticky="w")
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 "
@ -57,10 +78,10 @@ class OrdnerWahl(tk.Toplevel):
"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="#a00000",
font=("Segoe UI", 10, "bold"),
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(
"der Ordner, in dem auch die Rechnungen liegen.").grid(
row=2, column=0, sticky="w", pady=(0, 14))
wahl = ttk.Frame(rahmen)
@ -68,11 +89,18 @@ class OrdnerWahl(tk.Toplevel):
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))
# "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",
font=("Segoe UI", 9))
bg=self._bg,
font=(theme.SCHRIFT_NAME, 9))
self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0))
knoepfe = ttk.Frame(rahmen)
@ -129,12 +157,12 @@ class OrdnerWahl(tk.Toplevel):
"""
ordner = self.pfad.get().strip()
if not ordner:
self.stand.configure(text="Noch kein Ordner gewählt.", fg="#a00000")
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="#a00000")
self.stand.configure(text=grund, fg=self._warn)
self.ok.state(["disabled"])
return
self.ok.state(["!disabled"])
@ -146,27 +174,39 @@ class OrdnerWahl(tk.Toplevel):
self.stand.configure(
text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – "
"das ist der Ordner des anderen Programms. Genau richtig.",
fg="#1f5c3d")
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="#444444")
"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:
messagebox.showwarning("Ordner geht nicht", grund or "Bitte einen Ordner wählen.",
parent=self)
self._melden("Ordner geht nicht", grund or "Bitte einen Ordner wählen.")
return
if not messagebox.askyesno(
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",
icon="question", default="yes", parent=self):
"Nein = zurück zur Auswahl"):
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
self.feld.focus_set()
return

View file

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""Pruefstand: finden sich die beiden Programme von SELBST?
Sein Einwand vom 07.09.2026, wortwoertlich:
"wie soll das rechnungstool und steuertool wissen wo es ist, das muss im
... 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, die sagt hallo da ist was neues"
Er hat recht: die beiden haengen an genau zwei Faeden - dieser SQLite-Datei
und der kleinen API. Vorher fragte jedes Programm beim ersten Start nach einem
Ordner, und der Nutzer musste in BEIDEN denselben eintragen. Ein Buchstabe
daneben, und jedes Programm fuehrt sein eigenes Buch weiter - auffallen wuerde
das erst, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist
(§ 14 Abs. 4 Nr. 4 UStG).
GEPRUEFT WIRD
1. Beide Programme nennen denselben Standardordner. Das ist die Grundlage;
weichen sie hier ab, hilft alles Weitere nichts.
2. Der Ordner laesst sich anlegen, und darin entsteht EIN Nummernbuch.
3. Was das eine eintraegt, findet das andere - ohne dass jemand einen Pfad
eingegeben hat.
Angefasst wird dabei nichts Echtes: der Standardordner wird ueber die
Umgebungsvariable LOCALAPPDATA auf einen Temp-Ordner umgelenkt.
"""
import os
import shutil
import sys
import tempfile
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kod = sys.stdout.encoding or "utf-8"
print(zeile.encode(kod, "replace").decode(kod))
if not bedingung:
fehler.append(text)
RECHNUNG = r"C:\claude\rechnungstool"
STEUER = r"C:\claude\beherbergungssteuer"
# ---- 1. Beide Programme meinen denselben Ort ----------------------------
# Geladen wird jede Fassung EINZELN, damit wirklich die Datei des jeweiligen
# Programms befragt wird und nicht die zufaellig zuerst importierte.
import importlib.util # noqa: E402
def modul_aus(ordner, name):
pfad = os.path.join(ordner, name + ".py")
kennung = f"{name}_{os.path.basename(ordner)}"
spec = importlib.util.spec_from_file_location(kennung, pfad)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
tmp = tempfile.mkdtemp(prefix="gemeinsam_auto_")
os.environ["LOCALAPPDATA"] = tmp
g_rechnung = modul_aus(RECHNUNG, "gemeinsam")
g_steuer = modul_aus(STEUER, "gemeinsam")
a, b = g_rechnung.standard_ordner(), g_steuer.standard_ordner()
pruefe(a == b, f"beide Programme nennen denselben Ordner ({a})")
pruefe(a.startswith(tmp),
"und er haengt an LOCALAPPDATA, nicht an einem festen Pfad im Quelltext")
pruefe(os.path.basename(a) == "ravokk",
f"er heisst 'ravokk', wie er es wollte ({os.path.basename(a)})")
# ---- 2. Er wird angelegt, und darin liegt EIN Buch ----------------------
angelegt = g_rechnung.standard_anlegen()
pruefe(bool(angelegt) and os.path.isdir(angelegt),
f"der Ordner wird angelegt ({angelegt})")
pruefe(g_rechnung.DATEINAME == g_steuer.DATEINAME,
f"beide erwarten dieselbe Datei ({g_rechnung.DATEINAME})")
pruefe(g_rechnung.pfad_zur_datei(angelegt) == g_steuer.pfad_zur_datei(angelegt),
"und damit denselben vollen Pfad")
# ---- 3. Was das eine eintraegt, findet das andere -----------------------
buch_r = g_rechnung.Nummernbuch(angelegt)
buch_s = g_steuer.Nummernbuch(angelegt)
buch_r.eintragen("2026-777", jahr=2026, nachname="Probe", quelle="rechnungstool")
gefunden = [n for n in buch_s.nummern(2026) if n == "2026-777"]
pruefe(bool(gefunden),
"was das Rechnungstool eintraegt, sieht das Steuerjournal sofort")
buch_s.eintragen("2026-778", jahr=2026, nachname="Gegenprobe", quelle="journal")
zurueck = [n for n in buch_r.nummern(2026) if n == "2026-778"]
pruefe(bool(zurueck), "und umgekehrt genauso")
pruefe(len(os.listdir(angelegt)) >= 1
and any(n.endswith(".sqlite3") for n in os.listdir(angelegt)),
f"es liegt genau ein Buch dort ({os.listdir(angelegt)})")
shutil.rmtree(tmp, ignore_errors=True)
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Beide Programme finden dasselbe Nummernbuch ohne Nachfrage")

185
pruef_installation.py Normal file
View file

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
"""Pruefstand: installieren, deinstallieren - bleibt der Ordner sauber?
Seine Ansage vom 07.09.2026:
"du solltest die auch mal deinstallieren und neu installieren von windows
lassen, dass du auch pruefen kannst, dass der ordner dann leer ist und
sauber - will keine tools die unsauber sind"
Er hatte recht. Gemessen blieb nach dem Deinstallieren liegen:
Rechnungstool-1.7.1.exe, Rechnungstool-1.8.0.exe <- vom UPDATER geladen
Daten/config.json, Daten/rechnungstool.pid
Die EXEs kennt der Installer nicht - die hat der Updater abgelegt. Ohne eine
eigene Regel bleiben sie stehen, und mit jeder Fassung wird es eine mehr.
ZWEI DINGE MUESSEN GLEICHZEITIG STIMMEN
---------------------------------------
1. Der PROGRAMMordner ist danach restlos weg.
2. Die DATEN sind unangetastet - Rechnungen, Journal, Nummernbuch. Die sind
zehn Jahre aufbewahrungspflichtig (§ 147 AO); ein Deinstallieren darf sie
nie mitnehmen.
Beides zusammen geht nur, weil die Daten NEBEN dem Programm liegen:
%LOCALAPPDATA%/ravokk/ <- Daten, gemeinsames Nummernbuch
Rechnungstool/ <- Programm, wird geleert
Steuerrechnungstool/ <- Programm, wird geleert
Genau so hat er es beschrieben: *"es gibt ravokk als main verzeichnis, da liegt
config und sqlite drin, und dann gibt es den ordner rechnungstool und
steuertool"* - und dazu, warum es so heisst: *"ravokk ist meine firma und die
programme gehoeren zu der firma"*.
⚠ DIESER PRUEFSTAND INSTALLIERT WIRKLICH. Er laeuft deshalb nur, wenn das
Setup gebaut ist, und er fasst NUR die Programmordner an - vorhandene Daten
werden vorher gesichert und danach zurueckgelegt.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import time
HIER = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HIER)
import version # noqa: E402
LOKAL = os.environ.get("LOCALAPPDATA", "")
RAVOKK = os.path.join(LOKAL, "ravokk")
NAME = os.path.splitext(version.EXE_NAME)[0]
PROGRAMMORDNER = os.path.join(RAVOKK, NAME)
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kod = sys.stdout.encoding or "utf-8"
print(zeile.encode(kod, "replace").decode(kod))
if not bedingung:
fehler.append(text)
def setup_datei():
ordner = os.path.join(HIER, "release")
if not os.path.isdir(ordner):
return ""
for d in sorted(os.listdir(ordner)):
if d.startswith(NAME + "-Setup") and d.endswith(".exe"):
return os.path.join(ordner, d)
return ""
def beenden():
subprocess.run(["powershell", "-NoProfile", "-Command",
"Get-Process | Where-Object { $_.ProcessName -match "
f"'{NAME}|Updater' " + "} | Stop-Process -Force "
"-ErrorAction SilentlyContinue"], capture_output=True)
time.sleep(1.5)
def inhalt(ordner):
if not os.path.isdir(ordner):
return []
gefunden = []
for wurzel, _, dateien in os.walk(ordner):
for d in dateien:
gefunden.append(os.path.relpath(os.path.join(wurzel, d), ordner))
return sorted(gefunden)
setup = setup_datei()
if not setup:
print(f"UEBERSPRUNGEN | kein Setup in release/ (vorher: python bauen.py)")
sys.exit(0)
if not LOKAL:
print("UEBERSPRUNGEN | kein LOCALAPPDATA")
sys.exit(0)
# ---- Vorhandene Daten in Sicherheit bringen -----------------------------
# Hier laeuft echte Software auf einem echten Rechner. Was schon da ist, wird
# beiseitegelegt und hinterher zurueckgestellt - ein Pruefstand darf niemandem
# seine Buchhaltung anfassen.
sicher = tempfile.mkdtemp(prefix="installpruef_")
gesichert = []
if os.path.isdir(RAVOKK):
for d in os.listdir(RAVOKK):
quelle = os.path.join(RAVOKK, d)
if os.path.isfile(quelle):
shutil.copy2(quelle, os.path.join(sicher, d))
gesichert.append(d)
print(f" ({len(gesichert)} vorhandene Datei(en) gesichert)")
beenden()
alter_uninstaller = os.path.join(PROGRAMMORDNER, "unins000.exe")
if os.path.exists(alter_uninstaller):
subprocess.run([alter_uninstaller, "/VERYSILENT", "/SUPPRESSMSGBOXES",
"/NORESTART"], capture_output=True)
time.sleep(6)
# ---- 1. Installieren ----------------------------------------------------
e = subprocess.run([setup, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"],
capture_output=True)
time.sleep(5)
pruefe(e.returncode == 0, f"das Setup laeuft durch (Rueckgabe {e.returncode})")
pruefe(os.path.isdir(PROGRAMMORDNER),
f"das Programm landet unter ravokk ({PROGRAMMORDNER})")
dabei = inhalt(PROGRAMMORDNER)
pruefe(any(d.startswith(NAME + "-") for d in dabei),
f"die Programmdatei liegt dort ({dabei})")
pruefe("Updater.exe" in dabei, "und der Updater daneben")
# ---- 2. Eine Datei anlegen, wie der Updater es taete ---------------------
# So sieht der Ordner aus, nachdem das Programm ein paar Fassungen lang
# gelaufen ist. Genau diese Dateien blieben frueher liegen.
geerbt = os.path.join(PROGRAMMORDNER, f"{NAME}-9.9.9.exe")
with open(geerbt, "wb") as f:
f.write(b"als haette der Updater sie geladen")
with open(os.path.join(PROGRAMMORDNER, "installiert.json"), "w",
encoding="utf-8") as f:
f.write("{}")
# ---- 3. Eine Datendatei daneben, die ueberleben MUSS ---------------------
probe = os.path.join(RAVOKK, "pruefstand-beleg.txt")
with open(probe, "w", encoding="utf-8") as f:
f.write("steht fuer Rechnungen, Journal und Nummernbuch")
# ---- 4. Deinstallieren --------------------------------------------------
beenden()
u = os.path.join(PROGRAMMORDNER, "unins000.exe")
pruefe(os.path.exists(u), "es gibt einen Uninstaller")
if os.path.exists(u):
subprocess.run([u, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"],
capture_output=True)
time.sleep(7)
uebrig = inhalt(PROGRAMMORDNER)
pruefe(not os.path.isdir(PROGRAMMORDNER),
f"der Programmordner ist danach weg ({uebrig})")
pruefe(not uebrig,
f"und darin liegt nichts mehr - auch keine geladene Fassung ({uebrig})")
pruefe(os.path.exists(probe),
"die Daten daneben sind unangetastet (Rechnungen, Journal, Nummernbuch)")
pruefe(os.path.isdir(RAVOKK),
"und der Ordner der Firma steht noch")
# ---- Aufraeumen ---------------------------------------------------------
if os.path.exists(probe):
os.remove(probe)
for d in gesichert:
ziel = os.path.join(RAVOKK, d)
if not os.path.exists(ziel):
os.makedirs(RAVOKK, exist_ok=True)
shutil.copy2(os.path.join(sicher, d), ziel)
shutil.rmtree(sicher, ignore_errors=True)
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Deinstallieren laesst den Programmordner leer - und die Daten stehen")

View file

@ -45,37 +45,79 @@ def pfad(name):
return ""
def symbol_datei_dunkel():
"""Pfad zur aufgehellten ICO - wird angelegt, wenn sie fehlt.
def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22):
"""Das Logo auf eine helle Kachel mit runden Ecken setzen.
Sein Befund am 07.09.2026: *"icon musst wenn es dunkel ist mit ändern auf
das hellere geht sonst mit unter"*. Die dunkle Haelfte des Zeichens liegt
genau auf der Farbe der dunklen Titelleiste.
Seine Ansage am 07.09.2026: *"bei den windows icons kannst du einen
weissen bg setzen hinter die icons, mit abgerundeten ecken, und da das svg
drauf - das geht auf dem desktop unter"*.
`iconbitmap` braucht eine DATEI - ein Bild im Speicher genuegt nicht.
Deshalb wird sie einmal neben die normale gelegt. Gelingt das nicht (kein
Schreibrecht im Programmordner), landet sie im Temp-Ordner; und wenn auch
das nicht geht, bleibt es beim normalen Symbol.
Ein freigestelltes Zeichen ohne Flaeche verschwindet auf einem dunklen
Hintergrundbild - und welchen Hintergrund jemand eingestellt hat, weiss
das Programm nicht. Eine helle Kachel ist auf jedem sichtbar; so machen es
die meisten Programme, und deshalb sieht es auch nicht fremd aus.
`rand` ist der Abstand zum Kachelrand, `radius` die Eckenrundung - beides
als Anteil der Kantenlaenge, damit es in jeder Groesse gleich aussieht.
Die 22 % entsprechen etwa dem, was Windows fuer Kacheln verwendet.
"""
from PIL import Image, ImageDraw
kante = int(kante)
# In vierfacher Groesse zeichnen und herunterrechnen: sonst werden die
# runden Ecken bei 32 oder 48 Pixeln treppig.
fein = kante * 4
maske = Image.new("L", (fein, fein), 0)
ImageDraw.Draw(maske).rounded_rectangle(
(0, 0, fein - 1, fein - 1), radius=int(fein * radius), fill=255)
flaeche = Image.new("RGBA", (fein, fein), tuple(grund) + (255,))
flaeche.putalpha(maske)
innen = max(1, int(fein * (1 - 2 * rand)))
zeichen = bild.convert("RGBA").resize((innen, innen), Image.LANCZOS)
versatz = (fein - innen) // 2
flaeche.paste(zeichen, (versatz, versatz), zeichen)
return flaeche.resize((kante, kante), Image.LANCZOS)
# Groessen, die eine Windows-ICO enthalten soll. 16 und 24 fuer Titelleiste
# und Listen, 32/48 fuer den Desktop, 256 fuer die grosse Ansicht im Explorer.
ICO_GROESSEN = (16, 24, 32, 48, 64, 128, 256)
def symbol_bauen(ziel):
"""Die Windows-ICO aus dem Logo erzeugen. Gibt den Pfad zurueck oder ''.
JEDE Groesse wird als Kachel gebaut: helle Flaeche, runde Ecken, Zeichen
darauf - *"so wie das rustdesk, nur halt alle ecken in der rundung"*. Ein
freigestelltes Logo verschwindet sonst auf einem dunklen
Desktop-Hintergrund.
Auch die 16 Pixel der Titelleiste: dort ist die Kachel zwar winzig, aber
sichtbar. Ein freigestelltes Zeichen war dort je nach Darstellung mal da
und mal nicht - und ein Symbol, das manchmal fehlt, ist schlechter als
eines, das immer klein ist.
Bei den kleinsten Groessen wird der Rand knapper gefasst: bliebe der
Abstand anteilig gleich, waere vom Zeichen bei 16 Pixeln fast nichts mehr
uebrig.
"""
fertig = pfad("programm_dunkel.ico")
if fertig:
return fertig
quelle = pfad("programm_logo.png")
if not quelle:
return ""
ziel = os.path.join(os.path.dirname(quelle), "programm_dunkel.ico")
try:
from PIL import Image
bild = _umfaerben(Image.open(quelle).convert("RGBA"), True)
groessen = [(g, g) for g in (16, 24, 32, 48, 64, 128, 256)]
try:
bild.save(ziel, sizes=groessen)
except OSError:
import tempfile
ziel = os.path.join(tempfile.gettempdir(), "programm_dunkel.ico")
bild.save(ziel, sizes=groessen)
roh = Image.open(quelle).convert("RGBA")
bilder = []
for g in ICO_GROESSEN:
bilder.append(kachel(roh, g, rand=0.10 if g <= 24 else 0.16))
# Pillow schreibt die uebrigen Groessen selbst aus dem groessten Bild -
# deshalb wird das groesste uebergeben und der Rest angehaengt.
bilder[-1].save(ziel, format="ICO",
sizes=[(g, g) for g in ICO_GROESSEN],
append_images=bilder[:-1])
return ziel
except Exception: # noqa: BLE001 - ohne helles Symbol laeuft es weiter
except Exception as e: # noqa: BLE001 - ohne eigenes Symbol laeuft es weiter
print("[symbol] konnte nicht gebaut werden:", e)
return ""
@ -86,10 +128,12 @@ def symbol_setzen(fenster, melden=print, dunkel=False):
steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes
hat.
In der dunklen Darstellung wird die aufgehellte Fassung genommen, sonst
verschwindet die dunkle Haelfte des Zeichens in der dunklen Titelleiste.
`dunkel` wird nicht mehr gebraucht und nur noch entgegengenommen, damit
bestehende Aufrufe weiterlaufen: seit das Symbol eine helle Kachel ist, ist
es in jeder Darstellung sichtbar. Vorher gab es dafuer eine zweite,
aufgehellte Datei - die ist mit der Kachel ueberfluessig geworden.
"""
p = (symbol_datei_dunkel() if dunkel else "") or pfad("programm.ico")
p = pfad("programm.ico")
if not p:
melden("[symbol] assets/programm.ico nicht gefunden")
return False