6 changed files with 115 additions and 5 deletions
@ -0,0 +1,40 @@ |
|||||||
|
# Generated manually because the container image has no write access to the workspace. |
||||||
|
|
||||||
|
import django.db.models.deletion |
||||||
|
import uuid |
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [("finance", "0003_reconciliationcheckpoint")] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name="Tag", |
||||||
|
fields=[ |
||||||
|
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)), |
||||||
|
("updated_at", models.DateTimeField(auto_now=True)), |
||||||
|
("name", models.CharField(max_length=255, unique=True)), |
||||||
|
("archived", models.BooleanField(default=False)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name="CategoryGroup", |
||||||
|
fields=[ |
||||||
|
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)), |
||||||
|
("updated_at", models.DateTimeField(auto_now=True)), |
||||||
|
("name", models.CharField(max_length=255, unique=True)), |
||||||
|
("archived", models.BooleanField(default=False)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.AlterField(model_name="category", name="name", field=models.CharField(max_length=255)), |
||||||
|
migrations.AddField(model_name="category", name="group", field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name="categories", to="finance.categorygroup")), |
||||||
|
migrations.AddConstraint(model_name="categorygroup", constraint=models.CheckConstraint(condition=models.Q(("name__regex", "\\S")), name="category_group_nonblank_name")), |
||||||
|
migrations.AddConstraint(model_name="tag", constraint=models.CheckConstraint(condition=models.Q(("name__regex", "\\S")), name="tag_nonblank_name")), |
||||||
|
migrations.AddConstraint(model_name="category", constraint=models.UniqueConstraint(fields=("group", "name"), name="category_group_name_unique")), |
||||||
|
migrations.AddConstraint(model_name="category", constraint=models.UniqueConstraint(condition=models.Q(("group__isnull", True)), fields=("name",), name="category_ungrouped_name_unique")), |
||||||
|
migrations.AddField(model_name="transaction", name="tags", field=models.ManyToManyField(blank=True, related_name="transactions", to="finance.tag")), |
||||||
|
] |
||||||
@ -1,9 +1,9 @@ |
|||||||
from .ledger import CardStatement, InstallmentPlan, Transaction, Transfer |
from .ledger import CardStatement, InstallmentPlan, Transaction, Transfer |
||||||
from .provenance import AuditEvent, SourceRecord |
from .provenance import AuditEvent, SourceRecord |
||||||
from .reconciliation import ReconciliationCheckpoint |
from .reconciliation import ReconciliationCheckpoint |
||||||
from .reference import Account, Category, Payee |
from .reference import Account, Category, CategoryGroup, Payee, Tag |
||||||
|
|
||||||
__all__ = [ |
__all__ = [ |
||||||
"Account", "AuditEvent", "CardStatement", "Category", "InstallmentPlan", |
"Account", "AuditEvent", "CardStatement", "Category", "CategoryGroup", "InstallmentPlan", |
||||||
"Payee", "ReconciliationCheckpoint", "SourceRecord", "Transaction", "Transfer", |
"Payee", "ReconciliationCheckpoint", "SourceRecord", "Tag", "Transaction", "Transfer", |
||||||
] |
] |
||||||
|
|||||||
@ -0,0 +1,41 @@ |
|||||||
|
from datetime import date |
||||||
|
from decimal import Decimal |
||||||
|
|
||||||
|
import pytest |
||||||
|
|
||||||
|
from finance.models import Account, Category, Tag, Transaction |
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db |
||||||
|
|
||||||
|
|
||||||
|
def test_ungrouped_categories_and_transaction_links_remain_valid(): |
||||||
|
account = Account.objects.create(name="Synthetic bank", kind="BANK") |
||||||
|
category = Category.objects.create(name="Synthetic Food", kind="EXPENSE") |
||||||
|
transaction = Transaction.objects.create( |
||||||
|
account=account, |
||||||
|
amount=Decimal("-10.00"), |
||||||
|
posting_date=date(2026, 9, 14), |
||||||
|
kind="EXPENSE", |
||||||
|
category=category, |
||||||
|
) |
||||||
|
|
||||||
|
category.refresh_from_db() |
||||||
|
|
||||||
|
assert category.group_id is None |
||||||
|
assert transaction.category_id == category.pk |
||||||
|
|
||||||
|
|
||||||
|
def test_tags_can_be_attached_without_changing_category(): |
||||||
|
account = Account.objects.create(name="Synthetic bank", kind="BANK") |
||||||
|
transaction = Transaction.objects.create( |
||||||
|
account=account, |
||||||
|
amount=Decimal("-10.00"), |
||||||
|
posting_date=date(2026, 9, 14), |
||||||
|
kind="EXPENSE", |
||||||
|
) |
||||||
|
tag = Tag.objects.create(name="Synthetic Tax") |
||||||
|
|
||||||
|
transaction.tags.add(tag) |
||||||
|
|
||||||
|
assert list(transaction.tags.all()) == [tag] |
||||||
Loading…
Reference in new issue