#!/usr/bin/env python3 """Findet Überträge, deren Gegenseite zusätzlich als eigene Buchung dasteht. Ein Kontoübertrag bewegt Geld in EINER Buchung. Liefert die Bank dieselbe Bewegung zusätzlich als Gutschrift oder Belastung, steht das Geld auf dem betroffenen Konto zweimal. Genau das ist passiert, solange Certain beim Anlegen eines Übertrags nicht nachgesehen hat, ob die Gegenseite schon gebucht ist. Das Skript ÄNDERT NICHTS. Es öffnet die Datenbank nur lesend und sagt, was zu prüfen ist – aufräumen tust du von Hand, damit nichts Richtiges mit weggeräumt wird. Aufruf: pruefe_doppelte_uebertraege.py [pfad-zur-db] """ import sqlite3 import sys from datetime import date, timedelta # So viele Tage darf die Gegenbuchung abweichen. Banken buchen eine Bewegung # oft ein bis zwei Tage nach dem Auslösen – dieselbe Spanne wie beim Import. TAGE = 3 # Über Währungen hinweg ist der erwartete Betrag gerechnet, der echte kommt # von der Bank. Ohne Spielraum fände man diese Fälle nie. FX_SPIELRAUM = 0.025 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 def geld(cents, waehrung=""): return f"{cents / 100:,.2f}".replace(",", "'") + (f" {waehrung}" if waehrung else "") konten = {r["id"]: r for r in con.execute("SELECT id, name, currency FROM accounts")} verdacht = [] for u in con.execute(""" SELECT id, date, account_id, to_account_id, amount_cents, to_amount_cents, payee, description FROM transactions WHERE type = 'transfer' ORDER BY date, id """): tag = date.fromisoformat(u["date"]) von, bis = tag - timedelta(days=TAGE), tag + timedelta(days=TAGE) # Beide Seiten prüfen: Die überzählige Buchung kann auf dem Abgangs- wie # auf dem Zugangskonto liegen, je nachdem, welche Seite umgewidmet wurde. for konto_id, betrag, richtung in ( (u["account_id"], u["amount_cents"], "expense"), (u["to_account_id"], u["to_amount_cents"] or u["amount_cents"], "income"), ): if konto_id is None or not betrag: continue gleiche_waehrung = (konten.get(u["account_id"], {})["currency"] == konten.get(u["to_account_id"], {})["currency"] if u["to_account_id"] in konten and u["account_id"] in konten else True) spielraum = 0 if gleiche_waehrung else max(1, round(betrag * FX_SPIELRAUM)) for k in con.execute(""" SELECT id, date, amount_cents, payee, description, category_id FROM transactions WHERE account_id = ? AND type = ? AND date BETWEEN ? AND ? AND id != ? """, (konto_id, richtung, von.isoformat(), bis.isoformat(), u["id"])): if abs(k["amount_cents"] - betrag) <= spielraum: verdacht.append((u, konto_id, betrag, k)) if not verdacht: print("Nichts gefunden – kein Übertrag hat eine zusätzlich gebuchte Gegenseite.") sys.exit(0) print(f"{len(verdacht)} Stelle(n) zu prüfen.\n") summe = {} for u, konto_id, betrag, k in verdacht: konto = konten.get(konto_id, {"name": "?", "currency": ""}) a = konten.get(u["account_id"], {"name": "?"}) b = konten.get(u["to_account_id"], {"name": "?"}) print(f"Übertrag #{u['id']} {u['date']} {a['name']} → {b['name']} " f"{geld(u['amount_cents'])}") print(f" {u['payee'] or u['description'] or 'ohne Text'}") print(f" daneben auf '{konto['name']}': Buchung #{k['id']} {k['date']} " f"{geld(k['amount_cents'], konto['currency'])}") print(f" {k['payee'] or k['description'] or 'ohne Text'}") print(f" → dasselbe Geld zweimal auf '{konto['name']}'\n") summe[konto_id] = summe.get(konto_id, 0) + k["amount_cents"] print("Zu viel auf den Konten, wenn sich der Verdacht bestätigt:") for konto_id, cents in sorted(summe.items()): konto = konten.get(konto_id, {"name": "?", "currency": ""}) print(f" {konto['name']:<28} {geld(cents, konto['currency'])}") print(""" So räumst du auf – je Paar EINE der beiden Zeilen, nicht beide: Beide Seiten kommen von der Bank? Dann sind BEIDE Buchungen richtig, und falsch ist nur der Übertrag. Lösch ihn und lass die zwei Buchungen von Certain verbinden ("Zu erledigen" → Mögliche Überträge). Die Belastung, aus der der Übertrag entstand, holst du mit einem erneuten Abruf des Kontos zurück. Das Gegenkonto holt Certain NICHT selbst ab? Dann ist der Übertrag richtig und die zusätzliche Buchung überzählig – die kann weg. Im Zweifel: Kontenseite ansehen. Steht dort "Saldo stimmt mit der Bank überein", ist das Konto in Ordnung, egal was hier steht.""")