Steuerrechnungstool/pruef_installation.py
TheMockTv 96671027c8 Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle
Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe
Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer
sich pruefbar - nach jeder liefen beide Pruefketten gruen.

* Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks.
  Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung
  des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt
  hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine
  Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird
  ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm()
  begrenzt; am unteren Fensterrand lief sie vorher hinaus.
  Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse.

* %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt
  fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren
  die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn
  eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG).

* pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py.
  Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in
  einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py
  im Rechnungstool schrieb ein eigenes Format.

* dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden
  app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen
  denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es
  eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31)
  und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen.

* pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft
  ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt
  == 1, und der Dateiname wird mitgeprueft.
  Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden
  Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die
  Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF.

* .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen.
  Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen
  Unterschied, den es im Repository gar nicht gibt.

* Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py
  sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben:
  dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung
  waere Rauschen.

* Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=),
  kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von
  theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes".

39 Pruefstaende, 0 rot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 18:39:03 +02:00

190 lines
7.2 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.
Und er hinterlaesst den Zustand, den er vorgefunden hat: war das Programm
vorher installiert, ist es danach wieder installiert. Beim ersten Lauf war es
das NICHT - danach stand ein deinstalliertes Programm da, und der naechste
Schritt lief ins Leere. Ein Pruefstand, der aufraeumt, muss auch sich selbst
aufraeumen.
"""
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
from pruefhelfer import fehler, pruefe
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)
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()
war_installiert = os.path.exists(os.path.join(PROGRAMMORDNER, "unins000.exe"))
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)
if war_installiert:
# So hinterlassen, wie es vorgefunden wurde.
subprocess.run([setup, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"],
capture_output=True)
time.sleep(5)
print(f" (war vorher installiert - wieder eingerichtet: "
f"{os.path.isdir(PROGRAMMORDNER)})")
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")