Files
certain/backend/scripts/pruefe_doppelte_uebertraege.py
T
GuidoandClaude Opus 5 0cd0aab90d feat: Doppelt gebuchte Uebertraege auffindbar machen
Solange beim Anlegen eines Uebertrags nicht geprueft wurde, ob die Gegenseite
schon gebucht ist, konnte dieselbe Bewegung zweimal auf einem Konto landen.
Das ist seit dem vorigen Commit unterbunden – die Daten von vorher bleiben
aber, wie sie sind.

Das Skript findet sie: Zu jedem Uebertrag sieht es auf BEIDEN Konten nach, ob
dort zusaetzlich eine passende Buchung steht (gleiche Tage, passender Betrag,
ueber Waehrungen hinweg mit Spielraum), und rechnet zusammen, wieviel dann zu
viel dasteht.

Es aendert NICHTS und oeffnet die Datenbank nur lesend. Aufraeumen bleibt Sache
des Nutzers, denn welche der beiden Zeilen falsch ist, haengt daran, ob Certain
das Gegenkonto selbst abholt – das kann das Skript nicht wissen, und ein
Aufraeumskript hat hier schon einmal Richtiges mit weggeraeumt. Beide Faelle
stehen als Anleitung dabei.

Dazu ein Regressionstest fuer genau den gemeldeten Fall: Gutschrift steht schon
auf dem Zielkonto, die Belastung soll zum Uebertrag werden – das wird jetzt
abgefangen, und der Saldo des Zielkontos bleibt unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:28:12 +02:00

108 lines
4.7 KiB
Python
Executable File
Raw 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
"""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.""")