Merge branch 'main' into rest_filtering

This commit is contained in:
Herculino Trotta
2026-01-10 17:43:45 -03:00
committed by GitHub
21 changed files with 1317 additions and 1071 deletions

View File

@@ -22,6 +22,9 @@ class DCAStrategyViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return DCAStrategy.objects.all()
def get_queryset(self):
return DCAStrategy.objects.all().order_by("id")
@action(detail=True, methods=["get"])
def investment_frequency(self, request, pk=None):
strategy = self.get_object()

View File

@@ -75,6 +75,8 @@ def transactions_list(request, month: int, year: int):
if order != request.session.get("monthly_transactions_order", "default"):
request.session["monthly_transactions_order"] = order
today = timezone.localdate(timezone.now())
f = TransactionsFilter(request.GET)
transactions_filtered = f.qs.filter(
reference_date__year=year,
@@ -92,12 +94,28 @@ def transactions_list(request, month: int, year: int):
"dca_income_entries",
)
# Late transactions: date < today and is_paid = False (only shown for default ordering)
late_transactions = None
if order == "default":
late_transactions = transactions_filtered.filter(
date__lt=today,
is_paid=False,
).order_by("date", "id")
# Exclude late transactions from the main list
transactions_filtered = transactions_filtered.exclude(
date__lt=today,
is_paid=False,
)
transactions_filtered = default_order(transactions_filtered, order=order)
return render(
request,
"monthly_overview/fragments/list.html",
context={"transactions": transactions_filtered},
context={
"transactions": transactions_filtered,
"late_transactions": late_transactions,
},
)

View File

@@ -383,6 +383,10 @@ class Transaction(OwnedObject):
def clean(self):
super().clean()
# Convert empty internal_id to None to allow multiple "empty" values with unique constraint
if self.internal_id == "":
self.internal_id = None
# Only process amount and reference_date if account exists
# If account is missing, Django's required field validation will handle it
try:

View File

@@ -125,6 +125,70 @@ class TransactionTests(TestCase):
datetime.datetime(day=1, month=2, year=2000).date(),
)
def test_empty_internal_id_converts_to_none(self):
"""Test that empty string internal_id is converted to None"""
transaction = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction",
internal_id="", # Empty string should become None
)
self.assertIsNone(transaction.internal_id)
def test_unique_internal_id_works(self):
"""Test that unique non-empty internal_id values work correctly"""
transaction1 = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction 1",
internal_id="unique-id-123",
)
transaction2 = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction 2",
internal_id="unique-id-456",
)
self.assertEqual(transaction1.internal_id, "unique-id-123")
self.assertEqual(transaction2.internal_id, "unique-id-456")
def test_multiple_transactions_with_empty_internal_id(self):
"""Test that multiple transactions can have empty/None internal_id"""
transaction1 = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction 1",
internal_id="",
)
transaction2 = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction 2",
internal_id="",
)
transaction3 = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction 3",
internal_id=None,
)
# All should be saved successfully with None internal_id
self.assertIsNone(transaction1.internal_id)
self.assertIsNone(transaction2.internal_id)
self.assertIsNone(transaction3.internal_id)
class InstallmentPlanTests(TestCase):
def setUp(self):

View File

@@ -152,7 +152,9 @@ def transaction_simple_add(request):
date_param = request.GET.get("date")
if date_param:
try:
initial_data["date"] = datetime.datetime.strptime(date_param, "%Y-%m-%d").date()
initial_data["date"] = datetime.datetime.strptime(
date_param, "%Y-%m-%d"
).date()
except ValueError:
pass
@@ -160,7 +162,9 @@ def transaction_simple_add(request):
reference_date_param = request.GET.get("reference_date")
if reference_date_param:
try:
initial_data["reference_date"] = datetime.datetime.strptime(reference_date_param, "%Y-%m-%d").date()
initial_data["reference_date"] = datetime.datetime.strptime(
reference_date_param, "%Y-%m-%d"
).date()
except ValueError:
pass
@@ -172,7 +176,10 @@ def transaction_simple_add(request):
except (ValueError, TypeError):
# Try to find by name
from apps.accounts.models import Account
account = Account.objects.filter(name__iexact=account_param, is_archived=False).first()
account = Account.objects.filter(
name__iexact=account_param, is_archived=False
).first()
if account:
initial_data["account"] = account.pk
@@ -207,7 +214,10 @@ def transaction_simple_add(request):
except (ValueError, TypeError):
# Try to find by name
from apps.transactions.models import TransactionCategory
category = TransactionCategory.objects.filter(name__iexact=category_param, active=True).first()
category = TransactionCategory.objects.filter(
name__iexact=category_param, active=True
).first()
if category:
initial_data["category"] = category.pk
@@ -457,7 +467,7 @@ def transaction_pay(request, transaction_id):
context={"transaction": transaction, **request.GET},
)
response.headers["HX-Trigger"] = (
f'{"paid" if new_is_paid else "unpaid"}, selective_update'
f"{'paid' if new_is_paid else 'unpaid'}, selective_update"
)
return response
@@ -552,6 +562,8 @@ def transaction_all_list(request):
if order != request.session.get("all_transactions_order", "default"):
request.session["all_transactions_order"] = order
today = timezone.localdate(timezone.now())
transactions = Transaction.objects.prefetch_related(
"account",
"account__group",
@@ -565,12 +577,27 @@ def transaction_all_list(request):
"dca_income_entries",
).all()
transactions = default_order(transactions, order=order)
f = TransactionsFilter(request.GET, queryset=transactions)
# Late transactions: date < today and is_paid = False (only shown for default ordering on first page)
late_transactions = None
page_number = request.GET.get("page", 1)
paginator = Paginator(f.qs, 100)
if order == "default" and str(page_number) == "1":
late_transactions = f.qs.filter(
date__lt=today,
is_paid=False,
).order_by("date", "id")
# Exclude late transactions from the main paginated list
main_transactions = f.qs.exclude(
date__lt=today,
is_paid=False,
)
else:
main_transactions = f.qs
main_transactions = default_order(main_transactions, order=order)
paginator = Paginator(main_transactions, 100)
page_obj = paginator.get_page(page_number)
return render(
@@ -579,6 +606,7 @@ def transaction_all_list(request):
{
"page_obj": page_obj,
"paginator": paginator,
"late_transactions": late_transactions,
},
)