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:
ZProger
2026-08-25 02:38:04 +03:00
parent 09a0e383e0
commit 94782ee073
24 changed files with 585 additions and 10 deletions

View File

@@ -42,6 +42,13 @@ class CategoryBackup(BaseModel):
parent_id: int | None = None
class TagBackup(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
class TransactionBackup(BaseModel):
model_config = ConfigDict(from_attributes=True)
@@ -55,6 +62,11 @@ class TransactionBackup(BaseModel):
merchant: str | None
notes: str | None
date: date_
# Defaulted so a backup exported before tags existed still imports
# cleanly under the same format version. Not a plain column — populated
# explicitly in build_backup() from the `tags` relationship, since
# from_attributes can't map a `tags` relationship to a `tag_ids` field.
tag_ids: list[int] = Field(default_factory=list)
class AssetBackup(BaseModel):
@@ -155,6 +167,9 @@ class BackupPayload(BaseModel):
app_version: str
accounts: list[AccountBackup]
categories: list[CategoryBackup]
# Defaulted so a backup exported before tags existed still imports
# cleanly under the same format version.
tags: list[TagBackup] = Field(default_factory=list)
transactions: list[TransactionBackup]
assets: list[AssetBackup]
asset_valuations: list[AssetValuationBackup]