mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
feat: add free-form tags on transactions
Third of the four ideas picked from the Maybe/Sure feature audit. Tags are orthogonal to categories (category = what kind of spend, tag = which event/project) — many-to-many, created inline while typing in the transaction form, filterable from the Transactions page. Also adds tests/test_backup.py, the first test coverage the backup export/import path has ever had, and fixes a MissingGreenlet bug it surfaced in restore_backup's tag-wiring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from app.models.category import Category
|
||||
from app.models.goal import Goal, GoalContribution
|
||||
from app.models.recurring import RecurringTransaction
|
||||
from app.models.settings import AppSettings
|
||||
from app.models.tag import Tag
|
||||
from app.models.transaction import Transaction
|
||||
|
||||
__all__ = [
|
||||
@@ -17,5 +18,6 @@ __all__ = [
|
||||
"Goal",
|
||||
"GoalContribution",
|
||||
"RecurringTransaction",
|
||||
"Tag",
|
||||
"Transaction",
|
||||
]
|
||||
|
||||
26
backend/app/models/tag.py
Normal file
26
backend/app/models/tag.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""A free-form, reusable label on transactions — orthogonal to Category:
|
||||
a category answers "what kind of spend is this", a tag answers "which
|
||||
event/project does it belong to" (e.g. "trip:georgia", "tax-deductible").
|
||||
A transaction can carry any number of tags, a tag can be reused across any
|
||||
number of transactions — see transaction_tags below.
|
||||
"""
|
||||
from sqlalchemy import Column, ForeignKey, String, Table
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
transaction_tags = Table(
|
||||
"transaction_tags",
|
||||
Base.metadata,
|
||||
Column("transaction_id", ForeignKey("transactions.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("tag_id", ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
__tablename__ = "tags"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||
|
||||
transactions: Mapped[list["Transaction"]] = relationship(secondary=transaction_tags, back_populates="tags")
|
||||
@@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.db.base import Base
|
||||
from app.models.enums import TransactionType
|
||||
from app.models.mixins import TimestampMixin
|
||||
from app.models.tag import transaction_tags
|
||||
|
||||
|
||||
class Transaction(Base, TimestampMixin):
|
||||
@@ -33,3 +34,4 @@ class Transaction(Base, TimestampMixin):
|
||||
account: Mapped["Account"] = relationship(back_populates="transactions", foreign_keys=[account_id])
|
||||
transfer_account: Mapped["Account | None"] = relationship(foreign_keys=[transfer_account_id])
|
||||
category: Mapped["Category | None"] = relationship(back_populates="transactions")
|
||||
tags: Mapped[list["Tag"]] = relationship(secondary=transaction_tags, back_populates="transactions")
|
||||
|
||||
Reference in New Issue
Block a user