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 <noreply@anthropic.com>
101 lines
4.3 KiB
Python
Executable File
101 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Zeigt, welches Konto an welches überträgt – und wo dabei etwas fehlt.
|
||
|
||
Ein falscher Kontostand kommt selten von den Ausgaben, fast immer von den
|
||
Überträgen: falsche Richtung, fehlendes Zielkonto oder ein Währungswechsel
|
||
ohne Zielbetrag (dann wird der Fremdbetrag 1:1 gutgeschrieben).
|
||
|
||
Aufruf: pruefe_transfers.py [pfad-zur-db]
|
||
"""
|
||
import sqlite3
|
||
import sys
|
||
|
||
DB = sys.argv[1] if len(sys.argv) > 1 else "/var/lib/certain-finance/app.db"
|
||
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
||
con.row_factory = sqlite3.Row
|
||
|
||
konten = {r["id"]: r for r in con.execute("SELECT id, name, currency FROM accounts")}
|
||
|
||
|
||
def name(i):
|
||
return konten[i]["name"] if i in konten else f"?{i}"
|
||
|
||
|
||
def waehrung(i):
|
||
return konten[i]["currency"] if i in konten else "?"
|
||
|
||
|
||
def geld(c):
|
||
return f"{(c or 0) / 100:,.2f}".replace(",", "'")
|
||
|
||
|
||
print(f"Datenbank: {DB}\n")
|
||
print("=== Wer überträgt an wen ===")
|
||
print(f"{'von':<24}{'nach':<24}{'Anz':>5}{'Summe raus':>14}{'Summe rein':>14}")
|
||
print("-" * 81)
|
||
for r in con.execute(
|
||
"SELECT account_id a, to_account_id b, COUNT(*) n, SUM(amount_cents) raus, "
|
||
"SUM(COALESCE(to_amount_cents, amount_cents)) rein FROM transactions "
|
||
"WHERE type='transfer' AND to_account_id IS NOT NULL GROUP BY a, b ORDER BY raus DESC"
|
||
):
|
||
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 "
|
||
"WHERE type='transfer' AND to_account_id IS NULL").fetchone()
|
||
print(f"\nÜberträge OHNE Zielkonto: {offen['n']}, Summe {geld(offen['s'])}"
|
||
" <-- gehen vom Quellkonto ab und kommen nirgends an")
|
||
|
||
print("\n=== Währungswechsel ohne Zielbetrag ===")
|
||
schief = con.execute(
|
||
"SELECT t.id, t.date, t.amount_cents, COALESCE(t.payee,'') p, a.name qn, a.currency qw, "
|
||
"z.name zn, z.currency zw FROM transactions t "
|
||
"JOIN accounts a ON a.id = t.account_id JOIN accounts z ON z.id = t.to_account_id "
|
||
"WHERE t.type='transfer' AND a.currency <> z.currency AND t.to_amount_cents IS NULL "
|
||
"ORDER BY t.amount_cents DESC").fetchall()
|
||
if not schief:
|
||
print(" keine – gut.")
|
||
else:
|
||
summe = sum(r["amount_cents"] for r in schief)
|
||
print(f" {len(schief)} Stück, Summe {geld(summe)}"
|
||
" <-- der Betrag wird ohne Umrechnung gutgeschrieben")
|
||
for r in schief[:12]:
|
||
print(f" {r['date']} {geld(r['amount_cents']):>10} {r['qw']} -> {r['zw']} "
|
||
f"{r['qn'][:16]:<16} -> {r['zn'][:16]:<16} {r['p'][:26]}")
|
||
|
||
print("\n=== Die 12 grössten Überträge ===")
|
||
for r in con.execute(
|
||
"SELECT t.date, t.amount_cents, t.to_amount_cents, COALESCE(t.payee,'') p, "
|
||
"t.account_id a, t.to_account_id b FROM transactions t WHERE t.type='transfer' "
|
||
"ORDER BY t.amount_cents DESC LIMIT 12"
|
||
):
|
||
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}")
|