Sein Befund "und beim steuertool fehlt das theam in genze oder wird nicht durch die automatik gesetzt" "nur die einstellunden hilfe leiste die ist noch hell und der icon und titel leiste auch" Er hatte recht, und zwar wortwoertlich: dieses Programm lief im Tk-Standard, mit dreimal fest eingetragenem foreground="black". Es gab hier gar keine theme.py. Gebaut theme.py aus dem Rechnungstool uebernommen und dabei von config.py geloest - eine Datei, die in BEIDEN Programmen liegt, darf nichts aus einem der beiden importieren. Beim ersten Uebernehmen flog sie genau daran beim Import auseinander. Die Einstellungen liegen hier in der Journal-Tabelle, nicht in einer config.json. _einstellungen() ist die Bruecke: etwas mit .get() und []=, wie hinweis.py und theme.py es erwarten. Menueleiste aus menueleiste.py statt tk.Menu - ein natives Menue laesst sich unter Windows nicht einfaerben, das war der letzte helle Rest. Treeview-Stil in theme.py ergaenzt (ttk zeichnet Listen nicht ueber den Grundstil), Reiter schmaler, damit alle zwoelf Monatsnamen lesbar bleiben. Die Summenleisten in gui_monat.py holen ihre Farben jetzt vom Fenster statt sie fest zu tragen - sie waren ein weisser Block unten im dunklen Bild. Logo und Fenstersymbol werden fuer die dunkle Darstellung aufgehellt. ⚠️ Altfehler behoben: die Meldung des Rechnungstools stand nie pruef_wache lief mal gruen und mal rot - je nachdem, wann er hinsah. Das war kein Wackler im Pruefstand, sondern sein Symptom: meldet das Rechnungstool eine neue Rechnung, laeuft sofort das Einlesen an, und dessen Ergebnis landete in derselben Zeile. Der Benutzer sah NIE, dass etwas hereingekommen war. melde() kennt jetzt einen Vorrang in Sekunden; die Meldung des Rechnungstools bekommt sechs. Dreimal hintereinander stabil gruen. Ein wackelnder Pruefstand ist keine Nebensache, die man wegdrueckt. AUFGABE_STEUERSAETZE_DYNAMISCH.md Seine naechste Ansage notiert: die festen Saetze sollen raus, dynamisch per Tabelle wie im Rechnungstool. Mit dem, worauf dabei zu achten ist - ein Satz gilt ab einem DATUM, und die Saetze bereits erfasster Buchungen bleiben stehen (§ 147 AO). Fassung 1.9.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Der ganze Weg von der Quelle zur Setup-Datei - an EINER Stelle.
|
|
|
|
WARUM DAS HIER STEHT
|
|
--------------------
|
|
Bis hierher lag der Bau in einer Folge von Befehlen, die jemand von Hand
|
|
eintippen musste: PyInstaller fuers Programm, PyInstaller fuer den Updater,
|
|
umbenennen, Inno Setup mit den richtigen Schaltern. Wer einen Schritt
|
|
vergisst, liefert eine Setup-Datei aus, die eine alte EXE enthaelt - und
|
|
niemand sieht es ihr an. Genau das ist am 06.09.2026 passiert (der Installer
|
|
schrieb ueber eine laufende, gesperrte EXE hinweg und meldete trotzdem Erfolg).
|
|
|
|
Ab jetzt ist der Bau ein Befehl:
|
|
|
|
python bauen.py
|
|
|
|
WAS ER TUT
|
|
----------
|
|
1. Die Nummer kommt aus version.py - nicht von der Kommandozeile. So kann sie
|
|
nicht auseinanderlaufen mit dem, was das Programm ueber sich selbst sagt.
|
|
2. Programm und Updater werden gebaut.
|
|
3. Die Programm-EXE bekommt die Nummer in den Namen. Der Updater erkennt daran,
|
|
welche Fassungen installiert sind, und legt neue DANEBEN statt darueber -
|
|
deshalb kann er auch dann austauschen, wenn Windows die alte Datei sperrt.
|
|
4. Inno Setup baut die Setup-Datei.
|
|
5. Zum Schluss stehen die SHA256-Summen da. Die gehoeren in den Release-Text:
|
|
nur damit laesst sich spaeter nachweisen, dass die heruntergeladene Datei
|
|
genau die gebaute ist.
|
|
"""
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HIER)
|
|
|
|
import version # noqa: E402
|
|
|
|
# Inno Setup liegt bei ihm unter LOCALAPPDATA (Installation ohne Adminrechte).
|
|
ISCC_ORTE = [
|
|
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Inno Setup 6", "ISCC.exe"),
|
|
r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
|
|
r"C:\Program Files\Inno Setup 6\ISCC.exe",
|
|
]
|
|
|
|
|
|
def summe(pfad):
|
|
h = hashlib.sha256()
|
|
with open(pfad, "rb") as f:
|
|
for brocken in iter(lambda: f.read(1 << 20), b""):
|
|
h.update(brocken)
|
|
return h.hexdigest()
|
|
|
|
|
|
def lauf(befehl, was):
|
|
print(f"--- {was}")
|
|
e = subprocess.run(befehl, cwd=HIER, capture_output=True, text=True,
|
|
encoding="utf-8", errors="replace")
|
|
if e.returncode != 0:
|
|
print(e.stdout[-3000:])
|
|
print(e.stderr[-3000:])
|
|
raise SystemExit(f"{was} fehlgeschlagen (Rueckgabe {e.returncode})")
|
|
|
|
|
|
def iscc():
|
|
for p in ISCC_ORTE:
|
|
if p and os.path.exists(p):
|
|
return p
|
|
raise SystemExit("ISCC.exe nicht gefunden - Inno Setup 6 fehlt")
|
|
|
|
|
|
def main():
|
|
stamm, endung = os.path.splitext(version.EXE_NAME)
|
|
mit_nummer = f"{stamm}-{version.VERSION}{endung}"
|
|
|
|
# Die Bauordner werden geleert. Ein PyInstaller-Lauf ueber einen alten
|
|
# Bauordner uebernimmt sonst Reste, die es in der Quelle nicht mehr gibt -
|
|
# und in setup/ laege noch die Datei der VORIGEN Nummer herum. Beim
|
|
# Ausliefern greift man dann leicht zur falschen.
|
|
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.
|
|
sys.path.insert(0, HIER)
|
|
import wasserzeichen
|
|
dunkles = wasserzeichen.symbol_datei_dunkel()
|
|
print(f"--- Symbol dunkel: {dunkles or 'FEHLT'}")
|
|
|
|
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"],
|
|
f"{stamm} {version.VERSION}")
|
|
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"],
|
|
"Updater")
|
|
|
|
quelle = os.path.join(HIER, "dist", version.EXE_NAME)
|
|
ziel = os.path.join(HIER, "dist", mit_nummer)
|
|
os.replace(quelle, ziel)
|
|
|
|
lauf([iscc(), f"/DVersion={version.VERSION}", f"/DExeProgramm={mit_nummer}",
|
|
"Installer.iss"], "Setup")
|
|
|
|
# ---- release/ : genau das, was ins Release gehoert ---------------------
|
|
# Der Anhang, den der Updater sucht, heisst OHNE Nummer - so steht es in
|
|
# version.EXE_NAME, und danach sucht passendes_release(). Haengt man ihn
|
|
# mit Nummer an, findet der Updater nie ein Update und meldet stumm
|
|
# "aktuell". Die Nummer kommt erst beim Ablegen dazu (updater.dateiname).
|
|
#
|
|
# Daneben die Pruefsumme als eigene Datei: ohne sie prueft der Updater nur
|
|
# die GROESSE. Eine Datei, die unterwegs verfaelscht wurde, hat aber genau
|
|
# die richtige Groesse.
|
|
freigabe = os.path.join(HIER, "release")
|
|
os.makedirs(freigabe, exist_ok=True)
|
|
shutil.copy2(ziel, os.path.join(freigabe, version.EXE_NAME))
|
|
with open(os.path.join(freigabe, version.EXE_NAME + ".sha256"),
|
|
"w", encoding="ascii") as f:
|
|
f.write(summe(ziel) + chr(10))
|
|
shutil.copy2(os.path.join(HIER, "dist", "Updater.exe"),
|
|
os.path.join(freigabe, "Updater.exe"))
|
|
for name in os.listdir(os.path.join(HIER, "setup")):
|
|
shutil.copy2(os.path.join(HIER, "setup", name),
|
|
os.path.join(freigabe, name))
|
|
|
|
print()
|
|
print(f"FERTIG | {stamm} {version.VERSION}")
|
|
print()
|
|
print(" release/ - genau diese Dateien gehoeren ans Release:")
|
|
for name in sorted(os.listdir(freigabe)):
|
|
p = os.path.join(freigabe, name)
|
|
print(f" {name:44s} {os.path.getsize(p):>10,d} B {summe(p)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|