fix: Erkennungstext trifft nur noch an Wortgrenzen

Der Erkennungs-Text eines Kontos wurde als blosser Teilstring gesucht.
Am EUR-Konto stand „Euro" – der steckt auch in „PayPal Europe",
„Europa-Park" und „EUROBUS". Jede solche Zahlung wurde dadurch zum
Uebertrag auf dieses Konto, das Geld dort zusaetzlich gutgeschrieben und
der Saldo lief um mehr als tausend Euro daneben.

Gesucht wird jetzt nur an Wortgrenzen; eine Kartenendung wie „6543"
schlaegt damit auch nicht mehr mitten in einer laengeren Kontonummer an.
Das Kontoformular warnt zusaetzlich bei sehr kurzen Erkennungstexten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 15:31:41 +02:00
co-authored by Claude Opus 5
parent 5dbfd6abc4
commit 1e7c1ec014
5 changed files with 63 additions and 3 deletions
+18 -1
View File
@@ -6,6 +6,7 @@ import csv
import hashlib
import io
import json
import re
from collections import Counter
from datetime import date, datetime
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
@@ -251,13 +252,29 @@ def _match_rule(
return None
def _trifft_als_wort(nadel: str, heu: str) -> bool:
"""Erkennungs-Text nur an Wortgrenzen suchen, nicht mitten im Wort.
Ein kurzer Text richtet als reiner Teilstring Schaden an: Der Erkennungs-Text
„Euro" eines EUR-Kontos steckt auch in „PayPal Europe", „Europa-Park" und
„EUROBUS" – jede solche Zahlung wurde dadurch zum Übertrag auf dieses Konto
und das Geld doppelt gezählt. Genauso darf die Kartenendung „6543" nicht
mitten in einer längeren Kontonummer anschlagen.
"""
return re.search(rf"(?<![0-9a-zäöüß]){re.escape(nadel)}(?![0-9a-zäöüß])", heu) is not None
def _guess_target_account(text_idents: list[tuple[str, int]], payee: str, purpose: str, source_id: int) -> int | None:
"""Zielkonto eines Transfers aus dem Buchungstext raten: längster passender
Erkennungs-Text je Konto gewinnt."""
hay = f"{normalize_text(payee)} {normalize_text(purpose)}"
best: tuple[int, int] | None = None # (Länge, Konto-ID)
for val, acc_id in text_idents:
if val and acc_id != source_id and val in hay and (best is None or len(val) > best[0]):
if not val or acc_id == source_id:
continue
if not _trifft_als_wort(val, hay):
continue
if best is None or len(val) > best[0]:
best = (len(val), acc_id)
return best[1] if best else None