fix: Bankabruf verschluckte Umsaetze mit wiederkehrendem Betrag

Der Eingangs-Abgleich vergleicht Betrag und Datum mit ein paar Tagen
Spielraum – bewusst unscharf, damit eine per Kurzbefehl erfasste Zahlung nicht
ein zweites Mal ankommt. Er merkt sich dabei, welchen Eintrag er schon
verbraucht hat.

Nur ruft der Bankabruf ihn JE ZEILE einzeln auf, und damit begann diese Menge
jedes Mal von vorn: Derselbe Eintrag liess sich beliebig oft verbrauchen. Bei
Revolut-Aufrundungen – dreimal 1.00 in drei Tagen – kam so nur die erste an,
und der Saldo wich ab, ohne dass irgendwo etwas stand.

Zwei Aenderungen:

- Der Aufrufer kann die Menge jetzt ueber mehrere Aufrufe mitfuehren; der
  Bankabruf haelt eine je Konto.
- Gegen andere BANK-Eintraege wird gar nicht mehr unscharf verglichen. Zwei
  Umsaetze derselben Quelle sind nie Dubletten voneinander – dort entscheidet
  die Kennung der Bank, und die ist exakt.

Dazu ein Weg zurueck: Ein verschluckter Umsatz hinterliess keinen Eintrag, der
naechste Abruf haette ihn also wieder geholt – nur liegt er dann vor dem
Fenster des letzten Abrufs. "Aelteres nachholen" gibt es jetzt auch je Konto,
wie beim Postfach.

Nachgestellt und als Test festgehalten: dreimal 1.00 in drei Tagen, alle drei
kommen an, Saldo -3.00.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 16:45:38 +02:00
co-authored by Claude Opus 5
parent f2c1ab7dec
commit b4c90f5221
8 changed files with 131 additions and 26 deletions
+21 -7
View File
@@ -485,7 +485,7 @@ def _ist_gebucht(umsatz: dict) -> bool | None:
def _startdatum(db: Session, user: User, konto: Account,
verknuepfung: BankAccountLink) -> date:
verknuepfung: BankAccountLink, tage: int | None = None) -> date:
"""Ab welchem Tag Umsaetze DIESES Kontos geholt werden.
Beim ERSTEN Abruf ab dem Tag nach der neuesten vorhandenen Buchung des
@@ -501,6 +501,10 @@ def _startdatum(db: Session, user: User, konto: Account,
ein Einzelabruf das Startdatum der uebrigen Konten mit vor, und deren
Umsaetze aus dem uebersprungenen Zeitraum kaemen nie an.
"""
if tage is not None:
# Uebergeht den letzten Abruf. Noetig, wenn etwas nicht angekommen ist:
# Sonst laege es fuer immer vor dem Fenster, obwohl die Bank es haette.
return date.today() - timedelta(days=tage)
if verknuepfung.last_sync_at:
return verknuepfung.last_sync_at.date() - timedelta(days=3)
frueheste = date.today() - timedelta(days=ERSTER_ABRUF_TAGE)
@@ -515,7 +519,7 @@ def _startdatum(db: Session, user: User, konto: Account,
def hole_umsaetze(db: Session, user: User, verbindung: BankConnection,
nur_link_id: int | None = None) -> dict:
nur_link_id: int | None = None, tage: int | None = None) -> dict:
"""Umsätze der zugeordneten Konten dieser Verbindung in den Eingang holen.
Mit ``nur_link_id`` nur das eine Konto – samt seinem Saldo. Ein Abruf
@@ -541,8 +545,10 @@ def hole_umsaetze(db: Session, user: User, verbindung: BankConnection,
continue
# Je Konto eigen: Das eine ist bis gestern per CSV gepflegt, das andere
# ganz neu.
seit = _startdatum(db, user, konto, verknuepfung)
seit = _startdatum(db, user, konto, verknuepfung, tage)
ab = seit if ab is None else min(ab, seit)
# Je Konto EINE Menge verbrauchter Eingangs-Eintraege.
verbraucht: set[int] = set()
for umsatz in client.umsaetze(verknuepfung.remote_uid, seit):
geholt += 1
zustand = _ist_gebucht(umsatz)
@@ -577,10 +583,18 @@ def hole_umsaetze(db: Session, user: User, verbindung: BankConnection,
# Durch dieselbe Erkennung wie CSV und Kurzbefehl: Schlagwortregeln
# schlagen die Kategorie vor, der Eingangs-Abgleich verhindert, dass
# eine bereits erfasste Zahlung ein zweites Mal auftaucht.
erkannt = csv_import.detect_rows(db, user, konto, [{
"date": tag.isoformat(), "amount_cents": cents,
"payee": payee, "purpose": zweck,
}])
erkannt = csv_import.detect_rows(
db, user, konto,
[{"date": tag.isoformat(), "amount_cents": cents,
"payee": payee, "purpose": zweck}],
# Gegen andere BANK-Umsaetze nicht unscharf vergleichen: Dafuer
# gibt es die Kennung der Bank, und die ist exakt. Sonst
# verschluckt eine Aufrundung ueber 1.00 die naechste.
eingang_ohne=InboxSource.bank,
# Und ueber alle Zeilen dieses Kontos hinweg dieselbe Menge,
# denn hier wird je Zeile einzeln aufgerufen.
verbraucht=verbraucht,
)
zeile = erkannt[0] if erkannt else {}
if zeile.get("is_duplicate"):
continue