fix: Ein Uebertragsvorschlag erscheint nur noch einmal

Jedes Paar steht in der Kandidatenliste zweimal, einmal je Richtung. Was
verknuepft wird, faellt ueber "verbraucht" heraus - ein blosser Vorschlag aber
nicht. Also bot /uebertraege/vorschlaege dieselbe Zahlung zweimal zum
Verbinden an, und wer das zweite anklickte, bekam "bereits verknuepft" fuer
etwas, das er gerade selbst verknuepft hatte.

Betrifft auch die Weboberflaeche: Uebertragsvorschlaege.jsx zeigt die Liste
unveraendert an und hatte denselben Doppeleintrag.

Gefunden beim Bauen der Arbeitsliste fuer die iOS-App - die Pruefdatei brachte
den Vorschlag doppelt zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-10-21 21:28:00 +02:00
co-authored by Claude Opus 5
parent d04b6b74ad
commit adbc3e0aa4
3 changed files with 76 additions and 4 deletions
+15 -4
View File
@@ -310,6 +310,17 @@ def finde(db: Session, user: User, nur_vorschlaege: bool = False) -> dict:
verbraucht: set[int] = set()
verknuepft = 0
vorschlaege: list[tuple[Transaction, Transaction]] = []
# Jedes Paar steht in ``kandidaten`` zweimal, einmal je Richtung. Was
# verknüpft wird, fällt über ``verbraucht`` heraus – ein Vorschlag aber
# nicht, und so kam dieselbe Zahlung zweimal zum Verbinden angeboten.
vorgeschlagen: set[tuple[int, int]] = set()
def schlage_vor(a: Transaction, b: Transaction) -> None:
paar = (min(a.id, b.id), max(a.id, b.id))
if paar in vorgeschlagen:
return
vorgeschlagen.add(paar)
vorschlaege.append((a, b))
for a_id in sorted(kandidaten):
if a_id in verbraucht:
@@ -321,17 +332,17 @@ def finde(db: Session, user: User, nur_vorschlaege: bool = False) -> dict:
gegen = _eindeutig(a, offen, kandidaten, verbraucht)
if gegen is None:
if offen:
vorschlaege.append((a, offen[0]))
schlage_vor(a, offen[0])
continue
# Automatisch nur ohne Kategorie und mit Hinweis – siehe _hat_hinweis.
if a.category_id is not None or gegen.category_id is not None:
vorschlaege.append((a, gegen))
schlage_vor(a, gegen)
continue
if not _hat_hinweis(a, gegen, bezeichner, typen):
vorschlaege.append((a, gegen))
schlage_vor(a, gegen)
continue
if nur_vorschlaege:
vorschlaege.append((a, gegen))
schlage_vor(a, gegen)
continue
verbinde(db, user, a, gegen, automatisch=True)
verbraucht.update((a.id, gegen.id))
+34
View File
@@ -91,6 +91,37 @@ def _seed(c: TestClient) -> None:
params={"planned_cents": 50000})
c.post(f"/api/subscriptions/{abo['id']}/buchungen/{kauf['id']}")
# --- Arbeitsliste -----------------------------------------------------
# Ein Eingangseintrag entsteht nur ueber die Schnellerfassung; von Hand
# legt man keinen an. Also eine Marke ausstellen und damit buchen.
marke = c.post("/api/inbox/tokens",
json={"name": "Fixture", "default_account_id": giro["id"]}).json()
kopf = {"X-Certain-Token": marke["token"]}
c.post("/api/quick/booking", headers=kopf,
json={"amount": "12.90", "payee": "Baeckerei", "date": heute,
"external_id": "fixture-1"})
# Zwei gleich aussehende Buchungen: gleiches Konto, gleicher Tag,
# gleicher krummer Betrag. Genau das meldet /dubletten.
for _ in range(2):
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 1043, "payee": "Kiosk", "category_id": ausgabe["id"]})
# Ein Abgang und ein Zugang ueber verschiedene Konten, gleicher Betrag,
# unverknuepft - daraus macht /uebertraege/vorschlaege einen Vorschlag.
# Beide Konten muessen dieselbe Waehrung haben: 75 CHF und 75 EUR sind
# nicht derselbe Betrag, nur dieselbe Zahl (siehe _kandidaten).
zweitgiro = c.post("/api/accounts", json={
"name": "Zweitkonto", "type": "girokonto", "currency": "EUR",
"opening_balance_cents": 0}).json()
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
c.post("/api/transactions", json={
"type": "income", "date": heute, "account_id": zweitgiro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
PFADE = {
"version": "/api/version",
@@ -108,6 +139,9 @@ PFADE = {
"budgetfortschritt": "/api/dashboard/budget-progress",
"einnahmenAusgaben": "/api/dashboard/income-expense",
"letzteBuchungen": "/api/dashboard/recent",
"eingang": "/api/inbox",
"dubletten": "/api/transactions/dubletten",
"uebertragsvorschlaege": "/api/uebertraege/vorschlaege",
}
+27
View File
@@ -354,3 +354,30 @@ def test_vorschlag_auch_wenn_die_gegenseite_tage_spaeter_steht(admin):
# Bestaetigen kann der Nutzer es trotzdem.
assert admin.post("/api/uebertraege/verbinden",
json={"a_id": raus["id"], "b_id": rein["id"]}).status_code == 200
def test_ein_paar_wird_nur_einmal_vorgeschlagen(admin):
"""Jedes Paar steht intern zweimal, einmal je Richtung.
Ohne Entdopplung saehe der Nutzer dieselbe Zahlung zweimal zum Verbinden
angeboten - und wer das zweite anklickt, bekaeme den Fehler "bereits
verknuepft" fuer etwas, das er gerade selbst verknuepft hat.
"""
import secrets
betrag = 100000 + secrets.randbelow(800000)
a = admin.post("/api/accounts", json={
"name": f"A {secrets.token_hex(3)}", "currency": "EUR"}).json()
b = admin.post("/api/accounts", json={
"name": f"B {secrets.token_hex(3)}", "currency": "EUR"}).json()
raus = admin.post("/api/transactions", json={
"type": "expense", "date": "2026-07-14", "account_id": a["id"],
"amount_cents": betrag, "payee": "Umbuchung"}).json()
rein = admin.post("/api/transactions", json={
"type": "income", "date": "2026-07-14", "account_id": b["id"],
"amount_cents": betrag, "payee": "Umbuchung"}).json()
paare = admin.get("/api/uebertraege/vorschlaege").json()["vorschlaege"]
meins = [p for p in paare
if {p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]}]
assert len(meins) == 1, f"einmal vorschlagen, nicht {len(meins)}-mal"