feat: Certain fragt in der Buchungsliste nach, ob es ein Uebertrag ist

Die Erkennung gab es laengst – aber nur auf der Seite "Zu erledigen", in einer
Karte, die man erst finden muss. Wer in der Buchungsliste steht und die zweite
Haelfte eines Uebertrags sieht, greift stattdessen zum ↔-Knopf und legt einen
zweiten Uebertrag an. Genau so ist heute eine Zahlung doppelt verschoben
worden.

Jetzt steht die Frage in der Zeile: "Uebertrag von/nach Revolut CHF?" mit
Verbinden und Ignorieren. Ignorieren wird gemerkt, sonst fragte Certain bei
jedem Laden erneut.

Dafuer gibt es die Suche jetzt auch als reines Nachschlagen: ``finde`` mit
``nur_vorschlaege`` verknuepft nichts, und dahinter liegt GET
/uebertraege/vorschlaege. Eine Ansicht, die bloss fragt, darf beim Hinsehen
nichts veraendern – sonst entschiede das Anschauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-30 13:03:25 +02:00
co-authored by Claude Opus 5
parent 7b3ed3e63c
commit bb748cc092
8 changed files with 128 additions and 7 deletions
+10
View File
@@ -41,6 +41,16 @@ def list_links(db: Session = Depends(get_db), user: User = Depends(get_current_u
]
@router.get("/vorschlaege")
def vorschlaege(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
"""Nur fragen, nichts tun.
Getrennt von ``/suchen``: Eine Ansicht, die bloss „ist das ein Übertrag?"
anzeigt, darf beim Hinsehen nichts verknüpfen.
"""
return uebertraege.finde(db, user, nur_vorschlaege=True)
@router.post("/suchen")
def suchen(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
"""Paare suchen. Eindeutige mit Übertrags-Hinweis werden verknüpft, der
+12 -3
View File
@@ -226,8 +226,13 @@ def _eindeutig(a: Transaction, moegliche: list[Transaction],
return moegliche[0] if len(konten) == 1 else None
def finde(db: Session, user: User) -> dict:
"""Paare suchen. Eindeutige verknüpfen, den Rest als Vorschlag melden."""
def finde(db: Session, user: User, nur_vorschlaege: bool = False) -> dict:
"""Paare suchen. Eindeutige verknüpfen, den Rest als Vorschlag melden.
Mit ``nur_vorschlaege`` wird NICHTS verknüpft – dann kommt alles als
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)
bezeichner = _kontobezeichner(db, user)
nach_id = {}
@@ -260,11 +265,15 @@ def finde(db: Session, user: User) -> dict:
if not _hat_hinweis(a, gegen, bezeichner):
vorschlaege.append((a, gegen))
continue
if nur_vorschlaege:
vorschlaege.append((a, gegen))
continue
verbinde(db, user, a, gegen, automatisch=True)
verbraucht.update((a.id, gegen.id))
verknuepft += 1
db.commit()
if not nur_vorschlaege:
db.commit()
return {
"verknuepft": verknuepft,
"vorschlaege": [
+26
View File
@@ -283,3 +283,29 @@ def test_ignorierte_paare_kehren_nicht_zurueck(admin):
admin.post("/api/uebertraege/ignorieren", json={"a_id": aus["id"], "b_id": ein["id"]})
danach = _suchen(admin)["vorschlaege"]
assert not any({v["a"]["id"], v["b"]["id"]} == {aus["id"], ein["id"]} for v in danach)
def test_vorschlaege_veraendern_nichts(admin):
"""Eine Ansicht, die bloss fragt „ist das ein Uebertrag?", darf beim
Hinsehen nichts verknuepfen – sonst entscheidet das Anschauen."""
import secrets
a = _konto(admin, name=f"V-A {secrets.token_hex(3)}")
b = _konto(admin, name=f"V-B {secrets.token_hex(3)}")
betrag = 30000 + secrets.randbelow(900)
# Mit Hinweis im Text – das wuerde /suchen automatisch verknuepfen.
raus = admin.post("/api/transactions", json={
"type": "expense", "date": "2026-08-20", "account_id": a["id"],
"amount_cents": betrag, "payee": f"Uebertrag auf {b['name']}"}).json()
rein = admin.post("/api/transactions", json={
"type": "income", "date": "2026-08-20", "account_id": b["id"],
"amount_cents": betrag, "payee": "Gutschrift"}).json()
r = admin.get("/api/uebertraege/vorschlaege").json()
assert r["verknuepft"] == 0, "beim Fragen darf nichts verknuepft werden"
paare = [(v["a"]["id"], v["b"]["id"]) for v in r["vorschlaege"]]
assert any({raus["id"], rein["id"]} == set(p) for p in paare)
# Und es ist wirklich keine Verknuepfung entstanden.
links = admin.get("/api/transactions/links").json()
assert str(raus["id"]) not in links and str(rein["id"]) not in links