Files
GuidoandClaude Opus 5 d88efebfeb feat: Diagnose zeigt Stichtag und Beispieltexte je Übertrag
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>
2026-08-21 15:22:59 +02:00

101 lines
4.3 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}")