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:
@@ -9,6 +9,7 @@ from datetime import date
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import select, union
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
@@ -19,6 +20,7 @@ from ..models import (
|
|||||||
InboxItem,
|
InboxItem,
|
||||||
InboxStatus,
|
InboxStatus,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
TransferLink,
|
||||||
TxnType,
|
TxnType,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
@@ -134,7 +136,16 @@ def list_transactions(
|
|||||||
if category_id is not None:
|
if category_id is not None:
|
||||||
q = q.filter(Transaction.category_id == category_id)
|
q = q.filter(Transaction.category_id == category_id)
|
||||||
if uncategorized:
|
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:
|
if type is not None:
|
||||||
q = q.filter(Transaction.type == type)
|
q = q.filter(Transaction.type == type)
|
||||||
if date_from is not None:
|
if date_from is not None:
|
||||||
|
|||||||
@@ -46,3 +46,35 @@ def test_offene_buchungen_lassen_uebertraege_aus(admin):
|
|||||||
offen = admin.get("/api/transactions", params={"uncategorized": "true",
|
offen = admin.get("/api/transactions", params={"uncategorized": "true",
|
||||||
"account_id": a["id"]}).json()
|
"account_id": a["id"]}).json()
|
||||||
assert offen == []
|
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"
|
||||||
|
|||||||
@@ -347,8 +347,10 @@ export default function Transactions() {
|
|||||||
filterText: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
|
filterText: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
|
||||||
sortValue: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
|
sortValue: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
|
||||||
render: (tx) => (
|
render: (tx) => (
|
||||||
// Ein Übertrag bekommt keine Kategorie – er ordnet kein Geld ein.
|
// Ein Übertrag bekommt keine Kategorie – er ordnet kein Geld ein. Eine
|
||||||
tx.type === "transfer" ? (
|
// 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>
|
<span className="muted">—</span>
|
||||||
) : (
|
) : (
|
||||||
<CategorySelect
|
<CategorySelect
|
||||||
|
|||||||
Reference in New Issue
Block a user