rechnungstool/pruef_installation.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

185 lines
6.7 KiB
Python

# -*- 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")