Neue Rechnungen tauchen von selbst auf - das Journal hoert auf die gemeinsame SQLite

Stehen beide Programme offen und wird im Rechnungstool eine Rechnung
geschrieben, erscheint sie hier ohne Zutun. Das Journal traegt sich dafuer beim
Start mit seiner PID in der gemeinsamen SQLite ein (Tabelle "laeuft") und traegt
sich beim Schliessen wieder aus; das Rechnungstool sieht daran, dass jemand
zuhoert, und legt eine Zeile in die Tabelle "nachrichten".

Nachgesehen wird nur dort - erst ein os.stat auf die eine Datei, und nur wenn
sich etwas geruehrt hat, wird sie geoeffnet. Kein Absuchen des
Rechnungsordners, kein Server, kein Port. Beim Zurueckklicken ins Fenster wird
sofort nachgesehen statt zwei Sekunden zu warten.

Die Wache faengt jeden Fehler ab und stirbt nie - ein Netzlaufwerk, das kurz
weg ist, darf das Fenster nicht lahmlegen.

⚠ Ein Denkfehler daraus gleich behoben: der erste Blick nach dem Start merkt
sich den Stand AUCH dann, wenn es die Datei noch gar nicht gibt. Sonst galt die
erste Meldung danach als "schon gesehen" und wurde verschluckt.

Pruefstand pruef_wache.py (faehrt BEIDE Programme): 23 Pruefungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
TheMockTv 2026-09-03 21:02:24 +02:00
parent 59d2db75af
commit 31bb0c600b
3 changed files with 478 additions and 0 deletions

93
app.py
View file

@ -104,6 +104,13 @@ class App(tk.Tk):
if self.pdf_ordner and os.path.isdir(self.pdf_ordner):
self.after(200, lambda: self.einlesen(still=True))
self.after(400, self._frage_gemeinsam_falls_noetig)
# Beide Programme laufen nebeneinander: schreibt das Rechnungstool eine
# Rechnung, meldet es das hier herein - siehe _wache_jetzt().
self._briefkasten_stand = "?" # "?" = noch nicht nachgesehen
self._nachricht_stand = 0 # bis hierher sind Meldungen gelesen
self.after(600, self._wache)
# Beim Zurueckklicken sofort nachsehen statt zwei Sekunden zu warten.
self.bind("<FocusIn>", lambda _e: self._wache_jetzt())
# ------------------------------------------------------------ Aufbau ----
def _menue(self):
@ -253,6 +260,62 @@ class App(tk.Tk):
text=f"Rechnungen: {self.pdf_ordner}" if self.pdf_ordner
else "kein Rechnungs-Ordner gewählt (Datei → Rechnungs-Ordner wählen…)")
# ------------------------------------------------------------- Wache
# Das Rechnungstool MELDET, wenn es eine Rechnung geschrieben hat - es legt
# eine Zeile in den Briefkasten im gemeinsamen Ordner. Hier wird nur diese
# eine Datei angesehen (ein os.stat), nicht der ganze Rechnungsordner.
# Damit das Rechnungstool weiss, dass jemand zuhoert, traegt sich dieses
# Fenster beim Start mit seiner PID ein (siehe _anmelden).
WACHE_SEKUNDEN = 2
def _wache_jetzt(self):
"""Einmal nachsehen. True, wenn eine Meldung da war und eingelesen wurde.
Billig gehalten: erst ein os.stat auf die gemeinsame Datei. Nur wenn
sich dort etwas geruehrt hat, wird sie ueberhaupt geoeffnet.
"""
if self.buch is None:
return False
stand = gemeinsam.datei_stand(self.buch.ordner)
if self._briefkasten_stand == "?":
# Erster Blick nach dem Start: nur merken, auch wenn es die Datei
# noch gar nicht gibt - sonst gilt die erste Meldung danach als
# "schon gesehen" und wird verschluckt.
self._briefkasten_stand = stand
self._nachricht_stand = self._letzte_nachricht()
return False
if stand is None or stand == self._briefkasten_stand:
return False
self._briefkasten_stand = stand
try:
texte, neu = self.buch.nachrichten_ab(self._nachricht_stand)
except Exception as e: # noqa: BLE001
log.warning("Briefkasten nicht lesbar: %s", e)
return False
if not texte:
return False # das Buch hat sich geaendert, aber ohne Meldung
self._nachricht_stand = neu
text = texte[-1] if len(texte) == 1 else f"{len(texte)} Meldungen, zuletzt: {texte[-1]}"
log.info("Rechnungstool meldet: %s", text)
self.einlesen(still=True)
self.melde(f"Rechnungstool meldet: {text}")
return True
def _letzte_nachricht(self):
try:
return self.buch.letzte_nachricht() if self.buch else 0
except Exception: # noqa: BLE001
return 0
def _wache(self):
"""Der Wecker. Laeuft weiter, auch wenn ein Durchgang schiefgeht."""
try:
self._wache_jetzt()
except Exception as e: # noqa: BLE001 - die Wache darf nie sterben
log.warning("Wache: %s", e)
finally:
self.after(self.WACHE_SEKUNDEN * 1000, self._wache)
def suche_leeren(self):
self.suche.set("")
self.suchfeld.focus_set()
@ -262,6 +325,32 @@ class App(tk.Tk):
log.info(text)
# ---- Gemeinsames Nummernbuch -------------------------------------------
def _anmelden(self):
"""Mit der eigenen PID im gemeinsamen Ordner eintragen.
Daran sieht das Rechnungstool, dass dieses Fenster offen ist - es fragt
Windows nach der Prozessnummer, nicht nach einem Fenster.
"""
ordner = self.journal.hole("gemeinsam_ordner", "")
if not ordner:
return
if self.buch is None:
self._buch_oeffnen()
if self.buch is None:
return
try:
self.buch.anmelden("journal", os.getpid(), os.path.basename(sys.executable))
except Exception as e: # noqa: BLE001
log.warning("Anmeldung fehlgeschlagen: %s", e)
def _abmelden(self):
if self.buch is None:
return
try:
self.buch.abmelden("journal")
except Exception as e: # noqa: BLE001 - beim Schliessen nichts erzwingen
log.debug("Abmelden fehlgeschlagen: %s", e)
def _buch_oeffnen(self):
ordner = self.journal.hole("gemeinsam_ordner", "")
if not ordner:
@ -284,6 +373,7 @@ class App(tk.Tk):
self._buch_oeffnen()
if self.buch is None and not TESTLAUF:
self.dlg_gemeinsam(erststart=True)
self._anmelden()
self._buch_fuellen()
def dlg_gemeinsam(self, erststart=False):
@ -300,6 +390,7 @@ class App(tk.Tk):
return
self.journal.setze("gemeinsam_ordner", ordner)
self._buch_oeffnen()
self._anmelden()
self._buch_fuellen(melden=True)
def _buch_fuellen(self, melden=False):
@ -669,6 +760,8 @@ class App(tk.Tk):
os.startfile(pfad) # noqa: S606
def _beenden(self):
# Austragen, damit das Rechnungstool nicht weiter meldet.
self._abmelden()
self.journal.setze("jahr", self.jahr.get())
self.journal.schliessen()
self.destroy()