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:
@@ -310,6 +310,17 @@ def finde(db: Session, user: User, nur_vorschlaege: bool = False) -> dict:
|
|||||||
verbraucht: set[int] = set()
|
verbraucht: set[int] = set()
|
||||||
verknuepft = 0
|
verknuepft = 0
|
||||||
vorschlaege: list[tuple[Transaction, Transaction]] = []
|
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):
|
for a_id in sorted(kandidaten):
|
||||||
if a_id in verbraucht:
|
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)
|
gegen = _eindeutig(a, offen, kandidaten, verbraucht)
|
||||||
if gegen is None:
|
if gegen is None:
|
||||||
if offen:
|
if offen:
|
||||||
vorschlaege.append((a, offen[0]))
|
schlage_vor(a, offen[0])
|
||||||
continue
|
continue
|
||||||
# Automatisch nur ohne Kategorie und mit Hinweis – siehe _hat_hinweis.
|
# Automatisch nur ohne Kategorie und mit Hinweis – siehe _hat_hinweis.
|
||||||
if a.category_id is not None or gegen.category_id is not None:
|
if a.category_id is not None or gegen.category_id is not None:
|
||||||
vorschlaege.append((a, gegen))
|
schlage_vor(a, gegen)
|
||||||
continue
|
continue
|
||||||
if not _hat_hinweis(a, gegen, bezeichner, typen):
|
if not _hat_hinweis(a, gegen, bezeichner, typen):
|
||||||
vorschlaege.append((a, gegen))
|
schlage_vor(a, gegen)
|
||||||
continue
|
continue
|
||||||
if nur_vorschlaege:
|
if nur_vorschlaege:
|
||||||
vorschlaege.append((a, gegen))
|
schlage_vor(a, gegen)
|
||||||
continue
|
continue
|
||||||
verbinde(db, user, a, gegen, automatisch=True)
|
verbinde(db, user, a, gegen, automatisch=True)
|
||||||
verbraucht.update((a.id, gegen.id))
|
verbraucht.update((a.id, gegen.id))
|
||||||
|
|||||||
@@ -91,6 +91,37 @@ def _seed(c: TestClient) -> None:
|
|||||||
params={"planned_cents": 50000})
|
params={"planned_cents": 50000})
|
||||||
c.post(f"/api/subscriptions/{abo['id']}/buchungen/{kauf['id']}")
|
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 = {
|
PFADE = {
|
||||||
"version": "/api/version",
|
"version": "/api/version",
|
||||||
@@ -108,6 +139,9 @@ PFADE = {
|
|||||||
"budgetfortschritt": "/api/dashboard/budget-progress",
|
"budgetfortschritt": "/api/dashboard/budget-progress",
|
||||||
"einnahmenAusgaben": "/api/dashboard/income-expense",
|
"einnahmenAusgaben": "/api/dashboard/income-expense",
|
||||||
"letzteBuchungen": "/api/dashboard/recent",
|
"letzteBuchungen": "/api/dashboard/recent",
|
||||||
|
"eingang": "/api/inbox",
|
||||||
|
"dubletten": "/api/transactions/dubletten",
|
||||||
|
"uebertragsvorschlaege": "/api/uebertraege/vorschlaege",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -354,3 +354,30 @@ def test_vorschlag_auch_wenn_die_gegenseite_tage_spaeter_steht(admin):
|
|||||||
# Bestaetigen kann der Nutzer es trotzdem.
|
# Bestaetigen kann der Nutzer es trotzdem.
|
||||||
assert admin.post("/api/uebertraege/verbinden",
|
assert admin.post("/api/uebertraege/verbinden",
|
||||||
json={"a_id": raus["id"], "b_id": rein["id"]}).status_code == 200
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user