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
171 lines
6.5 KiB
Python
171 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Wo das Programm seine Daten ablegt.
|
|
|
|
Als EXE legt sich das Programm beim ersten Start einen Ordner an - dort liegen
|
|
Journal, Berichte und Log beieinander. Zum Sichern reicht damit dieser eine
|
|
Ordner.
|
|
|
|
1. neben der EXE ein Unterordner "Daten"
|
|
2. geht das nicht (EXE liegt z. B. unter "Programme"), dann der Ordner
|
|
"Beherbergungssteuer" unter LOCALAPPDATA
|
|
|
|
Was schon da ist, wird benutzt; was fehlt, wird angelegt. Lagen die Dateien
|
|
noch direkt neben der EXE - so war es frueher -, ziehen sie mit um. Sonst waere
|
|
das Journal ploetzlich "leer" und die Meldung ans Amt faengt bei null an.
|
|
|
|
Aus der Quelle heraus (python app.py) bleibt alles, wo es immer war.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
log = logging.getLogger("bst.ablage")
|
|
|
|
if getattr(sys, "frozen", False):
|
|
EXE_DIR = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
else:
|
|
EXE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Diese Dateien und Ordner gehoeren dem Programm und ziehen mit um.
|
|
EIGENE_DATEN = ("journal.sqlite3", "beherbergungssteuer.log", "berichte")
|
|
|
|
|
|
def _beschreibbar(ordner):
|
|
"""Kann in diesen Ordner geschrieben werden? Wird ausprobiert, nicht geraten."""
|
|
try:
|
|
os.makedirs(ordner, exist_ok=True)
|
|
probe = os.path.join(ordner, ".schreibprobe")
|
|
with open(probe, "w") as f:
|
|
f.write("x")
|
|
os.remove(probe)
|
|
return True
|
|
except OSError:
|
|
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("~")
|
|
return os.path.join(heim, "Beherbergungssteuer", "datenordner.txt")
|
|
|
|
|
|
def _gemerkter_ordner():
|
|
"""(Pfad, Lage): Lage ist "ok", "unerreichbar" oder None (kein Merkzettel).
|
|
|
|
Faengt bewusst mehr als OSError ab: ein zerrissener Merkzettel (Stromausfall
|
|
beim Schreiben) enthaelt Null-Bytes oder kaputte Zeichen - der Fehler wuerde
|
|
sonst beim Import fliegen, also ohne Fenster.
|
|
"""
|
|
try:
|
|
with open(_zeiger_datei(), "r", encoding="utf-8") as f:
|
|
pfad = f.read().strip()
|
|
except (OSError, ValueError, UnicodeDecodeError) as e:
|
|
log.debug("Merkzettel nicht lesbar: %s", e)
|
|
return None, None
|
|
if not pfad or "\x00" in pfad or not os.path.isabs(pfad):
|
|
log.warning("Merkzettel unbrauchbar - es wird neu angelegt")
|
|
return None, None
|
|
try:
|
|
laufwerk = os.path.splitdrive(pfad)[0]
|
|
erreichbar = (not laufwerk) or os.path.exists(
|
|
laufwerk if laufwerk.startswith("\\\\") else laufwerk + os.sep)
|
|
except (OSError, ValueError):
|
|
erreichbar = False
|
|
if not erreichbar:
|
|
return pfad, "unerreichbar"
|
|
return (pfad, "ok") if _beschreibbar(pfad) else (pfad, "unerreichbar")
|
|
|
|
|
|
def _ordner_merken(pfad):
|
|
"""Erst daneben schreiben, dann umlegen - so ist der Merkzettel nie halb."""
|
|
ziel = _zeiger_datei()
|
|
try:
|
|
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
|
neben = ziel + ".neu"
|
|
with open(neben, "w", encoding="utf-8") as f:
|
|
f.write(pfad)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(neben, ziel)
|
|
except (OSError, ValueError) as e:
|
|
log.warning("Datenordner konnte nicht gemerkt werden: %s", e)
|
|
|
|
|
|
def daten_ordner():
|
|
"""Der Ordner, in dem Journal, Berichte und Log liegen."""
|
|
if not getattr(sys, "frozen", False):
|
|
return EXE_DIR
|
|
|
|
gemerkt, lage = _gemerkter_ordner()
|
|
if lage == "ok":
|
|
return gemerkt
|
|
if lage == "unerreichbar":
|
|
# Stick ab, NAS aus: NICHT still woanders neu anfangen - sonst hat der
|
|
# Nutzer zwei Journale und keinen Weg zurueck.
|
|
log.warning("Datenordner %s ist nicht erreichbar - es wird nichts geändert", gemerkt)
|
|
return gemerkt if _beschreibbar(gemerkt) else EXE_DIR
|
|
|
|
# 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):
|
|
# Rueckfall wie frueher: neben der EXE, dann der alte eigene Ordner.
|
|
kandidat = os.path.join(EXE_DIR, "Daten")
|
|
if not _beschreibbar(kandidat):
|
|
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)
|
|
if os.path.exists(alt) and not os.path.exists(neu):
|
|
try:
|
|
os.replace(alt, neu)
|
|
log.info("%s in den Datenordner übernommen", name)
|
|
except OSError as e:
|
|
log.warning("%s ließ sich nicht übernehmen: %s", name, e)
|
|
return kandidat
|