fix: Gegenbuchung nimmt den besten statt des erstbesten Treffers

Liegen mehrere Währungstausche im selben Zeitfenster, schnappte der zuerst
geprüfte Transfer der eigentlich gemeinten Zeile das Gegenstück weg – diese blieb
dann ohne Gegenkonto stehen. Jetzt gewinnt der Kandidat mit der geringsten
Datums- und Betragsabweichung.

Toleranz von 10 % auf 20 % erhöht: Wir schätzen die Gegenseite mit dem EZB-Kurs,
die Bank rechnet mit ihrem eigenen – und fehlt uns der Kurs, schätzen wir 1:1.
Ein echter Tausch lag damit schnell über 10 % daneben (CHF 150.00 -> EUR 165.60
sind 10,4 %), eine fremde Buchung trifft das Zeitfenster praktisch nie.

An den echten Auszügen: offene Zeilen im Revolut-EUR-Import von 3 auf 1 gesunken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 09:56:09 +02:00
co-authored by Claude Opus 4.8
parent 7eed5ee6c4
commit 511cf1abe0
+16 -6
View File
@@ -288,7 +288,7 @@ COUNTERPART_DAYS = 5
# damit er noch als dieselbe Bewegung gilt. Bei einem Währungstausch rechnen wir # damit er noch als dieselbe Bewegung gilt. Bei einem Währungstausch rechnen wir
# mit dem EZB-Kurs, die Bank nutzt ihren eigenen – ein paar Prozent Unterschied # mit dem EZB-Kurs, die Bank nutzt ihren eigenen – ein paar Prozent Unterschied
# sind normal, eine andere Buchung liegt praktisch nie so nah. # sind normal, eine andere Buchung liegt praktisch nie so nah.
COUNTERPART_TOLERANCE = 0.10 COUNTERPART_TOLERANCE = 0.20
def _find_counterpart( def _find_counterpart(
@@ -303,10 +303,11 @@ def _find_counterpart(
Bei einem Umtausch stehen zwei Währungen und damit zwei Beträge gegenüber; Bei einem Umtausch stehen zwei Währungen und damit zwei Beträge gegenüber;
dann zählt ein plausibler Betrag statt eines exakten.""" dann zählt ein plausibler Betrag statt eines exakten."""
absamt = abs(amount) absamt = abs(amount)
unscharf = None bester = None
for eintrag in side_index.get(account.id, []): for eintrag in side_index.get(account.id, []):
txn_id, t_date, erwartet, other_id, richtung = eintrag txn_id, t_date, erwartet, other_id, richtung = eintrag
if txn_id in verbraucht or abs((t_date - d).days) > COUNTERPART_DAYS: tage = abs((t_date - d).days)
if txn_id in verbraucht or tage > COUNTERPART_DAYS:
continue continue
# Die Richtung muss stimmen: Wo Geld ankam, kann keine Ausgabe die # Die Richtung muss stimmen: Wo Geld ankam, kann keine Ausgabe die
# Gegenseite sein. Ohne diese Prüfung verschluckt eine Ausgabe über # Gegenseite sein. Ohne diese Prüfung verschluckt eine Ausgabe über
@@ -319,9 +320,18 @@ def _find_counterpart(
if absamt == erwartet: if absamt == erwartet:
return eintrag return eintrag
cross = waehrung.get(other_id, account.currency) != account.currency cross = waehrung.get(other_id, account.currency) != account.currency
if cross and erwartet and abs(absamt - erwartet) <= erwartet * COUNTERPART_TOLERANCE: if not cross or not erwartet:
unscharf = unscharf or eintrag continue
return unscharf abweichung = abs(absamt - erwartet) / erwartet
if abweichung > COUNTERPART_TOLERANCE:
continue
# Den BESTEN Treffer nehmen, nicht den ersten: Liegen mehrere Umtausche
# im Zeitfenster, schnappte sonst der frühere den passenden Transfer weg
# und die eigentlich gemeinte Zeile blieb ohne Gegenstück.
rang = (tage, abweichung)
if bester is None or rang < bester[0]:
bester = (rang, eintrag)
return bester[1] if bester else None
def parse_file_to_candidates(data: bytes, template: ImportTemplate) -> list[dict]: def parse_file_to_candidates(data: bytes, template: ImportTemplate) -> list[dict]: