feat: Buchungen lassen sich mit eigenen, farbigen Tags versehen und danach filtern

Die Kategorie beantwortet "wofuer war das Geld" und gilt genau einmal. Alles,
was quer dazu liegt – "Urlaub Island", "absetzbar", "zurueckfordern" –, hatte
bisher keinen Platz, weil eine Buchung davon mehrere auf einmal sein kann.

Tags werden auf einer eigenen Seite angelegt, jeder mit seiner Farbe. Gesetzt
werden sie an der Buchung: direkt in der Liste oder beim Bearbeiten. Die
Spalte laesst sich filtern, auch nach mehreren Tags gleichzeitig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-19 07:16:17 +02:00
co-authored by Claude Opus 5
parent a9ea0432d8
commit 50fc0e3353
16 changed files with 961 additions and 11 deletions
@@ -0,0 +1,60 @@
"""Tags fuer Buchungen.
Die Kategorie beantwortet „wofuer war das Geld". Alles, was quer dazu liegt,
hatte bisher keinen Platz: „Urlaub Island", „steuerlich absetzbar",
„zurueckfordern". Dafuer gibt es jetzt frei vergebene Etiketten mit eigener
Farbe, mehrere je Buchung.
Zwei neue Tabellen, keine bestehende wird angefasst – ``create_table`` ist
deshalb unbedenklich. Die Warnung gegen ``batch_alter_table`` aus 0034-0037
betrifft den UMBAU vorhandener Tabellen, der ueber ON DELETE CASCADE haengende
Zeilen mitrisse (tests/test_migrations.py).
Revision ID: 0038
Revises: 0037
"""
import sqlalchemy as sa
from alembic import op
revision = "0038"
down_revision = "0037"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"tags",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("owner_id", sa.Integer(),
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.String(length=60), nullable=False),
sa.Column("color", sa.String(length=9), nullable=False,
server_default="#6b7480"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.UniqueConstraint("owner_id", "name", name="uq_tag_owner_name"),
)
op.create_index("ix_tags_owner_id", "tags", ["owner_id"])
op.create_table(
"transaction_tags",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("owner_id", sa.Integer(),
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("transaction_id", sa.Integer(),
sa.ForeignKey("transactions.id", ondelete="CASCADE"), nullable=False),
sa.Column("tag_id", sa.Integer(),
sa.ForeignKey("tags.id", ondelete="CASCADE"), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.UniqueConstraint("transaction_id", "tag_id", name="uq_txn_tag"),
)
op.create_index("ix_transaction_tags_owner_id", "transaction_tags", ["owner_id"])
op.create_index("ix_transaction_tags_transaction_id", "transaction_tags",
["transaction_id"])
op.create_index("ix_transaction_tags_tag_id", "transaction_tags", ["tag_id"])
def downgrade() -> None:
op.drop_table("transaction_tags")
op.drop_table("tags")