feat: Die Buchungsliste laesst sich im Server durchsuchen und filtern
Drei neue Abfrageparameter an GET /api/transactions: q sucht in Empfaenger, Verwendungszweck und Notiz, tag_id filtert auf ein Etikett, subscription_id auf einen Vertrag. Bisher gab es das nur im Browser - die Weboberflaeche laedt alle Buchungen und siebt sie dort. Am Telefon ist das der Unterschied zwischen "laedt" und "benutzbar"; das Limit steht nicht umsonst auf 5000. Zwei Dinge, die die Tests festhalten: Prozent- und Unterstrich-Zeichen im Suchtext sind Zeichen, keine Platzhalter - sonst faende die Eingabe "50%" alles. Und ein fremder tag_id liefert nichts statt alles, weil der Filter auf TransactionTag.owner_id anschlaegt. Der Tag-Filter nimmt eine Unterabfrage wie der uncategorized-Zweig daneben, keinen Join: Ein Join gaebe jede Buchung so oft zurueck, wie sie Tags traegt. Die lokale Abfragevariable hiess q und heisst jetzt abfrage, damit der Parameter den Namen bekommen konnte. Bekannte Grenze: SQLite gleicht bei LIKE nur ASCII gross/klein ab, "Mueller" mit Umlaut findet also nicht jede Schreibweise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +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 import or_, select, union
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
@@ -194,18 +194,24 @@ def list_transactions(
|
||||
type: TxnType | None = None,
|
||||
date_from: date | None = None,
|
||||
date_to: date | None = None,
|
||||
# Suche und die zwei Filter, die es bisher nur im Browser gab. Die
|
||||
# Weboberfläche lädt alle Buchungen und siebt sie dort; ein Telefon soll
|
||||
# dafür nicht 5000 Zeilen über die Leitung ziehen müssen.
|
||||
q: str | None = None,
|
||||
tag_id: int | None = None,
|
||||
subscription_id: int | None = None,
|
||||
# Großzügiges Limit: die Buchungsliste (DataTable) filtert/sortiert im Browser,
|
||||
# deshalb sollen standardmäßig ALLE Buchungen geladen werden, nicht nur 200.
|
||||
limit: int = Query(default=5000, le=50000),
|
||||
offset: int = 0,
|
||||
):
|
||||
q = db.query(Transaction).filter(Transaction.owner_id == user.id)
|
||||
abfrage = db.query(Transaction).filter(Transaction.owner_id == user.id)
|
||||
if account_id is not None:
|
||||
q = q.filter(
|
||||
abfrage = abfrage.filter(
|
||||
(Transaction.account_id == account_id) | (Transaction.to_account_id == account_id)
|
||||
)
|
||||
if category_id is not None:
|
||||
q = q.filter(Transaction.category_id == category_id)
|
||||
abfrage = abfrage.filter(Transaction.category_id == category_id)
|
||||
if uncategorized:
|
||||
# Verknüpfte Buchungen sind erledigt: Sie gehören zu einem Übertrag und
|
||||
# zählen in keiner Auswertung. Sie hier nach einer Kategorie zu fragen,
|
||||
@@ -214,16 +220,39 @@ def list_transactions(
|
||||
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))
|
||||
abfrage = abfrage.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)
|
||||
abfrage = abfrage.filter(Transaction.type == type)
|
||||
if date_from is not None:
|
||||
q = q.filter(Transaction.date >= date_from)
|
||||
abfrage = abfrage.filter(Transaction.date >= date_from)
|
||||
if date_to is not None:
|
||||
q = q.filter(Transaction.date <= date_to)
|
||||
rows = q.order_by(Transaction.date.desc(), Transaction.id.desc()).limit(limit).offset(offset).all()
|
||||
abfrage = abfrage.filter(Transaction.date <= date_to)
|
||||
if q:
|
||||
# LIKE statt Volltextindex: Ein Haushalt bleibt im Zehntausenderbereich,
|
||||
# da kostet die Suche nichts Spürbares. Was SQLite dabei nicht kann:
|
||||
# Umlaute gross/klein gleichsetzen – „Müller" findet „müller" nicht.
|
||||
# Die Prozent- und Unterstrich-Zeichen des Suchtexts werden entwertet,
|
||||
# sonst wäre jede Eingabe mit „%" ein Platzhalter statt eines Zeichens.
|
||||
muster = "%" + q.translate(
|
||||
str.maketrans({"\\": "\\\\", "%": "\\%", "_": "\\_"})
|
||||
) + "%"
|
||||
abfrage = abfrage.filter(or_(
|
||||
Transaction.payee.ilike(muster, escape="\\"),
|
||||
Transaction.description.ilike(muster, escape="\\"),
|
||||
Transaction.notes.ilike(muster, escape="\\"),
|
||||
))
|
||||
if tag_id is not None:
|
||||
# Unterabfrage statt Join, wie im uncategorized-Zweig: Ein Join würde
|
||||
# jede Buchung so oft liefern, wie sie Tags trägt.
|
||||
mit_tag = select(TransactionTag.transaction_id).where(
|
||||
TransactionTag.owner_id == user.id, TransactionTag.tag_id == tag_id
|
||||
)
|
||||
abfrage = abfrage.filter(Transaction.id.in_(mit_tag))
|
||||
if subscription_id is not None:
|
||||
abfrage = abfrage.filter(Transaction.subscription_id == subscription_id)
|
||||
rows = abfrage.order_by(Transaction.date.desc(), Transaction.id.desc()).limit(limit).offset(offset).all()
|
||||
base = base_currency(db, user)
|
||||
ccy = _currency_map(db, user)
|
||||
woher = _herkunft(db, user, rows)
|
||||
|
||||
Reference in New Issue
Block a user