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)] ? (
—
) : (