Mini-API zwischen beiden Programmen, Proforma-Rechnung

MINI-API (seine Ansage: "die muessen ja sehen das die beiden auf sind"):
Jedes Programm macht beim Start einen winzigen Server auf 127.0.0.1 mit einem
Port, den das Betriebssystem aussucht, und traegt Port und PID in die
gemeinsame SQLite ein - die ist das Telefonbuch. Zwei Nachrichten reichen:
"ping" wird mit "hallo, bin noch da" beantwortet, "schau_nach" ist der Push.

Der Push traegt KEINE Daten. Er sagt nur "schau in die sqlite" - dort steht die
Meldung ohnehin. Eine Wahrheit, und wenn der Anruf nicht durchgeht, findet das
andere Programm die Meldung beim naechsten Nachsehen.

Gebunden wird nur auf Loopback: von aussen ist nichts erreichbar, Windows fragt
bei 127.0.0.1 nicht nach der Firewall, und die API fuehrt keine Befehle aus.
Kein Netzwerk-Server mit MySQL - dafuer braeuchte er Infrastruktur, die es
nicht gibt; auf verschiedenen Rechnern waere ohnehin ein anderer Bau noetig.

PROFORMA-RECHNUNG: ein Knopf, gewaehlt wird im Popup - die fruehere
Sicherheitsfrage IST diese Auswahl geworden (eine Frage statt zwei).

Seine Bedingung: "musst nur auf passen das der rechner dann die rechnungs
nummer nicht aendert". Die Proforma laeuft in einer EIGENEN Reihe (2026-P001);
Nummernfeld, Rechnungszaehler, Nummernbuch und die Meldung ans Journal bleiben
unberuehrt. Genau das prueft pruef_proforma.py zuerst.

An den Quellen geprueft, nicht an Ratgeberseiten:
* § 14 Abs. 1 S. 1 UStG / UStAE 14.1 - Rechnung ist jedes Dokument, mit dem
  abgerechnet wird, "gleichgueltig, wie dieses Dokument im Geschaeftsverkehr
  bezeichnet wird". Das Wort "Proforma" allein schuetzt also nicht.
* § 14c Abs. 2 UStG - wer Steuer gesondert ausweist, ohne berechtigt zu sein,
  schuldet den Betrag; Korrektur nur mit Antrag und Zustimmung des Finanzamts.
* § 14 Abs. 4 Nr. 4 UStG - die fortlaufende, einmalig vergebene Nummer gilt
  fuer Rechnungen.

Deshalb: Ueberschrift "Proforma-Rechnung", der Satz "keine Rechnung im Sinne
des § 14 UStG ... nicht zu bezahlen", "Proforma an:" statt "Rechnung an:", die
Nummer ausdruecklich als KEINE Rechnungsnummer, KEIN Steuersatz und KEIN
Steuerbetrag (auch die Beherbergungssteuer nicht - sonst waere das Blatt halb
ausgewiesen, halb nicht), dafuer "Voraussichtlicher Gesamtbetrag" mit dem
Hinweis, dass alles enthalten ist. Kein Blatt fuers Amt, kein Zahlungstext.

TESTLAUF: _testwahl sagt, was im Auswahlfenster geklickt worden waere;
pruef_proforma setzt _dialog_zeigen und drueckt die echten Knoepfe.

Pruefstaende: pruef_proforma.py (neu, mit dem echten Popup), pruef_montage,
pruef_storno, pruef_kette, pruef_kernregeln, pruef_nummernbuch, pruef_nummern,
pruef_dialoge - alle gruen.

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:22:48 +02:00
parent 8163213960
commit 94c2c7881a
7 changed files with 703 additions and 46 deletions

View file

@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS laeuft (
programm TEXT PRIMARY KEY, -- 'journal' | 'rechnungstool'
pid INTEGER NOT NULL DEFAULT 0,
exe TEXT NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 0, -- wo seine Mini-API lauscht (127.0.0.1)
seit TEXT NOT NULL DEFAULT ''
);
@ -191,6 +192,11 @@ class Nummernbuch:
con = sqlite3.connect(self.pfad, timeout=10)
con.row_factory = sqlite3.Row
con.executescript(SCHEMA)
# Spalte nachziehen: CREATE TABLE IF NOT EXISTS aendert eine bestehende
# Tabelle nicht, und ein Buch aus der Fassung ohne API hat sie noch nicht.
spalten = {z["name"] for z in con.execute("PRAGMA table_info('laeuft')")}
if spalten and "port" not in spalten:
con.execute("ALTER TABLE laeuft ADD COLUMN port INTEGER NOT NULL DEFAULT 0")
return con
# ---- Lesen -------------------------------------------------------------
@ -248,17 +254,37 @@ class Nummernbuch:
con.execute("SELECT storno_zu FROM nummern WHERE storno_zu <> ''")}
# ---- Wer laeuft, und der Briefkasten -----------------------------------
def anmelden(self, programm, pid, exe):
"""Eintragen: dieses Programm ist offen."""
def anmelden(self, programm, pid, exe, port=0):
"""Eintragen: dieses Programm ist offen - und wo seine API lauscht.
Die Tabelle ist das Telefonbuch der beiden Programme: PID (laeuft es
ueberhaupt?) und Port (wo erreiche ich es?).
"""
import datetime
with self._verbindung() as con:
con.execute(
"INSERT INTO laeuft (programm, pid, exe, seit) VALUES (?,?,?,?) "
"INSERT INTO laeuft (programm, pid, exe, port, seit) VALUES (?,?,?,?,?) "
"ON CONFLICT(programm) DO UPDATE SET pid=excluded.pid, exe=excluded.exe, "
"seit=excluded.seit",
(str(programm), int(pid), str(exe),
"port=excluded.port, seit=excluded.seit",
(str(programm), int(pid), str(exe), int(port or 0),
datetime.datetime.now().isoformat(timespec="seconds")))
log.info("angemeldet: %s (PID %s)", programm, pid)
log.info("angemeldet: %s (PID %s, Port %s)", programm, pid, port)
def wer_laeuft(self, programm):
"""Telefonbuch-Eintrag eines laufenden Programms - oder None.
Geprueft wird die PID (Windows antwortet), nicht ein Zeitstempel. Eine
Karteileiche wird gleich weggeraeumt.
"""
with self._verbindung() as con:
z = con.execute("SELECT programm, pid, exe, port FROM laeuft WHERE programm=?",
(str(programm),)).fetchone()
if not z:
return None
if not prozess_laeuft(z["pid"], z["exe"]):
self.abmelden(programm)
return None
return dict(z)
def abmelden(self, programm):
with self._verbindung() as con:
@ -266,15 +292,7 @@ class Nummernbuch:
def laeuft(self, programm):
"""Laeuft dieses Programm noch? Karteileichen werden weggeraeumt."""
with self._verbindung() as con:
z = con.execute("SELECT pid, exe FROM laeuft WHERE programm=?",
(str(programm),)).fetchone()
if not z:
return False
if prozess_laeuft(z["pid"], z["exe"]):
return True
self.abmelden(programm)
return False
return self.wer_laeuft(programm) is not None
def melden(self, absender, text):
"""Eine Zeile in den Briefkasten legen."""