fix: Eine Gegenseite, die Tage spaeter gebucht wurde, wird jetzt zum Verbinden vorgeschlagen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f52113e05d
commit
b1f5eb9a51
@@ -41,7 +41,7 @@ from ..models import (
|
||||
User,
|
||||
)
|
||||
from . import currency
|
||||
from .csv_import import normalize_text
|
||||
from .csv_import import INBOX_TAGE, normalize_text
|
||||
|
||||
# Wörter, die eine Bewegung zwischen eigenen Konten ankündigen. Ein Treffer
|
||||
# allein verknüpft noch nichts – er erlaubt nur, dass es ohne Rückfrage
|
||||
@@ -146,8 +146,19 @@ def _hat_hinweis(a: Transaction, b: Transaction, bezeichner: dict[int, list[str]
|
||||
return False
|
||||
|
||||
|
||||
def _kandidaten(db: Session, user: User) -> dict[int, list[Transaction]]:
|
||||
"""Zu jeder unverknüpften Buchung die möglichen Gegenstücke."""
|
||||
def _kandidaten(db: Session, user: User,
|
||||
fenster: int = 0) -> dict[int, list[Transaction]]:
|
||||
"""Zu jeder unverknüpften Buchung die möglichen Gegenstücke.
|
||||
|
||||
``fenster`` ist der erlaubte Abstand in Tagen. 0 heisst derselbe Tag – so
|
||||
eng muss es bleiben, wo von selbst verknüpft wird.
|
||||
|
||||
Für reine VORSCHLÄGE darf es weiter sein, denn dort entscheidet der Nutzer.
|
||||
Sonst klafft eine Lücke: Die Übertrags-Prüfung sucht die Gegenseite über
|
||||
±INBOX_TAGE und schickt den Nutzer zum Verbinden – die Vorschlagsliste aber
|
||||
kannte nur den gleichen Tag und bot dieses Paar nie an. Die Bank bucht die
|
||||
Gegenseite regelmässig ein bis drei Tage später.
|
||||
"""
|
||||
verknuepft = set()
|
||||
for a, b in db.query(TransferLink.from_txn_id, TransferLink.to_txn_id).filter(
|
||||
TransferLink.owner_id == user.id
|
||||
@@ -169,18 +180,23 @@ def _kandidaten(db: Session, user: User) -> dict[int, list[Transaction]]:
|
||||
if t.id not in verknuepft
|
||||
]
|
||||
|
||||
# Nach Tag und Betrag bündeln: Nur wer beides teilt, kommt überhaupt infrage.
|
||||
nach_schluessel: dict[tuple, list[Transaction]] = defaultdict(list)
|
||||
# Nach Betrag bündeln, der Tag wird danach geprüft – nur so lässt sich ein
|
||||
# Fenster zulassen. Innerhalb eines Bündels nach Datum sortiert, damit die
|
||||
# Suche abbrechen kann, sobald der Abstand zu gross wird.
|
||||
nach_betrag: dict[int, list[Transaction]] = defaultdict(list)
|
||||
for t in buchungen:
|
||||
nach_schluessel[(t.date, t.amount_cents)].append(t)
|
||||
nach_betrag[t.amount_cents].append(t)
|
||||
|
||||
kandidaten: dict[int, list[Transaction]] = defaultdict(list)
|
||||
for gruppe in nach_schluessel.values():
|
||||
for gruppe in nach_betrag.values():
|
||||
if len(gruppe) < 2:
|
||||
continue
|
||||
for a in gruppe:
|
||||
for b in gruppe:
|
||||
if a.id == b.id or a.account_id == b.account_id:
|
||||
gruppe.sort(key=lambda t: (t.date, t.id))
|
||||
for i, a in enumerate(gruppe):
|
||||
for b in gruppe[i + 1:]:
|
||||
if (b.date - a.date).days > fenster:
|
||||
break # sortiert: alles Weitere liegt noch ferner
|
||||
if a.account_id == b.account_id:
|
||||
continue
|
||||
if a.type == b.type:
|
||||
continue # eine Ausgabe, eine Einnahme
|
||||
@@ -190,11 +206,19 @@ def _kandidaten(db: Session, user: User) -> dict[int, list[Transaction]]:
|
||||
if tuple(sorted((a.id, b.id))) in ignoriert:
|
||||
continue
|
||||
kandidaten[a.id].append(b)
|
||||
kandidaten[b.id].append(a)
|
||||
|
||||
# Währungswechsel: gleicher Tag, aber unterschiedliche Beträge. Hier
|
||||
# entscheidet der EZB-Kurs, ob die beiden zueinander passen können.
|
||||
# Währungswechsel: unterschiedliche Beträge. Hier entscheidet der EZB-Kurs,
|
||||
# ob die beiden zueinander passen können.
|
||||
kandidaten = _ergaenze_waehrungswechsel(db, user, buchungen, waehrung,
|
||||
ignoriert, kandidaten)
|
||||
ignoriert, kandidaten, fenster)
|
||||
|
||||
# Das nächstliegende Datum zuerst: Wo mehrere passen, ist der gleiche Tag
|
||||
# das wahrscheinlichere Paar als der drei Tage später.
|
||||
datum_je_id = {t.id: t.date for t in buchungen}
|
||||
for a_id, liste in kandidaten.items():
|
||||
a_datum = datum_je_id[a_id]
|
||||
liste.sort(key=lambda b, d=a_datum: (abs((b.date - d).days), b.id))
|
||||
return kandidaten
|
||||
|
||||
|
||||
@@ -210,24 +234,28 @@ def _passt_ueber_kurs(db: Session, a: Transaction, b: Transaction,
|
||||
return abs(erwartet - b.amount_cents) <= erwartet * FX_TOLERANZ
|
||||
|
||||
|
||||
def _ergaenze_waehrungswechsel(db, user, buchungen, waehrung, ignoriert, kandidaten):
|
||||
nach_tag: dict = defaultdict(list)
|
||||
for t in buchungen:
|
||||
nach_tag[t.date].append(t)
|
||||
for gruppe in nach_tag.values():
|
||||
for a in gruppe:
|
||||
for b in gruppe:
|
||||
if a.id == b.id or a.account_id == b.account_id or a.type == b.type:
|
||||
continue
|
||||
va, vb = waehrung.get(a.account_id), waehrung.get(b.account_id)
|
||||
if not va or not vb or va == vb:
|
||||
continue # gleiche Währung ist oben schon erledigt
|
||||
if a.amount_cents == b.amount_cents:
|
||||
continue # zufällig gleiche Zahl, andere Währung
|
||||
if tuple(sorted((a.id, b.id))) in ignoriert:
|
||||
continue
|
||||
if _passt_ueber_kurs(db, a, b, va, vb):
|
||||
kandidaten[a.id].append(b)
|
||||
def _ergaenze_waehrungswechsel(db, user, buchungen, waehrung, ignoriert, kandidaten,
|
||||
fenster: int = 0):
|
||||
nach_datum = sorted(buchungen, key=lambda t: (t.date, t.id))
|
||||
for i, a in enumerate(nach_datum):
|
||||
for b in nach_datum[i + 1:]:
|
||||
if (b.date - a.date).days > fenster:
|
||||
break # sortiert: alles Weitere liegt noch ferner
|
||||
if a.account_id == b.account_id or a.type == b.type:
|
||||
continue
|
||||
va, vb = waehrung.get(a.account_id), waehrung.get(b.account_id)
|
||||
if not va or not vb or va == vb:
|
||||
continue # gleiche Währung ist oben schon erledigt
|
||||
if a.amount_cents == b.amount_cents:
|
||||
continue # zufällig gleiche Zahl, andere Währung
|
||||
if tuple(sorted((a.id, b.id))) in ignoriert:
|
||||
continue
|
||||
# Die Umrechnung hat eine Richtung – beide prüfen, sonst hinge es
|
||||
# davon ab, welche der beiden zufaellig zuerst einsortiert war.
|
||||
if _passt_ueber_kurs(db, a, b, va, vb):
|
||||
kandidaten[a.id].append(b)
|
||||
if _passt_ueber_kurs(db, b, a, vb, va):
|
||||
kandidaten[b.id].append(a)
|
||||
return kandidaten
|
||||
|
||||
|
||||
@@ -267,7 +295,9 @@ def finde(db: Session, user: User, nur_vorschlaege: bool = False) -> dict:
|
||||
Vorschlag zurück. Das braucht jede Ansicht, die bloss fragen will („ist das
|
||||
ein Übertrag?"), ohne beim Hinsehen schon etwas zu verändern.
|
||||
"""
|
||||
kandidaten = _kandidaten(db, user)
|
||||
# Von selbst verknüpft wird nur am gleichen Tag. Vorschläge dürfen weiter
|
||||
# greifen – sie ändern nichts, bis der Nutzer zustimmt.
|
||||
kandidaten = _kandidaten(db, user, INBOX_TAGE if nur_vorschlaege else 0)
|
||||
bezeichner = _kontobezeichner(db, user)
|
||||
typen = _kontotypen(db, user)
|
||||
nach_id = {}
|
||||
|
||||
Reference in New Issue
Block a user