From 511cf1abe00c6ac1d6722f9469ad3f162f09daab Mon Sep 17 00:00:00 2001 From: Guido Date: Fri, 21 Aug 2026 09:56:09 +0200 Subject: [PATCH] fix: Gegenbuchung nimmt den besten statt des erstbesten Treffers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/services/csv_import.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/app/services/csv_import.py b/backend/app/services/csv_import.py index 8138c73..9dd51c0 100644 --- a/backend/app/services/csv_import.py +++ b/backend/app/services/csv_import.py @@ -288,7 +288,7 @@ COUNTERPART_DAYS = 5 # 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 # sind normal, eine andere Buchung liegt praktisch nie so nah. -COUNTERPART_TOLERANCE = 0.10 +COUNTERPART_TOLERANCE = 0.20 def _find_counterpart( @@ -303,10 +303,11 @@ def _find_counterpart( Bei einem Umtausch stehen zwei Währungen und damit zwei Beträge gegenüber; dann zählt ein plausibler Betrag statt eines exakten.""" absamt = abs(amount) - unscharf = None + bester = None for eintrag in side_index.get(account.id, []): 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 # Die Richtung muss stimmen: Wo Geld ankam, kann keine Ausgabe die # Gegenseite sein. Ohne diese Prüfung verschluckt eine Ausgabe über @@ -319,9 +320,18 @@ def _find_counterpart( if absamt == erwartet: return eintrag cross = waehrung.get(other_id, account.currency) != account.currency - if cross and erwartet and abs(absamt - erwartet) <= erwartet * COUNTERPART_TOLERANCE: - unscharf = unscharf or eintrag - return unscharf + if not cross or not erwartet: + continue + 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]: