fix(transactions): keep transaction picker results in monthly view order

Results come back as today, tomorrow, yesterday, upcoming (soonest first)
and then past (newest first); past dates used to be oldest first. The
endpoint is now capped at 100 results, as its docstring already claimed.

tom-select also re-scored and re-sorted the server's results on the
client, which scrambled the order while searching. Load-based selects now
keep the server's order, drop the previous query's results, and preload
recent transactions on focus.
This commit is contained in:
Herculino Trotta committed 2026-11-08 03:27:15 -03:00
1 parent aa3ffe9b3e
commit 8482c85a76
2 files changed
+34 -27

No files matched your search

+27 -27
View File
@@ -25,7 +25,7 @@ from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Case, IntegerField, Q, Value, When
from django.db.models import Case, F, IntegerField, Q, Value, When
from django.http import FileResponse, Http404, HttpResponse, JsonResponse, QueryDict
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
@@ -972,40 +972,20 @@ def transactions_trash_can_list(request):
@login_required
@require_http_methods(["GET"])
def get_recent_transactions(request, filter_type=None):
"""Return the 100 most recent non-deleted transactions with optional search."""
# Get search term from query params
"""Return up to 100 non-deleted transactions, ordered like the monthly view, with optional search."""
search_term = request.GET.get("q", "").strip()
today = timezone.localdate(timezone.now())
yesterday = today - timezone.timedelta(days=1)
tomorrow = today + timezone.timedelta(days=1)
# Base queryset with selected fields
queryset = (
Transaction.objects.filter(deleted=False)
.annotate(
date_order=Case(
When(date=today, then=Value(0)),
When(date=tomorrow, then=Value(1)),
When(date=yesterday, then=Value(2)),
When(date__gt=tomorrow, then=Value(3)),
When(date__lt=yesterday, then=Value(4)),
default=Value(5),
output_field=IntegerField(),
)
)
.select_related("account__currency", "account__group", "category")
.prefetch_related("tags", "entities")
.order_by("date_order", "date", "id")
)
queryset = Transaction.objects.filter(deleted=False)
if filter_type:
if filter_type == "expenses":
queryset = queryset.filter(type=Transaction.Type.EXPENSE)
elif filter_type == "income":
queryset = queryset.filter(type=Transaction.Type.INCOME)
if filter_type == "expenses":
queryset = queryset.filter(type=Transaction.Type.EXPENSE)
elif filter_type == "income":
queryset = queryset.filter(type=Transaction.Type.INCOME)
# Apply search if provided
if search_term:
queryset = queryset.filter(
Q(description__icontains=search_term)
@@ -1015,4 +995,24 @@ def get_recent_transactions(request, filter_type=None):
| Q(category__name__icontains=search_term)
)
# Today, tomorrow, yesterday, then upcoming (soonest first), then past (newest first)
queryset = (
queryset.annotate(
date_order=Case(
When(date=today, then=Value(0)),
When(date=tomorrow, then=Value(1)),
When(date=yesterday, then=Value(2)),
When(date__gt=tomorrow, then=Value(3)),
default=Value(4),
output_field=IntegerField(),
),
upcoming_date=Case(When(date__gt=tomorrow, then=F("date"))),
)
.select_related("account__currency", "account__group", "category")
.prefetch_related("tags", "entities")
.order_by(
"date_order", F("upcoming_date").asc(nulls_last=True), "-date", "-id"
)[:100]
)
return JsonResponse([transaction_option(t) for t in queryset], safe=False)
+7
View File
@@ -130,11 +130,18 @@ window.TomSelect = function createDynamicTomSelect(element) {
}
if (element.dataset.load) {
// The server searches and orders; don't let tom-select re-filter or re-sort the results
config.score = () => () => 1;
config.sortField = [{field: '$order'}];
config.preload = 'focus';
config.shouldLoad = () => true;
config.load = function (query, callback) {
let url = element.dataset.load + '?q=' + encodeURIComponent(query);
fetch(url)
.then(response => response.json())
.then(json => {
// Drop results from the previous query, keeping the selected item
this.clearOptions();
callback(json);
}).catch(() => {
callback();