From 73042b771c6202256cc0bdc0a81f4a8a449107e6 Mon Sep 17 00:00:00 2001 From: TheMockTv Date: Sun, 6 Sep 2026 20:34:38 +0200 Subject: [PATCH] Logo in der Statuszeile, Bau an einer Stelle, Startprobe fuer die EXE Logo Das Programmlogo steht jetzt klein neben "powered by ravokk" - dort sitzt die Kennung des Werkzeugs, nicht die des Kunden. Eine eigene Kopfzeile hat dieses Programm nicht; oben sind die Reiter. bauen.py Der Weg von der Quelle zur Setup-Datei war eine Folge von Befehlen, die jemand von Hand eintippen musste. Wer einen Schritt vergisst, liefert eine Setup-Datei mit einer alten EXE aus - und sieht es ihr nicht an. Genau das ist heute passiert. Jetzt ist es ein Befehl, die Nummer kommt aus version.py, und am Ende stehen die SHA256-Summen fuer den Release-Text. pruef_start_exe.py Prueft, was kein anderer Pruefstand prueft: ob die GEBAUTE EXE ein sichtbares Fenster zeigt. Alle uebrigen fahren das Programm aus der Quelle, dort trat der Fehler nie auf. "Der Prozess laeuft" beweist dabei nichts - er lief ja, waehrend nichts zu sehen war. Aufgeraeumt "Rechnung Campinghof.exe" ist weg; gebaute Dateien gehoeren ins Release, nicht ins Repo. Die .spec-Dateien dagegen schon: ohne sie kann niemand die EXE nachbauen, die der Updater ausliefert. Fassung 1.7.0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax --- .gitignore | 15 + AUFGABE_AUSSENDIENST_ARBEITSPROTOKOLL.md | 95 ++++++ AUFGABE_ONLINE_API_MYSQL.md | 417 +++++++++++++++++++++++ Diagnose.spec | 48 +++ Installer.iss | 95 ++++++ Rechnungstool.spec | 48 +++ Updater.spec | 24 ++ app.py | 64 +++- assets/programm.ico | Bin 0 -> 51609 bytes assets/programm_logo.png | Bin 0 -> 24819 bytes assets/programm_logo.svg | 86 +++++ bauen.py | 109 ++++++ pdf_renderer.py | 2 +- pruef_hinweis.py | 28 +- pruef_selbstbezug.py | 123 +++++++ pruef_start_exe.py | 137 ++++++++ pruef_updater.py | 25 ++ updater.py | 33 +- version.py | 4 +- wasserzeichen.py | 106 ++++++ 20 files changed, 1445 insertions(+), 14 deletions(-) create mode 100644 AUFGABE_AUSSENDIENST_ARBEITSPROTOKOLL.md create mode 100644 AUFGABE_ONLINE_API_MYSQL.md create mode 100644 Diagnose.spec create mode 100644 Installer.iss create mode 100644 Rechnungstool.spec create mode 100644 Updater.spec create mode 100644 assets/programm.ico create mode 100644 assets/programm_logo.png create mode 100644 assets/programm_logo.svg create mode 100644 bauen.py create mode 100644 pruef_selbstbezug.py create mode 100644 pruef_start_exe.py create mode 100644 wasserzeichen.py diff --git a/.gitignore b/.gitignore index 3833f58..a21aa42 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,18 @@ _tmp/ *.7z.* release/ rechnungstool.pid + +# Gebaute Dateien - die gehoeren ins Release, nicht ins Repo. +*.exe +setup/ + +# Die Bauanleitungen selbst gehoeren SEHR WOHL hinein: ohne sie kann niemand +# die EXE nachbauen, die der Updater ausliefert. +!Rechnungstool.spec +!Updater.spec +!Diagnose.spec + +# Muster-PDFs. Der GAST darin ist erfunden - der BETRIEB nicht: sie tragen +# IBAN, Steuernummer und die private Rufnummer des Auftraggebers im Briefkopf. +# In ein oeffentliches Repository gehoert das nicht. +muster/ diff --git a/AUFGABE_AUSSENDIENST_ARBEITSPROTOKOLL.md b/AUFGABE_AUSSENDIENST_ARBEITSPROTOKOLL.md new file mode 100644 index 0000000..c520033 --- /dev/null +++ b/AUFGABE_AUSSENDIENST_ARBEITSPROTOKOLL.md @@ -0,0 +1,95 @@ +# Außendienst: Arbeitsprotokoll auf dem Tablet, Rechnung fällt heraus + +Seine Idee vom 06.09.2026, wörtlich: + +> „mach es an der kundenanzahl fest, und die bekommen eine vorab-nummer, die reserviert ist von der +> disponentin. die kunden haben eh eine kundennummer — heißt, es ist nur die rechnungsnummer, die +> zugeteilt wird. und hans hat nur die kundennummer mit adresse und wohnort im log, wo er +> protokolliert: anfahrt drücken, angekommen drücken … dann arbeit start und stop, dann noch: was +> hat er eigentlich gemacht — also gleich arbeitsprotokoll — und abfahrt. plus die rechnung für den +> kunden anhand des arbeitsblocks, den wir schon haben." + +**Status: Idee. Nichts davon ist gebaut.** + +--- + +## Was sich dadurch ändert + +Die Rechnung wird **nicht mehr getippt, sondern fällt aus dem Arbeitsprotokoll heraus.** Wer ohnehin +Anfahrt, Ankunft, Beginn und Ende drückt, hat am Abend die Rechnung fast fertig, ohne sie +geschrieben zu haben. + +Damit ist das **ein anderes Produkt** als das heutige Rechnungstool: eine Auftrags- und +Zeiterfassung, bei der die Rechnung das Ergebnis ist. Der Arbeitsdatensatz wird primär, die Rechnung +abgeleitet. + +## Die Nummernlogik wird dadurch einfacher + +- **Die Kundennummer hat der Kunde schon.** Reserviert wird nur die **Rechnungsnummer**. +- Die Blockgröße ergibt sich aus den **zugeteilten Aufträgen** — siehe + `AUFGABE_ONLINE_API_MYSQL.md`, Abschnitt „Woher die Blockgröße kommt". +- Hans hat auf dem Gerät nur, was er braucht: Kundennummer, Anschrift, Ort. + +--- + +## ⭐⭐ ENTSCHEIDUNG (Andreas, 06.09.2026): Daten auf dem Server, Rechnung im BÜRO + +> „das liegt auf dem server, und im büro wird die rechnung geschrieben. denke, das ist sauber — +> aber so weit sind wir noch nicht." + +**Das räumt ein Problem ab, und zwar das größte.** + +Wird die Rechnung im Büro geschrieben, **entfällt die Offline-Nummernvergabe komplett**. Das Tablet +erfasst nur Arbeit — und ein Arbeitsprotokoll braucht **keine Rechnungsnummer**. Die entsteht im +Büro, wo Netz da ist. + +→ **Nummernkontingente, Puffer und Blockverlängerung werden damit von einer Voraussetzung zu einer +OPTION** — nötig erst, falls später doch beim Kunden live abgerechnet werden soll. Die Überlegungen +dazu bleiben in `AUFGABE_ONLINE_API_MYSQL.md` stehen, sind aber nicht mehr auf dem kritischen Weg. + +**Das Tablet muss dann nur zweierlei können:** +1. den **Kundenstamm zum Nachschlagen** vorhalten (Zwischenspeicher mit sichtbarem Alter) +2. die **Protokolle zwischenspeichern**, bis wieder Netz da ist + +Beides **ohne Nummernvergabe** — also ohne die Gefahr, dass zwei Geräte sich in die Quere kommen. + +## ⚠️ Drei Punkte, die vorher geklärt sein müssen + +### 1. ⭐ Erfasste Zeit ist NICHT berechnete Zeit + +Anfahrt ist mal berechenbar, mal nicht. Pausen laufen mit, gehören aber nicht auf die Rechnung. + +**Wird das Protokoll eins zu eins zur Rechnungszeile, streitet der Betrieb irgendwann mit dem Kunden +über eine Minute, die das Programm erfunden hat.** + +> **Regel: Aus dem Protokoll wird ein VORSCHLAG, aus dem Vorschlag die Rechnung.** Das Protokoll +> bleibt unverändert stehen, auch wenn anders abgerechnet wird. + +Dann ist beides sauber und man kann später erklären, warum abgewichen wurde. + +### 2. Start/Stopp drücken heißt: das ist ARBEITSZEIT + +Damit hängt Arbeitsrecht daran, nicht nur Steuerrecht: + +- Aufzeichnungs- und Aufbewahrungspflicht (**§ 16 ArbZG**) +- Die Aufzeichnungen gehören dem **Arbeitsverhältnis**, nicht dem Kunden +- → **getrennt von den Kundendaten behandeln**, und sie dürfen nicht einfach in der Rechnung landen + +### 3. ⚠️ „Anfahrt/Angekommen" ist Standortverfolgung von Mitarbeitern + +Sobald Zeiten und Orte zusammenkommen, ist es Mitarbeiterüberwachung. In Deutschland heikel: + +- braucht eine **Rechtsgrundlage** und klare **Zweckbindung** (Art. 88 DSGVO, § 26 BDSG) +- gibt es je einen **Betriebsrat**: mitbestimmungspflichtig (§ 87 Abs. 1 Nr. 6 BetrVG) + +**Beherrschbar, solange der Mitarbeiter selbst drückt** und nichts im Hintergrund mitläuft. Ein +Programm, das ohne Zutun aufzeichnet, wo jemand ist, wäre etwas völlig anderes — und dann wird es +ernst. + +--- + +## Was schon da ist und wiederverwendet wird + +- der **Arbeits-/Leistungsblock** (Katalog) aus dem Rechnungstool +- der **Kundenstamm** (offen, siehe `AUFGABE_KUNDENSTAMM_UND_WEB.md`) +- die **Nummernkontingente** (siehe `AUFGABE_ONLINE_API_MYSQL.md`) diff --git a/AUFGABE_ONLINE_API_MYSQL.md b/AUFGABE_ONLINE_API_MYSQL.md new file mode 100644 index 0000000..31ca040 --- /dev/null +++ b/AUFGABE_ONLINE_API_MYSQL.md @@ -0,0 +1,417 @@ +# Online-Betrieb: API, MySQL, Tablet, Rechnungsversand + +Seine Idee vom 06.09.2026, wörtlich: + +> „die software ist aktuell nur lokal, und da reicht sqlite. wenn wir die online funktion einbauen, +> dann muss man einen api key eintragen, und die api verteilt das auf die mysql — dann ist das +> extern auf dem server und du kannst von überall über domain und apikey auch unterwegs per tablet +> beim kunden die rechnung schreiben, live oder vorab-rechnung, und der smtp-server schickt sie +> direkt dem kunden auf die hinterlegte e-mail. fertig. alles automatisch." + +**Status: Idee. Nichts davon ist gebaut.** Der Schnitt ist richtig gedacht — lokal SQLite, zentral +MySQL, die API als einzige Tür dazwischen. + +--- + +## 1. ⭐ Die Rechnungsnummer darf nur vom Server kommen + +Heute stimmen sich zwei lokale Programme über eine gemeinsame SQLite ab. Zentral wird das +**einfacher**: eine Transaktion in MySQL vergibt die Nummer, fertig. + +**Aber:** Auf dem Tablet beim Kunden ohne Empfang darf keine Nummer entstehen. + +> **Regel: Offline entsteht ein ENTWURF, keine Rechnung.** Die Nummer wird vergeben, sobald der +> Server erreichbar ist. + +Sonst gibt es irgendwann zwei Belege mit derselben Nummer — genau der Fehler, wegen dem das +Amt-Blatt herausgeflogen ist (§ 14 Abs. 4 Nr. 4 UStG). + +Daraus folgt für die Oberfläche: Ein Entwurf muss **sichtbar** ein Entwurf sein, ohne Nummer, und +darf nicht ausgedruckt oder verschickt werden können. + +## ⭐⭐ Das Offline-Problem — und seine Lösung: NUMMERNKONTINGENTE + +Sein Einwand (06.09.2026): + +> „das ding, was wir haben, ist das offline-problem. wenn das internet schwach oder weg ist, bringt +> das online nichts — also musst du auf dem client eine kopie per sqlite haben. wie willst es sonst +> machen?" + +**Er hat recht, und das ersetzt die zu strenge Regel „offline nur Entwürfe" aus Punkt 1.** + +### Was zwischengespeichert werden kann — und was nicht + +| | offline möglich? | +|---|---| +| Kundenstamm, Preise, Einstellungen, alte Rechnungen zum Nachschlagen | **ja** — lokale SQLite, lesend, mit sichtbarem Alter | +| **Vergabe der Rechnungsnummer** | **nein** — zwei Geräte nähmen beide „die nächste" | + +### Die Lösung: der Server gibt Blöcke im Voraus aus + +So machen es Kassensysteme. Tablet A bekommt **2026-101 bis 2026-150**, Tablet B **2026-151 bis +2026-200**. Offline vergibt jedes Gerät aus **seinem eigenen** Block — eine Kollision ist +**unmöglich**, weil kein anderes Gerät diese Nummern besitzt. Bei Netz meldet es den Verbrauch und +holt einen neuen Block. + +→ **Damit sind beim Kunden echte Rechnungen möglich, nicht nur Entwürfe.** + +### Zwei Bedingungen, die dazugehören + +1. **Lücken werden normal — und müssen erklärbar sein.** Kommt Tablet A nur bis 2026-137 und holt + dann einen neuen Block, fehlen 138–150. Das ist zulässig: § 14 UStG verlangt eine **einmalig + vergebene** fortlaufende Nummer, ausdrücklich auch in mehreren Kreisen (z. B. je Filiale). + **Aber das System muss zeigen können, welcher Block wem gehörte und welche Nummern daraus nie + verbraucht wurden.** Sonst sieht ein Prüfer eine Lücke und fragt nach der Rechnung. + → Gehört in den **Steuerprüfungs-Ausdruck** (siehe `AUFGABE_BEWEISKETTE_PRUEFBAR.md`, Schritt 3). +2. **Der Block ist endlich.** Ist er verbraucht und kein Netz da, ist Schluss — dann wieder + Entwürfe. **Lieber ein Gerät, das sagt „ich kann gerade nicht", als eines, das sich Nummern + ausdenkt.** + +### ⭐⭐ Woher die Blockgröße kommt: aus der Disposition (Andreas, 06.09.2026) + +> „die disponentin hat jedem handwerker ja kunden zugeteilt — daraus kann ein block generiert +> werden. die software sagt: handwerker hans werner hat 10 kunden, also bekommt er block A mit 10 +> nummern, der hans werner heißt. dann ist es erklärbar, weil hans die kunden im log hatte. wenn er +> mehr kunden hatte, bekommt er von anfang an 10 puffernummern als schutz. und wenn die nicht +> gebraucht werden, bekommt er sie für den nächsten tag wieder. wenn sie voll sind, überspringt er +> in den nächsten block. der server hält selber immer reserve." + +**Der starke Punkt daran ist die Erklärbarkeit.** Ein Prüfer sieht reservierte Nummernbereiche und +fragt, warum ausgerechnet diese. Hier gibt es eine Antwort **aus dem Betrieb heraus**: Die +Disposition hat Hans an dem Tag zehn Kunden zugeteilt, also zehn Nummern plus Puffer. **Die +Zuteilung ist der Beleg für die Reservierung** — besser als jede technische Begründung. + +### ⚠️ Zwei Korrekturen daran + +**1. Der Block darf NICHT am Kalendertag hängen.** +*„Heute hast du 100 bis 110"* klingt logisch, ist aber gefährlich: Hat Hans zwei Tage keinen +Empfang, wäre er am zweiten Tag arbeitsunfähig, obwohl er noch Nummern hätte. + +> **Der Block gehört ihm, bis er ihn zurückmeldet. Kein Ablaufdatum.** Die Disposition gibt morgens +> neue *dazu*, nimmt aber nichts weg, was er noch hat. + +Am Bild der Disposition ändert das nichts — nur verhungert das Tablet nicht, wenn das Netz drei +Tage weg ist. + +**2. Eine Nummer ist erst verbraucht, wenn ein BELEG entsteht.** +Nicht bei der Zuteilung, und **nicht durch einen Entwurf**. Nur so stimmt, dass unbenutzte Nummern +am nächsten Tag wieder gelten — sonst würden Nummern wiederverwendet, die schon auf einem +Bildschirm standen. + +**Der Rest passt unverändert:** Puffer von Anfang an · Überspringen in den nächsten Block, wenn +voll · **Server hält eigene Reserve** für das, was die Disposition nicht wusste (Notfall, +Zusatzauftrag, Kunde ruft an). + +### ⭐ Beim Synchronisieren: Verbrauch melden, nicht blind neu ausgeben + +Sein Einwand (06.09.2026): + +> „wenn das tablet lte wieder hat, kann er das syncen und bekommt einen neuen block — oder sagt +> einfach: eh, ich habe aber in dem block erst 10 % genutzt" + +**Richtig, und das vermeidet Lücken statt sie nur zu erklären.** Das Gerät meldet seinen Stand: +*„2026-101 bis 110 verbraucht, 111 bis 150 gehören mir noch."* Der Server entscheidet: + +| Lage | Antwort | +|---|---| +| noch viel übrig | **nichts tun** — kein neuer Block, keine verbrannten Nummern | +| fast leer, folgende Nummern frei | **den vorhandenen Block VERLÄNGERN** (101–150 → 101–180) → **keine Lücke** | +| fast leer, folgende Nummern gehören schon jemandem | neuer Block weiter hinten → Lücke, aber eine, die der Server **benennen** kann | + +**Verlängern schlägt Neuausgabe.** Das ist der Unterschied zwischen einem System, das Lücken erzeugt +und sie hinterher erklären muss, und einem, das erst gar keine macht. + +### Warum das nicht optional ist + +> „kannst ja nicht kunden fragen nach wlan" + +Beim Kunden im Keller, im Neubau ohne Anschluss, auf dem Hof: kein WLAN, oft auch kein +Mobilfunknetz. Nach dem Passwort fragen macht kein Handwerker. + +**Zwei praktische Regeln daraus:** + +1. **Der Block muss großzügig sein** — nicht zehn Nummern, sondern so viele, dass eine Woche Arbeit + hineinpasst. Nummern kosten nichts, ein abgerissener Vorgang beim Kunden schon. +2. **Das Gerät warnt, BEVOR es eng wird.** *„Noch 5 Nummern im Vorrat, bitte einmal ins Netz"* — + solange man noch in der Werkstatt steht. Eine Warnung, die erst beim Kunden kommt, ist wertlos. + +Nachgefüllt wird **automatisch**, sobald irgendwo Netz da ist. Der Benutzer soll darüber nicht +nachdenken müssen. + +## 2. ⚠️ Beim Datenschutz kippt die Lage vollständig + +Heute lautet die Aussage im Programm: *Es verlässt nichts den Rechner.* Das ist der ganze +Datenschutztext. Sobald Gästedaten auf einem Server liegen, gilt die DSGVO in voller Breite: + +- verschlüsselte Übertragung (TLS), keine Ausnahme +- **Zugriffstrennung je Betrieb** — ein API-Schlüssel ist ein Passwort, keine Zugriffskontrolle +- Löschkonzept und Aufbewahrungsfristen (§ 147 AO) müssen zusammenpassen +- **Wenn er das für ANDERE Betriebe hostet, ist er deren Auftragsverarbeiter** und braucht einen + Vertrag nach Art. 28 DSGVO. Das ist keine Formalie, sondern Voraussetzung dafür, dass die + Betriebe das Programm überhaupt einsetzen dürfen. + +Der Datenschutztext im Programm muss dann neu geschrieben werden — der heutige wäre schlicht falsch. + +## 3. ⭐⭐ E-Rechnung (ZUGFeRD) — jetzt entscheiden, nicht später + +In Deutschland läuft die Pflicht zur elektronischen Rechnung im B2B an. Wer ein System baut, das +Rechnungen **per Mail verschickt**, sollte die PDF gleich mit **eingebettetem strukturiertem XML** +erzeugen (ZUGFeRD / PDF/A-3). + +Der Unterschied zwischen „von Anfang an mitgedacht" und „später nachrüsten" ist hier ein halber +Neubau: PDF/A-3 stellt andere Anforderungen an Schriften und Farbprofile als das heutige PDF, und +das XML muss aus denselben Daten kommen wie das Blatt — nicht daneben gepflegt werden. + +Stand schon auf der allerersten Aufgabenliste des Rechnungstools. + +## 4. Versand + +- **SMTP genügt zum Senden.** POP3/IMAP braucht man nur, um **Unzustellbarkeitsmeldungen** zu lesen + — und die *muss* man lesen. Sonst meldet das Programm „verschickt", und die Rechnung liegt + nirgends. +- **Eine per Mail verschickte Rechnung ist eine Rechnung** (§ 14 UStG). Für die GoBD muss das + Dokument **genau so archiviert werden, wie es verschickt wurde** — samt Versandnachweis. +- Zustellung ist nicht garantiert. Der Versandstatus gehört sichtbar in die Oberfläche, nicht nur + ins Log. + +## ⭐ Das Produktbild (Andreas, 06.09.2026) — lokal ist der Normalfall + +> „solange er den apikey mit domain nicht hat, kann er auch nur sqlite nutzen — reicht auch, wenn +> einer es als single programm für sich nutzt. will einer das online nutzen, dann trägt er beides +> ein, das programm synct einmal auf die mysql, fertig." + +**Damit ist der Aufbau entschieden: local-first, Online ist eine Zusatzfunktion, die man +einschaltet.** Wer nichts einträgt, merkt vom Server nichts. Kein Konto, keine Pflicht, keine +Abhängigkeit. + +### Die eine Regel, die den Umzug sicher macht + +**„Synct einmal" ist eine EINMALIGE ÜBERNAHME, keine ständige Synchronisierung in beide +Richtungen.** Danach gehört die Nummernvergabe dem Server; die lokale SQLite ist nur noch +Zwischenspeicher für Entwürfe (siehe Punkt 1). + +Zwei Richtungen dauerhaft offen zu halten hieße: zwei Stellen vergeben Nummern. Genau der Fehler, +wegen dem das Amt-Blatt herausflog. + +### Und daraus folgt eine Entscheidung VOR der ersten Übernahme + +Zwei Installationen, die beide 2026-001 bis 2026-050 vergeben haben, lassen sich **nicht** in eine +Datenbank übernehmen — dort lägen Nummern doppelt. Gehen je mehrere Betriebe auf denselben Server, +muss die **Trennung der Nummernkreise vorher** feststehen (eigene Datenbank je Betrieb, oder +Präfix je Betrieb). Nachträglich geht es nicht: die Belege sind dann schon beim Kunden. + +### Was der Umzug NICHT gefährdet + +**Die Wahrheit bleiben die Rechnungs-PDFs** — so steht es schon heute im Nummernbuch-Code: *„Die +Datei ist ein REGISTER, keine zweite Buchhaltung."* Geht bei der Übernahme etwas schief, fehlt +keine Rechnung; es muss nur neu eingelesen werden. + +## ⭐⭐ Die Umschaltung ist eine Tür, die nur in eine Richtung aufgeht + +Sein Modell (06.09.2026): + +> „ein einmal sync, danach ist es only online. die sql ist dann deaktiviert für immer — außer er +> trägt den key und die domain aus, dann hat er aber einen alten datenstand." + +> „das muss er mit einem popup — mit dem speichern-button, den es dafür gibt — am ende noch mal als +> ja/nein-frage kommen, ob ihm das bewusst ist, dass er sozusagen bei 0 anfängt." + +### Harte Anforderung + +**Beim SPEICHERN** von API-Schlüssel und Domain (nicht beim Eintippen) kommt eine Ja/Nein-Frage. +Beim Tippen weiß niemand, ob es ernst gemeint ist — beim Speichern schon. + +Entwurf des Textes: + +> **Auf Online-Betrieb umstellen?** +> +> Die vorhandenen Daten werden einmalig auf den Server übernommen. Danach läuft alles über den +> Server: Rechnungsnummern werden dort vergeben, und die lokale Datenbank wird **nicht mehr +> fortgeführt**. +> +> Entfernen Sie später Schlüssel und Domain, arbeitet das Programm wieder lokal weiter — dann aber +> mit dem Stand von **[Datum von heute]**. Alles, was zwischenzeitlich online entstanden ist, fehlt +> dort. +> +> Diese Umstellung lässt sich nicht rückgängig machen. +> +> Jetzt umstellen? + +Dasselbe Muster wie beim Erprobungshinweis und bei der Freigabe vor dem Beleg: Der Nutzer wird +**vorher** aufgeklärt, und die Entscheidung wird mit Datum vermerkt. + +### Die lokale Datenbank wird STILLGELEGT, nicht gelöscht + +Sie ist der Stand von damals und muss lesbar bleiben — schon wegen § 147 AO. + +### ⭐ Und der Punkt, der daran hängt: die PDFs müssen erreichbar bleiben + +Heute gilt in beiden Programmen: **Die Wahrheit sind die Rechnungs-PDFs, die Datenbank ist nur ein +Register.** Wird künftig auf dem Server erzeugt, muss der Betrieb **jederzeit an alle seine Belege +herankommen — als Dateien**, nicht nur als Ansicht im Browser. + +Der Grund ist **§ 147 AO: zehn Jahre Aufbewahrungspflicht.** Ein System, das den Betrieb von seinen +eigenen Belegen trennen kann — Server fällt aus, Vertrag endet, man zerstreitet sich — bringt den +BETRIEB in Schwierigkeiten. + +→ **Empfehlung: automatische lokale Kopie jeder Rechnung, auch im Online-Betrieb.** Dann bleibt der +Satz „die Wahrheit sind die PDFs" wahr, und der Betrieb hat seine Unterlagen auch dann, wenn nichts +mehr läuft. Das ist zugleich ein Verkaufsargument. + +## 📄 Paperless als Archiv (Andreas, 06.09.2026) — löst die Aufbewahrungsfrage + +> „ich dachte später an ein paperless — da muss eh alles rein" + +**Das ist die bessere Antwort auf § 147 AO als eine Exportfunktion.** Läuft jede Rechnung +automatisch in ein Dokumentensystem, liegt das Archiv **außerhalb** des Programms und überlebt +sowohl einen abgeschalteten Server als auch das Programm selbst. + +**Anbindung ist klein:** Paperless-ngx nimmt Dokumente über eine REST-Schnittstelle **oder über +einen überwachten Ordner** entgegen — Datei hineinlegen, fertig. Für das Rechnungstool eine +zusätzliche Zeile beim Speichern, kein Umbau. + +**⭐ Und die Verschlagwortung ist schon gebaut:** Rechnungsnummer, Datum, Beträge, Art und Vorgang +stehen bereits als maschinenlesbare Kenndaten im `/Subject` jeder PDF (das Steuerjournal liest sie +von dort). Daraus kann das Archiv seine Schlagworte ziehen — ohne Abtippen, ohne Texterkennung. + +⚠️ **Ehrlicher Vorbehalt:** Ein Dokumentensystem macht **nicht automatisch GoBD-konform**. Es +erfüllt den Kern (Belege bleiben unverändert und vollständig), aber dazu gehört eine +**Verfahrensdokumentation** — wie Belege entstehen, wohin sie laufen, wer was darf. Die schreibt +kein Programm. Ein großer Teil davon steht allerdings schon in den Aufgabendateien und +Commit-Texten dieser Projekte. + +## 🔑 Die API ist der Schlüssel zu allem (Andreas, 06.09.2026) + +> „das tool schickt es an die api, die api verteilt das alles. die api ist der schlüssel zu allem — +> wie beim gta fivem server" + +**Das ist seine eigene Serverregel, angewandt:** *Der Wille kommt vom Client, gerechnet wird auf dem +Server.* (Vgl. PcoreLife: nichts im Client, was Serversachen übernimmt.) + +| | Client (Tool/Tablet) | Server (API) | +|---|---|---| +| Was | „Rechnung für diese Leistungen an diesen Kunden" | Nummer vergeben, Beträge und Steuer rechnen, PDF erzeugen | +| Danach | zeigt an, was zurückkommt | verteilt: MySQL · Paperless · Mailversand | + +### ⭐ Folge 1: `berechnung.py` zieht auf den Server + +Heute steht dort ausdrücklich *„die einzige Stelle, an der gerechnet wird"*. Zentral gehört sie auf +den Server. + +**Das löst nebenbei die offene Frage aus `AUFGABE_KUNDENSTAMM_UND_WEB.md`:** die zwei Steuerwelten +(deutsche Bruttopreise 7/19 % gegen polnische Nettopreise mit Reverse Charge). Weiß der Server, +welcher Betrieb anfragt, weiß er auch, wie zu rechnen ist. **Der Client muss davon nichts wissen — +und die Frage „beide Welten in einem Programm oder zwei Fassungen" erledigt sich.** + +### ⭐ Die Aufgabenteilung im Einzelnen (Andreas, 06.09.2026) + +> „das tool schickt nur die metadaten, der server rendert die pdf und das tool bekommt den link. +> oder halt die vorschau muss es selber machen — das auf dem server ist dumm. und paperless macht +> den rest: archivieren und senden an die mitgegebene e-mail, und speichert das unter der +> kundennummer." + +| Wer | Was | +|---|---| +| **Tool** | schickt die Metadaten · zeichnet die **Vorschau lokal** · bekommt den Link zum fertigen PDF | +| **Server (API)** | vergibt die Nummer · rechnet · rendert das endgültige PDF · **verschickt per SMTP** · verteilt | +| **Paperless** | archiviert · verschlagwortet (Kundennummer als Korrespondent/Schlagwort) | + +**Vorschau lokal ist richtig:** ein Netzweg pro Tastendruck wäre sofort träge. + +### ⚠️ Die Regel, die dazugehört: EIN Zeichner, zwei Ausführungsorte + +Zeichnet das Tool die Vorschau und der Server das endgültige Blatt, gibt es **zwei Zeichner — und +zwei Zeichner driften auseinander**. Genau davor schützt sich der heutige Code bereits: +`berechnung.py` wird von Oberfläche UND PDF benutzt, *„damit Bildschirm und Ausdruck garantiert +identisch sind"*. + +Eine Vorschau, die nicht dem entspricht, was hinausgeht, ist **schlimmer als keine** — sie erzeugt +Vertrauen, das nicht gerechtfertigt ist. (Und die Vorschau war seine ausdrückliche Begründung für +das Ganze: *„sonst haben die den storno-aufwand"*.) + +→ **`pdf_renderer.py` und `berechnung.py` bleiben EINE gemeinsame Bibliothek**, die an zwei Orten +ausgeführt wird. Kein zweiter Code. + +### ⚠️ Korrektur: Paperless archiviert, es VERSCHICKT NICHT + +Paperless-ngx nimmt Dokumente entgegen, erkennt Text, verschlagwortet und lagert ein. **Rechnungen +an Kunden mailen kann es nicht.** Der Versand per SMTP gehört auf den Server / in die API. + +Die Ablage unter der Kundennummer geht dagegen sauber — Paperless kennt Korrespondenten und +Schlagworte, die Kundennummer passt in beides. + +### ⛔ Verworfen: PDF auf dem Client zeichnen und hochladen + +Andreas hatte es erwogen (*„ob das schlau ist, weiß ich nicht"*). **Nein**, aus einem Grund: + +Zeichnet der Client den endgültigen Beleg, entscheidet der Client, was darauf steht — und der +Server archiviert blind, was ankommt. Er könnte nicht mehr garantieren, dass zur Nummer, die er +vergeben hat, auch das Blatt gehört, das er ablegt. Eine ältere oder veränderte Fassung des Tools +erzeugte Dokumente, die niemand prüft. + +**Der Beleg entsteht dort, wo auch die Nummer entsteht.** Dazu käme: jede künftige Oberfläche +(Tablet, Browser) müsste den ganzen Renderer mitschleppen. + +Die **Vorschau** bleibt lokal — sie ist kein Beleg: ohne Nummer, mit Wasserzeichen, wie heute. + +### ⭐ Kundenstamm offline: ein ZWISCHENSPEICHER, kein Sync + +Seine Frage: *„wenn ein kunde eine kundennummer bekommt, wie willst die wiederfinden? musst die api +auch abfragen dafür — heißt, ohne internet wird da eh nichts. sonst musst wieder alles lokal und +online speichern, um das schnell zu halten, und dann nur eine art rsync raufpacken."* + +**Der Instinkt stimmt — nur ist es kein Sync, sondern ein Spiegel.** Der Unterschied ist keine +Wortklauberei: + +| | bedeutet | Folge | +|---|---|---| +| **Sync** | beide Seiten dürfen ändern | braucht Konfliktauflösung — wer gewinnt bei doppelter Änderung? | +| **Spiegel** | nur der Server besitzt die Daten, der Client hält eine Lesekopie | **keine Konflikte**, nur ein Stand, der mal älter ist | + +**Und die Datenmenge passt:** Kunden sind wenige und ändern sich selten — anders als Rechnungen. +Den ganzen Kundenstamm lokal zu halten kostet nichts und macht die Suche sofort schnell, auch ohne +Netz. + +**Sein Wort dafür: „eine Art Zwischenspeicher"** — und das ist ehrlicher als „Spiegel", weil es +schon sagt, dass er veralten darf. + +**Zwei Dinge braucht er, sonst wird er selbst zur Fehlerquelle:** + +1. **Er muss sein Alter zeigen.** Klein in der Oberfläche: *„Kundendaten vom 06.09.2026, 15:40"*. + Sonst sucht jemand einen Kunden, findet ihn nicht, legt ihn neu an — dabei war er nur seit + gestern nicht heruntergeladen. **Ein Zwischenspeicher, dem man sein Alter nicht ansieht, wird + irgendwann für die Wahrheit gehalten.** +2. **Er darf nie Quelle für etwas sein, das geschrieben wird.** Lesen und Suchen: ja. Eine Nummer + daraus vergeben: nie. Aufgefrischt wird beim Start (wenn Netz da ist) und auf Knopfdruck. + +**Dieselbe Regel wie bei den Rechnungsnummern:** Ein offline angelegter Kunde bekommt seine Nummer +**erst vom Server** — bis dahin ist er ein Entwurf. Sonst vergeben zwei Tablets dieselbe +Kundennummer. + +### Folge 2: der Client wird schlank + +Das Tablet beim Kunden braucht kein PDF-Werkzeug, keine Steuerlogik, keinen Nummernkreis. Ein Weg +hinein, mehrere hinaus. + +### Der Preis + +**Ohne Server geht nichts außer Entwürfen** (siehe Punkt 1). Bei FiveM ist das dasselbe — nur hängt +hier ein Beleg dran, den jemand zehn Jahre aufbewahren muss. + +### Sicherheit, weil „Schlüssel zu allem" wörtlich zutrifft + +Wenn ein Schlüssel alles öffnet, muss er: **je Installation** vergeben sein (nicht einer für alle), +**widerrufbar**, **nur über TLS** unterwegs, und die API muss **Missbrauch begrenzen** +(Rate-Limit). Sonst ist ein einziger abhandengekommener Schlüssel der Zugang zu allen Gästedaten. + +## 5. Offene Entscheidungen + +- Wer betreibt den Server, und für wen? (nur Bartl, oder mehrere Betriebe) +- Ein Mandant je Datenbank oder ein gemeinsames Schema mit Mandantenspalte? +- Was passiert mit dem lokalen Betrieb — bleibt SQLite als Offline-Fassung, oder wird alles zentral? +- Wie kommt der Bestand in die MySQL, ohne dass Nummern doppelt vergeben werden? + +--- + +**Nichts davon beginnen, bevor die Punkte 1 bis 3 entschieden sind** — sie bestimmen den Aufbau, +nicht die Ausstattung. diff --git a/Diagnose.spec b/Diagnose.spec new file mode 100644 index 0000000..d030d6f --- /dev/null +++ b/Diagnose.spec @@ -0,0 +1,48 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + # Roboto MUSS mit in die exe: sie wird nicht installiert, sondern beim Start + # nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt + # das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders + # aus als hier. + datas=[('assets/programm.ico', 'assets'), + ('assets/programm_logo.png', 'assets'),('assets/logo.png', 'assets'), + ('assets/fonts', 'assets/fonts'), + # Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe + # nur ihren Text, aber der Anordnen-Griff fehlt. + ('assets/icons', 'assets/icons')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='Diagnose', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + icon='assets/programm.ico', + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/Installer.iss b/Installer.iss new file mode 100644 index 0000000..86f6ce7 --- /dev/null +++ b/Installer.iss @@ -0,0 +1,95 @@ +; Installer fuer das Rechnungstool (Inno Setup 6) +; +; ENTSCHEIDUNGEN, DIE HIER STECKEN - und warum +; -------------------------------------------- +; 1. INSTALLATION PRO BENUTZER nach %LOCALAPPDATA%\Programs, NICHT nach +; "Program Files". Grund: der Updater muss die EXE ablegen duerfen. In +; Program Files darf ein normaler Benutzer nicht schreiben - jedes Update +; wuerde an fehlenden Rechten scheitern oder eine Administrator-Abfrage +; erzwingen. Chrome, VS Code, Discord, Teams und Fusion 360 machen es +; genauso, aus demselben Grund. +; +; 2. DIE VERKNUEPFUNG ZEIGT AUF DEN UPDATER, nicht auf das Programm. So schaut +; jeder Start zuerst nach einer neuen Fassung. Der Updater startet danach +; die eigentliche EXE - die ist dabei noch gar nicht in Benutzung und laesst +; sich deshalb problemlos ersetzen. +; +; 3. KEINE DATEN ANFASSEN. Der Installer legt keine Rechnungsordner an, kein +; Journal, kein Nummernbuch - und beim Deinstallieren wird davon nichts +; entfernt. Das Programm schreibt aufbewahrungspflichtige Belege +; (§ 147 AO, zehn Jahre). Ein Deinstallieren darf sie nie mitnehmen. +; +; 4. Der Eintrag unter "Apps & Features" entsteht von selbst - Inno Setup legt +; den Registry-Schluessel unter HKCU an. Da muss niemand von Hand hinein. + +#define Name "Rechnungstool" +#define Herausgeber "ravokk" +#define ExeUpdater "Updater.exe" +#define Webseite "https://git.pcore.de/TheMockTv/rechnungstool" + +[Setup] +AppId={{7F3A9C2E-51D4-4B86-9A17-2C0E8B4D6A31} +AppName={#Name} +AppVersion={#Version} +AppVerName={#Name} {#Version} +AppPublisher={#Herausgeber} +AppPublisherURL={#Webseite} +AppSupportURL={#Webseite}/issues +AppUpdatesURL={#Webseite}/releases + +; --- pro Benutzer, ohne Adminrechte --- +PrivilegesRequired=lowest +; Der Zielordner ist NICHT waehlbar. Wer hier "Programme" auswaehlen +; koennte, braeche sich den Updater: dort darf ein normaler Benutzer +; nicht schreiben. Chrome, Teams und Discord blenden diese Seite aus +; demselben Grund aus. In "Programme" gehoert hoechstens eine +; Verknuepfung - und die liegt ohnehin im Startmenue. +DisableDirPage=yes +DefaultDirName={localappdata}\Programs\{#Name} +DefaultGroupName={#Name} +DisableProgramGroupPage=yes +UsePreviousAppDir=yes + +OutputDir=setup +OutputBaseFilename=Rechnungstool-Setup-{#Version} +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +; Das Programmsymbol - auch fuer den Installer selbst und den +; Eintrag unter "Apps & Features". +SetupIconFile=assets\programm.ico +UninstallDisplayIcon={app}\{#ExeProgramm} +ArchitecturesInstallIn64BitMode=x64compatible + +; Erprobungsfassung - das steht auch im Programm selbst. +AppComments=Erprobungsfassung. Benutzung auf eigene Gefahr. + +[Languages] +Name: "deutsch"; MessagesFile: "compiler:Languages\German.isl" + +[Tasks] +Name: "desktopicon"; Description: "Verknüpfung auf dem Desktop anlegen"; \ + GroupDescription: "Zusätzliche Verknüpfungen:"; Flags: unchecked + +[Files] +; Der Updater - das ist, was der Benutzer startet. +Source: "dist\{#ExeUpdater}"; DestDir: "{app}"; Flags: ignoreversion +; Das Programm selbst, mit der Nummer im Namen. Der Updater erkennt daran, +; was installiert ist, und legt neue Fassungen daneben statt darueber. +Source: "dist\{#ExeProgramm}"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +; Beide Verknuepfungen zeigen auf den UPDATER, nicht auf das Programm. +Name: "{group}\{#Name}"; Filename: "{app}\{#ExeUpdater}" +Name: "{group}\{#Name} deinstallieren"; Filename: "{uninstallexe}" +Name: "{userdesktop}\{#Name}"; Filename: "{app}\{#ExeUpdater}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#ExeUpdater}"; Description: "{#Name} jetzt starten"; \ + Flags: nowait postinstall skipifsilent + +[UninstallDelete] +; Nur die Buchfuehrung des Updaters. +; NICHTS von den Daten des Betriebs - keine Rechnungen, kein Journal, kein +; Nummernbuch. Die liegen ohnehin woanders und muessen zehn Jahre bleiben. +Type: files; Name: "{app}\installiert.json" diff --git a/Rechnungstool.spec b/Rechnungstool.spec new file mode 100644 index 0000000..57d1265 --- /dev/null +++ b/Rechnungstool.spec @@ -0,0 +1,48 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + # Roboto MUSS mit in die exe: sie wird nicht installiert, sondern beim Start + # nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt + # das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders + # aus als hier. + datas=[('assets/programm.ico', 'assets'), + ('assets/programm_logo.png', 'assets'),('assets/logo.png', 'assets'), + ('assets/fonts', 'assets/fonts'), + # Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe + # nur ihren Text, aber der Anordnen-Griff fehlt. + ('assets/icons', 'assets/icons')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='Rechnungstool', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + icon='assets/programm.ico', + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/Updater.spec b/Updater.spec new file mode 100644 index 0000000..a82ec2a --- /dev/null +++ b/Updater.spec @@ -0,0 +1,24 @@ +# -*- mode: python ; coding: utf-8 -*- +# Der Updater ist der Starter: winzige Konsolen-EXE, die vor dem Programm laeuft. +a = Analysis( + ['updater.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=['version'], + hookspath=[], + runtime_hooks=[], + excludes=['tkinter', 'reportlab', 'PIL', 'pypdf', 'pypdfium2', 'numpy'], + noarchive=False, +) +pyz = PYZ(a.pure) +exe = EXE( + pyz, a.scripts, a.binaries, a.datas, [], + name='Updater', + debug=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + icon='assets/programm.ico', +) diff --git a/app.py b/app.py index b833b73..ade214f 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,7 @@ import config import berechnung import gemeinsam import hinweis +import wasserzeichen import ordnerwahl import pdf_renderer from helfer import ANREDEN, kunde_maengel, parse_date, parse_num, saeubern, safe_filename, ziffern @@ -63,6 +64,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, super().__init__() self.cfg = config.load_config() self.title(config.PROGRAMM) + self._fenstersymbol() # Fensterhoehe an den Bildschirm anpassen, damit nichts unter den Rand rutscht hoehe = max(560, min(840, self.winfo_screenheight() - 90)) self.geometry(f"1060x{hoehe}") @@ -118,11 +120,12 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, # es morgen ohne berichtigte Rechnung im Ordner und keiner weiss warum. self.protocol("WM_DELETE_WINDOW", self._beenden) - # after_idle statt after(0): laeuft, sobald Tk nichts mehr zu zeichnen hat - - # also nachdem das Hauptfenster steht. Bei after(0) haengt der modale Dialog - # an einem noch ungezeichneten Fenster und wirkt wie eingefroren; eine feste - # Wartezeit dagegen erreicht ein Pruefstand nie, der keine Schleife dreht. - self.after_idle(self._hinweis_pruefen) + # Der Hinweis darf erst aufgehen, wenn das Hauptfenster WIRKLICH + # gezeichnet ist. after_idle reicht dafuer nicht: in der gepackten + # EXE ist das Fenster dann noch nicht sichtbar, der modale Dialog + # sperrt es - und beide bleiben unsichtbar. Das sah aus wie ein + # haengendes Programm (06.09.2026, am Installer aufgefallen). + self.after(50, self._hinweis_wenn_sichtbar) # ---------------------------------------------------------------- Menue @@ -243,6 +246,11 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, self.flaeche = flaeche.Arbeitsflaeche(self, farben=self) self.flaeche.pack(side="top", fill="both", expand=True, padx=10, pady=(4, 0)) + # Das Wasserzeichen liegt HINTER der Arbeitsflaeche, unten rechts. Es + # wird mit place() gesetzt, damit es die Anordnung der Kaesten nicht + # anfasst - die darf der Benutzer ja selbst verschieben. lower() legt + # es unter alles andere. + self._wasserzeichen_setzen() def _anordnen_umschalten(self): self.flaeche.anordnen_starten() @@ -611,6 +619,12 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, def _build_summen(self): # Statuszeile ganz unten status = ttk.Frame(self, padding=(12, 2)) + # Das Programmlogo neben der Marke - klein, immer sichtbar. Es steht + # rechts bei "powered by ravokk", weil dort die Kennung des Werkzeugs + # sitzt und nicht die des Kunden. + self._kopf_bild = wasserzeichen.kopf(18, master=self) + if self._kopf_bild is not None: + ttk.Label(status, image=self._kopf_bild).pack(side="right", padx=(0, 6)) status.pack(side="bottom", fill="x") ttk.Label(status, text=config.MARKE, foreground=self.col_muted, font=(theme.SCHRIFT_NAME, 8)).pack(side="right") @@ -644,6 +658,40 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, # --------------------------------------------------- Gemeinsames Buch # ------------------------------------------------- Erprobungshinweis + + def _wasserzeichen_setzen(self, groesse=240): + """Das Programmlogo blass hinter die Arbeitsflaeche legen.""" + bild = wasserzeichen.hole(groesse, 0.06, master=self) + if bild is None: + return + # Die Referenz MUSS bleiben - sonst raeumt Python das Bild weg und Tk + # zeigt eine leere Flaeche (der klassische PhotoImage-Fehler). + self._wz_bild = bild + self._wz = tk.Label(self.flaeche, image=bild, borderwidth=0, + background=self.col_bg) + self._wz.place(relx=1.0, rely=1.0, anchor='se', x=-12, y=-12) + self._wz.lower() + + def _fenstersymbol(self): + """Das Programmsymbol auf das Fenster legen.""" + return wasserzeichen.symbol_setzen(self) + print("[symbol] assets/programm.ico nicht gefunden") + return False + + def _hinweis_wenn_sichtbar(self, versuche=0): + """Wartet, bis das Fenster gezeichnet ist, und fragt dann. + + winfo_viewable() sagt, ob das Fenster tatsaechlich auf dem Schirm + ist. Solange nicht, wird in kurzen Abstaenden nachgesehen. Nach + rund zwei Sekunden wird trotzdem gefragt - im Pruefstand ist das + Fenster absichtlich nicht sichtbar, und dort soll es trotzdem + laufen. + """ + if not self.winfo_viewable() and versuche < 40: + self.after(50, lambda: self._hinweis_wenn_sichtbar(versuche + 1)) + return + self._hinweis_pruefen() + def _hinweis_pruefen(self): """Hinweis auf die Erprobungsfassung. Ablehnen beendet das Programm. @@ -1821,7 +1869,11 @@ if __name__ == "__main__": if not fenster_nach_vorn(laufende_pid_lesen()): _r = tk.Tk() _r.withdraw() - self.melden_fehler( + # Hier gibt es kein self - das Fenster wird ja gerade NICHT gebaut. + # Deshalb die schlichte Windows-Meldung statt des eigenen Dialogs. + # (Frueher stand hier self.melden_fehler und ist beim Start eines + # zweiten Fensters mit NameError abgestuerzt.) + messagebox.showerror( "Rechnungstool läuft bereits", """Das Rechnungstool ist schon geöffnet, das Fenster ließ sich aber nicht nach vorn holen - bitte über die Taskleiste zurückwechseln. diff --git a/assets/programm.ico b/assets/programm.ico new file mode 100644 index 0000000000000000000000000000000000000000..cae16c942d04f204b66e0d334615784bcbb9b9d2 GIT binary patch literal 51609 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 D@5wHSQs+p{y`UNgS#s^sYSHUx3Z~ z6>@}n;U1bTs>-v%yA*gEyzkhB$>~z=SS)KkRhUSy$5Eyz^<(wgOo1B$J8!8(X^``Nlr=eTCd-+j)9(d`}^ z*WP1EN3;zgZ@1!!m8uj?^dFt0zaB!Lm?fQov8#49gU-gnw?!1kPjyr3hJCnQ0&60bjz*RKsBiLuLgoRWU%V8rl8C%ht<^lpzZ6mA8Whlt$Skg zh}WnT0E9Cq$<=(}B)LPr4L6z&WE_}58F?ZU^ZW2#`I*u^8;kjlRoW9iDxCbLm7mOx zU?o$}=O*w1P;m4>QQHtO!?CK^gA()ZBhgW|$TM*k8}In9`8%AsYNEM+gZ<$ z4+{{YY%aPXe}a$IGgFh!( zil?7$LR%RquWpukEI>lHCs(gr2lOaSmUbV)WQr2iFUqk2Vx?P0mcC= zu5fxM@KWJ#M=$_`l`_LG>%Ot+ag?pdxAh%?T;_>dC9Xbaxp zL^k=4m?8`R8LUTi^R6d_G&BTRul;zO4hm^90D2+38MZ3+7nkQlKyKe)J<}=9>Cw|d z+MA(YSUyVxXqdh)t2Du_>Y7-vLafLhDooYZ2)T_jttVOcXDp^N_(Qpq-*-Jx62@wS zJM_A3FI9D?_@E>OO=e=S9_v1f;MMuvEh1fMQiN|SY`d0zFYq!Ed`M=K( znb((RwO^pz)L>arUp5W1P$bJ#$9gz%S?pNL_8^OyuhlntnY2M3X4+CqWTH6V{T$48 z&wVCCLxO^l=%_%MVas5O*+i;SyBSW043W5%8Srkskh!9;o%`Ke5^Uk#buUok_ zEieLtL!+cguiMh=Q-8HpDJ||Fj(x@zM++0rBhCKO5=JJ&lX=uXQ;Bi!ZCPa9tMj|& zGvjwqlhX5*dz8ul3vC$HD@a1ozsj%NOW3`nVy>JcO1z=lpJ_|Yiu>NIXPgOE`(EmY z6rLqN+&QauWk63`W5+LLiv-5sV`njgiLq8}8j=9+txOv_5@pR}LREC%h%~~nz+O1a zYsA6vwb;M|ZfQuCESCL)*UvK1MTe(%{ijOD#}-*h{m_AE6qPNFB#6q6Q!>to&2!-6 zwc`uXPxj6mj~5{%`+aD)(#5;;06BmhWej*_QVWWqh6f#)~Wyu;@ zvV7;&z2Eosm*#!mXP$Y^IluFJ&hkJY>o~!|_WK2k5;~iHq9Y;(!ct29A(&JCnI;x8 z1tVgH{);g?`wlEZ_b@NN6q^F`~jkXAudhgv2b!ffm1{# zZ_=5gF**C7EN9`HzHidG(HpgnmaZ8uG7%`a^mjq{X8xdsZr2YMI3fLLVa0xYouT~$ zheu~$My-3YwNV9g-4pZcf#6PFBscq5*!DJ?Gm~F``7Q4@aQ|3#G7T;9Aa*@qY=v-* zsFXA&3d;R8pyC{{sMo`UETT0i?>V(KE9auF@7s@*%;5t5EFCofzVNg(O$oacJoDripRgt5(k~rJ;LlzTY`QiUl;(~1j(S+z zqLXV%RmI+1FLAjpITmh>5*~V| zJJWeoCyRoZKC-!yPWhU$-Ko|evO7MijXZ3r|KwPlU`H&|sZ>!u8?Mu>(K2{00ctO= zZ2nH`>FG&f`UowadZyO0Sa-3AL1p?RT$tO|!%Yg;=$s;6xbfHU_u6tr3V-+RTy>&q zpJ&y`PscsD?xPg$yofr3YvpZCf)cuk!gXDaF0pxF`&Rpf zis5poE#%?1r_^cAd>!StRA zA_RivMF?f|`_jl{$_`jZDUL>`H^Zd|@MJSodFA0q+JW%VvuFqww%8#sObUkaH6p?U z%QWy!VpZKp9GyNL3D3(#WYE{(`V1mb!4O;^M2G0|lh^kZhoT-b1S3pH-FxLnGng~D zOFF&a2z)o;HJ1z#j}`3viCI&B8Dg#~wC|G1uFs_qX&6vmg|0Id$4gTb^MomQq#C^z zAnhF64!y0vA9#y>ql*xtkw#>m28fQ1LKg>9mADGggoqb69#eR+87urCA4z?*bw|IK zncqV0gw3X98OmAR3mz@QO0f%0xaKE}qkcZ#w&i7vs-q959IlAyyD8qN;`^-PTYxoA z5Le$VW;(Nt8>)RZDrHZ)*NhtUfKxB`j!yX@plo{aSR|qyCLcc}MO$Le;raWWqYE(tg6QLDC(IwA~oqTL#Q^dr!0bH!43*R-SjpEGHoOg8}jb zFNL><6=Me-2ga1dWQ?2~&F8R_v)hu6oj=Z{vMZQm%8l~tQ>8vj{7bsWbas{`W_%sl%P}%@(Dj77*xIoZvkK3d#!#Iz>PTmg18yZwmpv0*w~NlEmUU zm<@zz3{1pA`WgdSp#h*}(5RSvRBJux=@8jrf|s2cskpXZ=+x1O-vHdNGuaQ55iHqz zQJwGnJ!ctTC3eS}rj{6bO`=~mIaS^TiJOnA89XrUAQU&mw)d@x!wT}Ds5l_2^#7FG z4uPrJH$Jb)2wnskncNT|@MMkNrR{=brkKi+nnJJLPr3M7RR+cSHnqk21RT#6l%MC%hP@+$)t9C6LH zmmj8c8fs=f`PTb*g?#@Rw3Iv}TWY)H8WqJ5gbpP;?Jt{3Z9f_FxvN@JeT%QX7tPig zAYfS`9++Ev$r%c=m)_DsJ162_PG0Hhofx%>pu1wv28(yulXoCmQrotv&iJL!C1c-N zrGA_Ki0#83Ga zI%7*Dw|#!qYFXdNtM^AZ&msAH7_N~c*aLwDdF0k>6PE`bEtP?#J$&J1rUuzcd9r_Ss@U z+>&S}qT;;e`>o8(@wSc!cKu65IHxszPk8EE&`+sgM_fHic))Q?iEC;B{JoTvzBvC| zrvh33f-wCDA3Whoe1chw#K2$8JhcNSfi-*loNCek4?tLg59>@odFmQL@UZacHQIbv>Kzh?#5TbU6mgMtHV_EO-#PX-cO?u7E#?muARVyNIK=p!B}8|PV| zJYsQ>HeLH#5))F>bh-h{$4@Ebd^x<8;)5za>TcoJ~qj(uTLI&|hLIEcL-~s|6Ur6VuDN7K$%u$vj^dQuA^wvuv5Ii_r@IBZhv&@DP9h zxh*NHEwr;m`~tKjq)fuCUp{a*Koa_s%X|k;{r}&bIMSDX8Mc#aO^SF;>@*HbV$N2; zsh_A|$#~tTzqsi&7u)*BV*?3c9lS)VRPRes3UakSgdEaSh>K3-^1FZfaaBcf3h|cK zdrQXS`prd=t;u-8cim0`qf08&vrB|fchGR_T}Il04%zKvq5v-+M@B`r^6~RL<}PUY zT=nhAxP3r&Z^xQFa5-eYELG5AjP@9r1A-_doTwf9*B)9;nq`Lr>`&cmfoRrY+QK`R z#+jdi=8okaM>P$y(L}_N1}RlqoQt9BX|mHKfy5l(zW2xfiFd5*Do8h%*6%?A{gnaH zqNL;YZ8&+8HwH_#OaN!_wsBZv699{7SQIscH>ZJC2m^9#<3;qY%Gun67q|aJsYAA2 z&nv(h{@=9Pr*B`ZEgtOR!?L{e7115 zPJTc4uQ=F9T#w${+ph+HO|%gdI*iS{)zmp^VW4R0yzY(cSdQO{ zAa&r^OB#oFy{X|QZmZ)=M@=Y301@(lg#9$v;6%E%=@$Uj%P1dvbT8K&XX$8<2Pg(! zi%|6R3(6n^&6QBQK;MJEHQ{f|$us;kAuWNu@$Cs>Gu)nAvi~JV6eq&aV2!hLz&(=L z>!~3>lH6d>R{2$h#S6@jOegzSOtCr&Ax-oB8OD4$!zr@0)luMim2a!;#(h+EE$IW1 zd-?MOTkT^G9@7-dJvbl$KsPp);^M2O;`g3Z)rR&G zr%t*qae$M)QSviHt`el7 zG@+ngpZ42_pm8dc>43c9^EHaNu0Jml@XPLfS+^4R-_7Vx*IIftiy9FA+HRPB4P!l? ztS$2r!L|54I;Fe~*LEWQ0Ozah8nNq_ApsC3uqrAy|LY%goDeB7CcSuW`V+YRkFl%iZxG-!XoU6CYJ~%wQq9Bo<}TVu7PjDBZ$| z4N6g@9d3!uQ~Po>-cUcwkN~vY3rmic)*D;Ub5-+u)$6n(UMY}Lc?&OPT=lZfb}1!_ zCdIQq^I-@dajXUTZ85H)Vsa+NVo!M0)PP4dpxdyGwXk&2MTjJpXv?J@_NOf|_pDgr zj^ouC$|w9JPql<|m_h+_%T$PvRWIlb(y8|Q7jw|?NDe<#!%1_qT@I56()fI*tuIAH z?D#dP;pCq0naQ|p)w`VJLsTty080fhOG2u|00IwNeJ~^F?t+kPglYoF8G}3&gmMT zz91I|`roB6ZgNwEAmHrhH|L6)s|RPWs8nS|oA7CGf)#~Y?Cod%)wTXTR7SErB)G2a zJ_5`?jc-fLnP5$stfTpy@z-vt1~3yFJv6@+6p4Fr@y6ZNJEr9YBfJD%ZP?N#kI-V0 z?gsn|bVIY~Y!A*2=dtgej1uY@PEW|h9oNWjJEW4NPjZ3{$Rn`_k8@-R{T4k-I5>P8 zeemCmRE5eAp4z}=F{a#lpU!g%?wD2HsrCK{goh{NV(-is{vc0{8-^xK0 z_g=3Z``$~g`o+1_xy%u%2i!QDi$-qZLb+SRm60M~FxPENq|Un!RCGAi>XY|~FCAJ= zbnTmQFMArV9JktuO>UWWLdaz77xov$UlX;fLmf``t9`AS;h*3f5oVy8HqfGHjt(1u z0|P~zIM~r$_2%HOvBy646UAizNtedL5h3XQvBBGvL*HPsk&jYi?_pwYb-Z!jHCl~{ z(RLlS>kXe#$vwmN;=g6GVon~o4_#gQtYC509<20TKMphFZBJ&vl{~IKahf+2d(zo9 ziHUJ;)Wlwaz#ldr1W8FgetvLK5)IgsAMOa5TtheR&k}tbYhv3q55p;!qjxy(?F$1W zOX7hxWj2V>UN|;3mj2PZz7Cnpv_p(8$iX?n_gPe=$%psej`+S8byuwF?^INL_@)FE zi^cvGc`InVfQJRXM9ur?(W3%kVfND8lb^Q<-a&PBB;oOieVZU{s9_c56-Xcmm>-bp zeNz19B87WOU(kxWd{*bq5`m*B4kmF%89bURG%HzA2ML7lDr{F>TE;cA%I=o00)U2! z_qODU{5=(VEREyZS;0Kt-mz5kSV6F5`BZ;K*H=5~A5R3Yhs$czR^*HiZo+Xct4~0_ z=B34Pg6Gk+sJ1Z49oKm0L(Z%y6;e2%o;(MlEYKch>M{?lE8>df2Q!wI+#RRc{AcY~ zVt{*wZ_7ryS9y*PE={yHx^ivoomyRdu4R4hMsG*{Gxn|2B&=5Ttuft%^J+MxiZ>@W z^62v#k1sFcG>aUc1WFj}5R1Qa_jIqu3$@w~$htw!b0YfT*1=1oDEI?!jlA60e-mlg z8ri*s346;D32PA7EA;r(RU;Ts9O9|318VDh0hgEz7&558~zC zF>qqN14_HBuAyNacPj|da+_>_>+H*6&c3Sf;uy@?=6!-9$XQekvuwY-p zI1bT1K{x!Cf9&weEmU zAb~QHWzIkoBx5T8QEDP?8&M%!;2RQhRwaFp14xd?S2$bC^yKaOA|66&R@1c&v0Gx6 zg=O4uI>yvW`l-Kk?p*k%q(T-B0q&NbTSzojn$n`Sp(Pbk6Mx#FenP92b{e(Tg=4By&uZ|+4WVma@Jl;h`MwS<*b+%DIm}*vhp$O)! zg?aZm_~J5l+K)R1n3j%`?!NgLe=ZEMzK#uGBZy3z+0J5PIyuiu5Eqb-VJ%cb+a>p5 zC@-|-){_doUB%=u&wnyie(0_2-pzJ)z{yB#;sR$7XIr;mt)dlY+}|sb;uTx9uv*W2 z_&oTBkRF=2MByL?x-S%dEoPhyJM$HQ;7gs7J;U>XedW^FvkzCMqjez~WlZA*4krph zIIDBcS<56r%~eidZ&*iL@DJs!-!Z{&WRXpqWo+&FLXWcdg2m zg^hfhN45GPEhELOa+y(5k!JumL3pDvHMu=fNm<)ZtX`z2ukt=;l|q}823VIzpEZ!N zX|PYdH_S8AGI_AeqRQ#_BySwqvHFUs3=sqft<7=m$bGjlUT%*~s=bjr%cPbuulI69 z_iwn?g4)?-sp8L167!Vn%dN(f$qQ}`C*npUa&8=NZ=^hIgCy~oqTu0^Xk;0-H&>)= zb>&ym`1trE3D#7kbfMn;!onJth3UbtXoiHSG(d(g0G%8(AtiJ(AI|9$w7u7Kw@F!R zS2l^72eh*$OL4?$ zKQJM`fx4%MJP)?;@MB4Y66Uf2L@q-yTR5D20+2c&Ar+?DZ%>Pc`%>3T-H$+5zX zK{GjH5S2y^$g=7d)PU459Mn4X8S{}RXTvT)?mr!GqmI{s(=!tQTcHHRtB}HeL*DB{ zH=Gz9>ke#3uVqB61cL&NF+|4g>oTgq6uJ>-VA+DVm#o6a{F4QH2ELlSoDabcd0i{N?kQ8Ub5ddYW%su z1gHn~OePn9dLOOp>w3M3{!s(UUTS(z*YVZn_`G<|fc1f!6S71dZL#?1u^^v!Bt3r= zy-&Y=BJpEpC(!&~jI#@U^`mXH%&H5s&6S}AyHg3E$){OkPDmpu(QWC1B^}>%8PW!O zV{V&?37-{~xmy-;2Wgnyu+ywMY^}AxEh6|x|xKwmRbU5Iwpx?>> z547B%K26~&P~m#!tR?9uy45WJYyhuHu+^z>L5MfBV9?=Kc$G7Jv{)oT0!DNzpasJ3 zpeTa94?S?CB5?&Q<3xsb)85IDlnB2ST9M{aZPnC2roNijxC)Up;6RyssqIOI9UHqs zQ5WdU^7b+<-sAnW;5}L`0TfB0THkjixS?#(%=YPpS&8U}BB^LdI5z_V{Si5}>ioL5 zL1>$x)X5(fjE~}=^?~L)t~u5h@ulv8kvklqLgTV2H9XmD=c;tj=DVS8$dKN8niodI z@{BFR|Lc73t4%a~1g1|n3Au&P3;G@#&3vaQoLA;#;^Cfb*dt1X71{jA4lDENXc=ho Y!+B0Bvt)(!NC^BpY-C0*GH|){KNjM@{{R30 literal 0 HcmV?d00001 diff --git a/assets/programm_logo.svg b/assets/programm_logo.svg new file mode 100644 index 0000000..f16f814 --- /dev/null +++ b/assets/programm_logo.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauen.py b/bauen.py new file mode 100644 index 0000000..2220498 --- /dev/null +++ b/bauen.py @@ -0,0 +1,109 @@ +# -*- 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"): + shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True) + + 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") + + print() + print(f"FERTIG | {stamm} {version.VERSION}") + for name in sorted(os.listdir(os.path.join(HIER, "dist"))): + p = os.path.join(HIER, "dist", name) + print(f" {name:44s} {os.path.getsize(p):>10,d} B {summe(p)}") + setup = os.path.join(HIER, "setup") + for name in sorted(os.listdir(setup)) if os.path.isdir(setup) else []: + p = os.path.join(setup, name) + print(f" {name:44s} {os.path.getsize(p):>10,d} B {summe(p)}") + + +if __name__ == "__main__": + main() diff --git a/pdf_renderer.py b/pdf_renderer.py index 9797acc..a72f2a7 100644 --- a/pdf_renderer.py +++ b/pdf_renderer.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -PDF-Renderer fuer die Campinghof-Bartl-Rechnung (reportlab). +PDF-Renderer fuer die Rechnung (reportlab). render_rechnung(pfad, cfg, rechnung, kopf) erzeugt ein druckfertiges A4-PDF im Layout der Excel-Vorlage: diff --git a/pruef_hinweis.py b/pruef_hinweis.py index 6558969..09eb11c 100644 --- a/pruef_hinweis.py +++ b/pruef_hinweis.py @@ -22,6 +22,7 @@ Geprueft wird die ganze Strecke, weil jeder Teil fuer sich wertlos waere: """ import json import os +import time import shutil import sys import tempfile @@ -89,6 +90,25 @@ messagebox.showwarning = lambda *a, **k: None messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "") +def pumpen(fenster, bedingung, sekunden=6): + """Ereignisschleife drehen, bis die Bedingung stimmt. + + Der Hinweis wartet, bis das Fenster gezeichnet ist, und meldet sich per + after(). Ein einzelnes update() erreicht das nicht - hier wird die Schleife + gedreht wie mainloop() es taete. Abgekuerzt wird nichts. + """ + ende = time.time() + sekunden + while time.time() < ende: + try: + fenster.update() + except Exception: # noqa: BLE001 - Fenster kann zwischendurch weg sein + return bedingung() + if bedingung(): + return True + time.sleep(0.02) + return bedingung() + + def gelesen(): return json.load(open(config.CONFIG_PATH, encoding="utf-8")) @@ -96,7 +116,7 @@ def gelesen(): # ---- 1./2. Ablehnen: Fenster zu, nichts vermerkt ----------------------- antworten["Erprobungsfassung"] = False f = A.RechnungsApp() -f.update() # laesst das after(0) laufen -> Hinweis kommt +pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen)) treffer = [t for t in gesehen if "Erprobungsfassung" in t[0]] pruefe(bool(treffer), "der Hinweis kommt beim Start") @@ -129,7 +149,7 @@ pruefe(not cfg.get("hinweis_zeit"), "nach Nein steht nichts in der Konfiguration gesehen.clear() antworten["Erprobungsfassung"] = True f = A.RechnungsApp() -f.update() +pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen)) pruefe(any("Erprobungsfassung" in t[0] for t in gesehen), "beim naechsten Start wird wieder gefragt") @@ -147,7 +167,7 @@ pruefe("gelesen und zur kenntnis genommen" in sig.lower(), gesehen.clear() f.destroy() f = A.RechnungsApp() -f.update() +pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen), sekunden=2) pruefe(not any("Erprobungsfassung" in t[0] for t in gesehen), "nach dem Ja wird nicht noch einmal gefragt") @@ -156,7 +176,7 @@ gesehen.clear() f.destroy() H.FASSUNG = "9999-12-31" # so, als waere der Text geaendert worden f = A.RechnungsApp() -f.update() +pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen)) pruefe(any("Erprobungsfassung" in t[0] for t in gesehen), "bei geaenderter Fassung wird erneut gefragt") diff --git a/pruef_selbstbezug.py b/pruef_selbstbezug.py new file mode 100644 index 0000000..d8a6e84 --- /dev/null +++ b/pruef_selbstbezug.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +"""Pruefstand: `self` darf nur dort stehen, wo es auch eines gibt. + +DER FALL, DER DAZU GEFUEHRT HAT (06.09.2026) +-------------------------------------------- +Nach dem Installieren meldete das Programm beim Start: + + File "app.py", line 1824, in + NameError: name 'self' is not defined + +Im `if __name__ == "__main__"`-Block stand `self.melden_fehler(...)`. Dort gibt +es kein `self` - das Fenster wird ja gerade NICHT gebaut. Beim Umbau auf die +eigenen Meldungsfenster war die Zeile mitgezogen worden. + +WARUM DAS KEINE SYNTAXPRUEFUNG FINDET +------------------------------------- +`ast.parse` und `py_compile` pruefen die FORM, nicht die Bedeutung. Ein +undefinierter Name faellt erst auf, wenn die Zeile ausgefuehrt wird - und diese +hier laeuft nur, wenn ein ZWEITES Fenster gestartet wird und sich das erste +nicht nach vorn holen laesst. Also fast nie. Genau deshalb ist sie durch alle +Pruefstaende gekommen. + +WAS HIER GEPRUEFT WIRD +---------------------- +Jede Verwendung des Namens `self` muss in einer Funktion stehen, die `self` auch +als ersten Parameter hat. Alles andere ist ein Ueberbleibsel aus einer Methode. + +Geprueft werden beide Programme - der Fehler war in einem, haette aber genauso +im anderen stecken koennen. +""" +import ast +import io +import os +import sys + +ORDNER = [r"C:\claude\rechnungstool", r"C:\claude\beherbergungssteuer"] + +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 fundstellen(quelltext): + """Alle Zeilen, in denen `self` steht, ohne dass es eines gibt.""" + baum = ast.parse(quelltext) + treffer = [] + + def erster_parameter(knoten): + args = knoten.args + alle = list(args.posonlyargs) + list(args.args) + return alle[0].arg if alle else None + + def durchgehen(knoten, hat_self): + for kind in ast.iter_child_nodes(knoten): + if isinstance(kind, (ast.FunctionDef, ast.AsyncFunctionDef)): + # Eine verschachtelte Funktion in einer Methode darf `self` aus + # der umgebenden Methode benutzen - das ist ein Abschluss und + # voellig richtig. Verfuegbar ist es also, sobald IRGENDEINE + # umgebende Funktion es als ersten Parameter hat. + durchgehen(kind, hat_self or erster_parameter(kind) == "self") + elif isinstance(kind, ast.Lambda): + durchgehen(kind, hat_self) + else: + if isinstance(kind, ast.Name) and kind.id == "self" and not hat_self: + treffer.append(kind.lineno) + durchgehen(kind, hat_self) + + durchgehen(baum, False) + return sorted(set(treffer)) + + +# ---- 1. Die Pruefung muss den bekannten Fehler FINDEN ------------------- +# Sonst prueft sie nichts und wiegt uns in Sicherheit. +kaputt = ( + "class A:\n" + " def m(self):\n" + " self.x = 1\n" + "\n" + 'if __name__ == "__main__":\n' + " self.melden_fehler('geht nicht')\n" +) +gefunden = fundstellen(kaputt) +pruefe(gefunden == [6], + f"die Pruefung findet den bekannten Fehler in Zeile 6 (gefunden: {gefunden})") + +sauber = ( + "class A:\n" + " def m(self):\n" + " self.x = 1\n" + " f = lambda: self.x\n" + " return f\n" +) +pruefe(fundstellen(sauber) == [], + "und meldet sauberen Code nicht faelschlich") + +# ---- 2. Beide Programme durchgehen ------------------------------------- +for ordner in ORDNER: + for name in sorted(os.listdir(ordner)): + if not name.endswith(".py"): + continue + pfad = os.path.join(ordner, name) + try: + quelle = io.open(pfad, encoding="utf-8").read() + stellen = fundstellen(quelle) + except (OSError, SyntaxError) as e: + pruefe(False, f"{name} liess sich nicht lesen ({e})") + continue + if stellen: + pruefe(False, f"{os.path.basename(ordner)}/{name}: `self` ohne self " + f"in Zeile(n) {stellen}") + +pruefe(True, f"{len(ORDNER)} Programme durchgegangen") + +if fehler: + print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") + sys.exit(1) +print("GRUEN | Nirgends steht `self`, wo es keines gibt") diff --git a/pruef_start_exe.py b/pruef_start_exe.py new file mode 100644 index 0000000..04f02fb --- /dev/null +++ b/pruef_start_exe.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +"""Pruefstand: kommt bei der GEBAUTEN EXE wirklich ein Fenster? + +DER FALL, DER DAZU GEFUEHRT HAT (06.09.2026) +-------------------------------------------- +Nach dem Installieren lief das Programm - im Taskmanager stand es da, es +verbrauchte Rechenzeit - und auf dem Bildschirm war NICHTS. Ursache: der +Erprobungshinweis ging auf, bevor das Hauptfenster gezeichnet war. Der modale +Dialog sperrte das Hauptfenster, und beide blieben unsichtbar. + +WARUM DAS KEIN ANDERER PRUEFSTAND FINDET +---------------------------------------- +Alle uebrigen Pruefstaende fahren das Programm aus der QUELLE. Dort ist das +Fenster sofort gezeichnet, der Fehler tritt nicht auf. Er gehoert zur gepackten +EXE - also muss genau die gestartet werden. + +Und "der Prozess laeuft" beweist nichts: er lief ja. Geprueft wird deshalb, ob +Windows ein Fenster dieses Programms als SICHTBAR fuehrt. + +ZWEI FALLEN, die hier schon zugeschnappt sind: + * Eine PyInstaller-Onefile-EXE ist zweistufig. Der gestartete Prozess packt + nur aus; das Fenster gehoert dem KIND. Wer nur die eigene PID betrachtet, + sieht nie ein Fenster. + * terminate() beendet nur den Auspacker. Das Kind laeuft weiter - und dessen + Einzelinstanz-Sperre haelt danach jeden weiteren Start ab. Es muss die + ganze Sippe beendet werden. +""" +import ctypes +import os +import subprocess +import sys +import time +from ctypes import wintypes + +HIER = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HIER) + +import version # noqa: E402 + +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) + + +stamm, endung = os.path.splitext(version.EXE_NAME) +EXE = os.path.join(HIER, "dist", f"{stamm}-{version.VERSION}{endung}") + +if not os.path.exists(EXE): + print(f"UEBERSPRUNGEN | {os.path.basename(EXE)} ist nicht gebaut " + f"(vorher: python bauen.py)") + sys.exit(0) + +u = ctypes.windll.user32 +p = subprocess.Popen([EXE]) +print(f" (gestartet: {os.path.basename(EXE)}, PID {p.pid})") + +RUECKRUF = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) +gefunden = [] +verwandte = {p.pid} + + +def sippe(): + alle = {p.pid} + try: + e = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_Process | Where-Object " + f"{{ $_.ParentProcessId -eq {p.pid} }} | ForEach-Object {{ $_.ProcessId }}"], + capture_output=True, text=True, timeout=20) + for zeile in e.stdout.split(): + if zeile.strip().isdigit(): + alle.add(int(zeile.strip())) + except Exception: # noqa: BLE001 - dann bleibt es bei der eigenen PID + pass + return alle + + +def je_fenster(hwnd, _): + pid = wintypes.DWORD() + u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) + if pid.value not in verwandte: + return True + laenge = u.GetWindowTextLengthW(hwnd) + puffer = ctypes.create_unicode_buffer(laenge + 1) + u.GetWindowTextW(hwnd, puffer, laenge + 1) + r = wintypes.RECT() + u.GetWindowRect(hwnd, ctypes.byref(r)) + gefunden.append((puffer.value, bool(u.IsWindowVisible(hwnd)), + r.right - r.left, r.bottom - r.top)) + return True + + +ende = time.time() + 60 +sichtbar = False +vorzeitig_weg = False +while time.time() < ende and not sichtbar: + if p.poll() is not None: + vorzeitig_weg = True + break + verwandte = sippe() + gefunden.clear() + u.EnumWindows(RUECKRUF(je_fenster), 0) + # Ein "Fenster" von wenigen Pixeln ist Verwaltung (IME, Notifier), kein + # Programmfenster. Deshalb die Groesse mitpruefen. + sichtbar = any(s and b > 300 and h > 300 for _, s, b, h in gefunden) + time.sleep(0.5) + +for titel, s, b, h in gefunden: + print(f" {titel!r:42s} sichtbar={s} {b}x{h}") + +pruefe(not vorzeitig_weg, + f"das Programm bleibt am Leben (Rueckgabe {p.returncode})" + if vorzeitig_weg else "das Programm bleibt am Leben") +if vorzeitig_weg and p.returncode == 0: + print(" (Rueckgabe 0 heisst meist: es laeuft schon eine Fassung. " + "Erst beenden, dann noch einmal.)") + +pruefe(sichtbar, "ein sichtbares Programmfenster ist da") +pruefe(any(str(t).startswith(stamm) and s for t, s, _, _ in gefunden), + f"und es traegt den Programmnamen ({stamm})") + +subprocess.run(["taskkill", "/PID", str(p.pid), "/T", "/F"], capture_output=True) +try: + p.wait(10) +except subprocess.TimeoutExpired: + p.kill() + +if fehler: + print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") + sys.exit(1) +print("GRUEN | Die gebaute EXE zeigt ein Fenster") diff --git a/pruef_updater.py b/pruef_updater.py index e1bebef..227bc3c 100644 --- a/pruef_updater.py +++ b/pruef_updater.py @@ -196,6 +196,31 @@ pruefe(os.path.basename(start) == "Rechnungstool-1.9.0.exe", pruefe(not any("Wird geladen" in m for m in meldungen), f"und nichts noch einmal geladen ({meldungen})") +# 8. Ordner nicht beschreibbar -> kein Update, aber das Programm startet +# Sein Einwand: "was ist, wenn ein user den ordner aendert?" Liegt die +# Installation dann in einem geschuetzten Ordner, darf das Update scheitern - +# aber NIE der Start. Ein fehlgeschlagenes Update, nach dem gar nichts mehr +# geht, waere schlimmer als kein Update. +meldungen.clear() +gesperrt = os.path.join(tmp, "gesperrt") +os.makedirs(gesperrt) +with open(os.path.join(gesperrt, "Rechnungstool-1.6.0.exe"), "wb") as f: + f.write(b"INSTALLIERT") + +echt_beschreibbar = U.beschreibbar +U.beschreibbar = lambda _o: False # so, als waere es "Programme" +try: + start = U.durchlauf(gesperrt, "x/y", EXE, sagen=sagen, holen_=lambda _r: [richtig]) +finally: + U.beschreibbar = echt_beschreibbar + +pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", + "geschuetzter Ordner: die vorhandene Fassung wird trotzdem gestartet") +pruefe(any("nicht geschrieben werden" in m for m in meldungen), + f"und es wird gesagt, warum ({meldungen})") +pruefe(not os.path.exists(os.path.join(gesperrt, "Rechnungstool-1.9.0.exe")), + "es wurde nichts abgelegt") + shutil.rmtree(tmp, ignore_errors=True) if fehler: diff --git a/updater.py b/updater.py index f06b06c..5db2447 100644 --- a/updater.py +++ b/updater.py @@ -129,6 +129,22 @@ def summe_aus_release(release, exe_name): # --------------------------------------------------------------------------- # Laden und pruefen # --------------------------------------------------------------------------- +def beschreibbar(ordner): + """Darf in diesen Ordner geschrieben werden? Wird ausprobiert, nicht geraten. + + Ein nicht beschreibbarer Ordner sieht von aussen genauso aus wie ein + beschreibbarer - erst der Versuch zeigt es. + """ + probe = os.path.join(ordner, ".schreibprobe") + try: + with open(probe, "w", encoding="ascii") as f: + f.write("x") + os.remove(probe) + return True + except OSError: + return False + + def sha256(pfad): h = hashlib.sha256() with open(pfad, "rb") as f: @@ -309,6 +325,16 @@ def durchlauf(ordner, repository, exe_name, sagen=print, holen_=None): sagen(f"{os.path.basename(ziel)} liegt schon da. Es wird gestartet.") return ziel + # Kann in den Installationsordner ueberhaupt geschrieben werden? Liegt die + # Installation in einem geschuetzten Ordner (etwa "Programme"), schlaegt das + # Ablegen fehl. Dann wird NICHT geladen - und vor allem: das Programm + # startet trotzdem. Ein fehlgeschlagenes Update darf nie dazu fuehren, dass + # gar nichts mehr geht. + if not beschreibbar(ordner): + sagen(f"In {ordner} darf nicht geschrieben werden - kein Update moeglich.") + sagen("Es wird die vorhandene Fassung gestartet.") + return hier_pfad + sagen(f"Neue Fassung {dort} gefunden (vorhanden: {hier}). Wird geladen ...") zwischen = tempfile.mkdtemp(prefix="update_") geladen = os.path.join(zwischen, exe_name) @@ -332,7 +358,12 @@ def durchlauf(ordner, repository, exe_name, sagen=print, holen_=None): return hier_pfad # Erst jetzt in den Installationsordner - vorher lag nichts Halbes dort. - shutil.move(geladen, ziel) + try: + shutil.move(geladen, ziel) + except OSError as e: # noqa: BLE001 + sagen(f"Die neue Fassung konnte nicht abgelegt werden ({e}).") + sagen("Es wird die vorhandene Fassung gestartet.") + return hier_pfad buch_schreiben(ordner, {"version": dort.lstrip("v"), "datei": os.path.basename(ziel), "sha256": erwartete_summe, diff --git a/version.py b/version.py index 4da2e80..acce0ab 100644 --- a/version.py +++ b/version.py @@ -10,11 +10,11 @@ und NUR hier; alles andere (Info-Dialog, Updater, Release) liest sie von hier. Regel ----- Die Nummer wird beim Ausliefern erhoeht und entspricht dem Release-Tag ohne das -"v": Tag `v1.6.0` -> VERSION "1.6.0". Wer das eine ohne das andere aendert, +"v": Tag `v1.7.0` -> VERSION "1.7.0". Wer das eine ohne das andere aendert, baut dem Updater eine Falle. """ -VERSION = "1.6.0" +VERSION = "1.7.0" # Zu welchem Repository dieses Programm gehoert - der Updater sucht dort. REPOSITORY = "TheMockTv/rechnungstool" diff --git a/wasserzeichen.py b/wasserzeichen.py new file mode 100644 index 0000000..935ba0a --- /dev/null +++ b/wasserzeichen.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""Das Programmlogo - als Fenstersymbol und als blasses Wasserzeichen. + +WOHER DAS BILD KOMMT +-------------------- +Die Quelle ist die SVG aus dem Entwurf (`assets/programm_logo.svg`). Sie wird +BEIM BAUEN einmal in 512 Pixel gerendert (`assets/programm_logo.png`) und in +eine `programm.ico` fuer Windows umgesetzt. Hier zur Laufzeit wird nur noch +skaliert. + +Warum nicht direkt aus der SVG zeichnen? Weil dafuer `svglib` und `lxml` mit in +die EXE muessten - und bis 512 Pixel ist das Ergebnis pixelgenau dasselbe. Wird +das Wasserzeichen je groesser gebraucht, gehoert das umgestellt. + +Warum ueberhaupt eine ICO? Weil Windows das Symbol einer EXE als Ressource in +der Datei erwartet, und dieses Format ist ICO. Das gilt fuer jede Sprache, nicht +nur fuer Python - eine SVG kann Windows dort nicht verwenden. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber. +""" + +import os +import sys +import tkinter as tk + +_lager = {} # (fenster, groesse, deckkraft) -> PhotoImage + + +def _ordner(): + """Alle Orte, an denen die Bilder liegen koennen - Quelle wie EXE.""" + hier = os.path.dirname(os.path.abspath(__file__)) + aus_exe = getattr(sys, "_MEIPASS", "") + neben_exe = os.path.dirname(os.path.abspath(sys.executable)) \ + if getattr(sys, "frozen", False) else "" + return [p for p in (aus_exe, neben_exe, hier) if p] + + +def pfad(name): + """Voller Pfad zu einer Datei in assets/ - oder ''.""" + for basis in _ordner(): + p = os.path.join(basis, "assets", name) + if os.path.exists(p): + return p + return "" + + +def symbol_setzen(fenster, melden=print): + """Das Programmsymbol auf ein Fenster legen. True bei Erfolg. + + Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das + steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes + hat. + """ + p = pfad("programm.ico") + if not p: + melden("[symbol] assets/programm.ico nicht gefunden") + return False + try: + fenster.iconbitmap(default=p) + return True + except Exception as e: # noqa: BLE001 - ohne Symbol laeuft es weiter + melden(f"[symbol] konnte nicht gesetzt werden: {e}") + return False + + +def hole(groesse=240, deckkraft=0.06, master=None, melden=print): + """Das Logo blass als PhotoImage - oder None. + + `deckkraft` 0..1, klein halten. Ein Wasserzeichen, das man LIEST, stoert + beim Arbeiten; eines, das man ahnt, gehoert zum Programm. + + `master` MUSS mitgegeben werden, wenn mehr als ein Tk-Fenster laeuft: + sonst gehoert das Bild dem falschen Fenster und Tk meldet spaeter, es + kenne es nicht. + """ + fenster = master if master is not None else tk._default_root + if fenster is None: + return None + schluessel = (id(fenster), groesse, round(deckkraft, 3)) + if schluessel in _lager: + return _lager[schluessel] + p = pfad("programm_logo.png") + if not p: + melden("[wasserzeichen] assets/programm_logo.png nicht gefunden") + return None + try: + from PIL import Image, ImageTk + bild = Image.open(p).convert("RGBA").resize((groesse, groesse), Image.LANCZOS) + bild.putalpha(bild.getchannel("A").point(lambda w: int(w * deckkraft))) + fertig = ImageTk.PhotoImage(bild, master=fenster) + except Exception as e: # noqa: BLE001 - ein Wasserzeichen darf nichts blockieren + melden(f"[wasserzeichen] nicht ladbar: {e}") + return None + _lager[schluessel] = fertig + return fertig + + +def kopf(hoehe=28, master=None, melden=print): + """Das Logo fuer die Kopfzeile - volle Deckkraft, kleine Hoehe. + + Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein + Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon + einen Schalter, den man beim Lesen nachschlagen muss. + """ + return hole(hoehe, 1.0, master=master, melden=melden)