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
190 lines
7.2 KiB
Python
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")
|