From d88efebfeb3aaa9739570e0b9cd1bfc98ab4dc2f Mon Sep 17 00:00:00 2001 From: Guido Date: Fri, 21 Aug 2026 15:22:59 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Diagnose=20zeigt=20Stichtag=20und=20Bei?= =?UTF-8?q?spieltexte=20je=20=C3=9Cbertrag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Stichtag eines Kontos wird von der Saldo-Berechnung ignoriert – das Skript weist jetzt aus, wie viel aus Buchungen bis zum Stichtag stammt und trotzdem mitzählt. Die Übertrags-Matrix nennt dazu Beispieltexte, Erkennungstexte der Konten und Transfer-Schlagwörter, damit sich ein falscher Übertrag seinem Auslöser zuordnen lässt. Co-Authored-By: Claude Opus 5 --- backend/scripts/pruefe_salden.py | 22 ++++++++++++++++++++-- backend/scripts/pruefe_transfers.py | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/backend/scripts/pruefe_salden.py b/backend/scripts/pruefe_salden.py index da87059..5215da3 100755 --- a/backend/scripts/pruefe_salden.py +++ b/backend/scripts/pruefe_salden.py @@ -26,7 +26,7 @@ def geld(c, w=""): print(f"Datenbank: {DB}\n") -for a in con.execute("SELECT id, name, currency, opening_balance_cents FROM accounts " +for a in con.execute("SELECT id, name, currency, opening_balance_cents, opening_date FROM accounts " "WHERE is_archived = 0 ORDER BY position, id"): ein = eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? AND type='income'", a["id"]) aus = eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? AND type='expense'", a["id"]) @@ -38,7 +38,9 @@ for a in con.execute("SELECT id, name, currency, opening_balance_cents FROM acco spanne = con.execute("SELECT MIN(date), MAX(date) FROM transactions WHERE account_id=?", (a["id"],)).fetchone() print(f"=== {a['name']} ({a['currency']}, id {a['id']}) ===") - print(f" Anfangssaldo {geld(a['opening_balance_cents']):>14}") + stichtag = a["opening_date"] + print(f" Anfangssaldo {geld(a['opening_balance_cents']):>14}" + f" Stichtag: {stichtag or '– keiner –'}") print(f" + Einnahmen {geld(ein):>14}") print(f" - Ausgaben {geld(aus):>14}") print(f" - Transfers raus {geld(raus):>14}") @@ -46,6 +48,22 @@ for a in con.execute("SELECT id, name, currency, opening_balance_cents FROM acco print(f" = SALDO {geld(saldo, a['currency']):>14}") print(f" {anz} Buchungen, {spanne[0] or '–'} bis {spanne[1] or '–'}") + # Der Stichtag wird von der Saldo-Berechnung derzeit ignoriert: sie addiert + # ALLE Buchungen. Gemeint ist aber "an diesem Tag stand so viel drauf", also + # dürften nur spätere Buchungen zählen. Hier steht, was das ausmacht. + if stichtag: + vor = (eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? " + "AND type='income' AND date <= ?", a["id"], stichtag) + - eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? " + "AND type='expense' AND date <= ?", a["id"], stichtag) + - eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? " + "AND type='transfer' AND date <= ?", a["id"], stichtag) + + eins("SELECT SUM(COALESCE(to_amount_cents, amount_cents)) FROM transactions " + "WHERE to_account_id=? AND type='transfer' AND date <= ?", a["id"], stichtag)) + if vor: + print(f" ACHTUNG: {geld(vor)} stammt aus Buchungen BIS zum Stichtag und wird") + print(f" trotzdem mitgezählt. Ohne sie: {geld(saldo - vor, a['currency'])}") + # Doppelte Auszüge erkennen: gleicher Dateiname mehrfach gebucht. batches = con.execute( "SELECT filename, status, imported_count, created_at FROM import_batches " diff --git a/backend/scripts/pruefe_transfers.py b/backend/scripts/pruefe_transfers.py index f5929fe..9574c41 100755 --- a/backend/scripts/pruefe_transfers.py +++ b/backend/scripts/pruefe_transfers.py @@ -41,6 +41,13 @@ for r in con.execute( print(f"{name(r['a'])[:23]:<24}{name(r['b'])[:23]:<24}{r['n']:>5}" f"{geld(r['raus']) + ' ' + waehrung(r['a']):>14}" f"{geld(r['rein']) + ' ' + waehrung(r['b']):>14}") + # Beispieltexte: sie verraten, welches Schlagwort oder welcher Erkennungs- + # text den Übertrag ausgelöst hat. + for b in con.execute( + "SELECT COALESCE(payee,'') p, COALESCE(description,'') d, COUNT(*) n " + "FROM transactions WHERE type='transfer' AND account_id=? AND to_account_id=? " + "GROUP BY p, d ORDER BY n DESC LIMIT 3", (r["a"], r["b"])): + print(f" {b['n']:>3}x {(b['p'] + ' | ' + b['d']).strip(' |')[:66]}") offen = con.execute( "SELECT COUNT(*) n, SUM(amount_cents) s FROM transactions " @@ -74,3 +81,20 @@ for r in con.execute( ziel = name(r["b"]) if r["b"] else "– kein Ziel –" print(f" {r['date']} {geld(r['amount_cents']):>10} {waehrung(r['a'])} " f"{name(r['a'])[:18]:<18} -> {ziel[:18]:<18} {r['p'][:30]}") + + +print("\n=== Erkennungstexte der Konten ===") +print("Ein kurzer Text trifft zu viel: er wird als Teilstring gesucht.") +for r in con.execute( + "SELECT a.name, i.kind, i.value FROM account_identifiers i " + "JOIN accounts a ON a.id = i.account_id ORDER BY LENGTH(i.value), a.name"): + warn = " <-- sehr kurz, trifft leicht Fremdes" if len(r["value"]) < 8 else "" + print(f" {r['name'][:24]:<26}{r['kind']:<10}{r['value']}{warn}") + +print("\n=== Transfer-Schlagwörter ===") +for r in con.execute( + "SELECT r.pattern, r.target_account_id t FROM categorization_rules r " + "WHERE r.is_transfer = 1 ORDER BY LENGTH(r.pattern)"): + ziel = name(r["t"]) if r["t"] else "– Ziel wird geraten –" + warn = " <-- sehr kurz" if len(r["pattern"]) < 8 else "" + print(f" {r['pattern'][:34]:<36}-> {ziel}{warn}")