diff --git a/backend/app/routers/transactions.py b/backend/app/routers/transactions.py index cb7a7d8..88d6793 100644 --- a/backend/app/routers/transactions.py +++ b/backend/app/routers/transactions.py @@ -9,6 +9,7 @@ from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel +from sqlalchemy import select, union from sqlalchemy.orm import Session from ..database import get_db @@ -19,6 +20,7 @@ from ..models import ( InboxItem, InboxStatus, Transaction, + TransferLink, TxnType, User, ) @@ -134,7 +136,16 @@ def list_transactions( if category_id is not None: q = q.filter(Transaction.category_id == category_id) if uncategorized: - q = q.filter(Transaction.category_id.is_(None), Transaction.type != TxnType.transfer) + # Verknüpfte Buchungen sind erledigt: Sie gehören zu einem Übertrag und + # zählen in keiner Auswertung. Sie hier nach einer Kategorie zu fragen, + # verlangt Arbeit, die nichts bewirkt. + verknuepft = union( + select(TransferLink.from_txn_id).where(TransferLink.owner_id == user.id), + select(TransferLink.to_txn_id).where(TransferLink.owner_id == user.id), + ) + q = q.filter(Transaction.category_id.is_(None), + Transaction.type != TxnType.transfer, + Transaction.id.notin_(verknuepft)) if type is not None: q = q.filter(Transaction.type == type) if date_from is not None: diff --git a/backend/tests/test_transactions_offen.py b/backend/tests/test_transactions_offen.py index f6affac..68b69f8 100644 --- a/backend/tests/test_transactions_offen.py +++ b/backend/tests/test_transactions_offen.py @@ -46,3 +46,35 @@ def test_offene_buchungen_lassen_uebertraege_aus(admin): offen = admin.get("/api/transactions", params={"uncategorized": "true", "account_id": a["id"]}).json() assert offen == [] + + +def test_verknuepfte_buchungen_sind_nicht_mehr_offen(admin): + """Sie gehoeren zu einem Uebertrag und zaehlen in keiner Auswertung. Nach + einer Kategorie zu fragen, verlangt Arbeit, die nichts bewirkt.""" + import secrets + + a = admin.post("/api/accounts", json={ + "name": f"Offen-A {secrets.token_hex(3)}", "type": "girokonto", + "currency": "CHF", "opening_balance_cents": 0}).json() + b = admin.post("/api/accounts", json={ + "name": f"Offen-B {secrets.token_hex(3)}", "type": "girokonto", + "currency": "CHF", "opening_balance_cents": 0}).json() + betrag = 20000 + secrets.randbelow(900) + raus = admin.post("/api/transactions", json={ + "type": "expense", "date": "2026-08-21", "account_id": a["id"], + "amount_cents": betrag, "payee": "Umbuchung"}).json() + rein = admin.post("/api/transactions", json={ + "type": "income", "date": "2026-08-21", "account_id": b["id"], + "amount_cents": betrag, "payee": "Umbuchung"}).json() + + offen = {t["id"] for t in admin.get("/api/transactions", + params={"uncategorized": True}).json()} + assert raus["id"] in offen and rein["id"] in offen, "vorher beide offen" + + assert admin.post("/api/uebertraege/verbinden", + json={"a_id": raus["id"], "b_id": rein["id"]}).status_code == 200 + + danach = {t["id"] for t in admin.get("/api/transactions", + params={"uncategorized": True}).json()} + assert raus["id"] not in danach and rein["id"] not in danach, \ + "verknuepfte Buchungen brauchen keine Kategorie mehr" diff --git a/web/src/pages/Transactions.jsx b/web/src/pages/Transactions.jsx index 16e885b..c0f3a5d 100644 --- a/web/src/pages/Transactions.jsx +++ b/web/src/pages/Transactions.jsx @@ -347,8 +347,10 @@ export default function Transactions() { filterText: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "", sortValue: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "", render: (tx) => ( - // Ein Übertrag bekommt keine Kategorie – er ordnet kein Geld ein. - tx.type === "transfer" ? ( + // Ein Übertrag bekommt keine Kategorie – er ordnet kein Geld ein. Eine + // VERKNÜPFTE Buchung ebenso wenig: Sie gehört zu einem Übertrag und + // zählt in keiner Auswertung, eine Kategorie bewirkt dort nichts. + tx.type === "transfer" || links[String(tx.id)] ? ( — ) : (