fix: Verknuepfte Buchungen brauchen keine Kategorie mehr

Eine verknuepfte Buchung gehoert zu einem Uebertrag und zaehlt in keiner
Auswertung. Trotzdem stand sie unter "Zu erledigen" und wurde in der
Buchungsliste nach einer Kategorie gefragt – Arbeit, die nichts bewirkt, und
ein falsches Signal: Sie sah aus, als fehle noch etwas.

Der Filter fuer offene Buchungen laesst verknuepfte jetzt aus, und in der
Buchungsliste steht bei ihnen ein Strich statt der Auswahl – so wie bei einem
echten Uebertrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-30 13:07:29 +02:00
co-authored by Claude Opus 5
parent bb748cc092
commit 6ed91ddd58
3 changed files with 48 additions and 3 deletions
+12 -1
View File
@@ -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:
+32
View File
@@ -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"
+4 -2
View File
@@ -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)] ? (
<span className="muted">—</span>
) : (
<CategorySelect