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 import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator 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.http import FileResponse, Http404, HttpResponse, JsonResponse, QueryDict
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.utils import timezone from django.utils import timezone
@@ -972,40 +972,20 @@ def transactions_trash_can_list(request):
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def get_recent_transactions(request, filter_type=None): def get_recent_transactions(request, filter_type=None):
"""Return the 100 most recent non-deleted transactions with optional search.""" """Return up to 100 non-deleted transactions, ordered like the monthly view, with optional search."""
# Get search term from query params
search_term = request.GET.get("q", "").strip() search_term = request.GET.get("q", "").strip()
today = timezone.localdate(timezone.now()) today = timezone.localdate(timezone.now())
yesterday = today - timezone.timedelta(days=1) yesterday = today - timezone.timedelta(days=1)
tomorrow = today + timezone.timedelta(days=1) tomorrow = today + timezone.timedelta(days=1)
# Base queryset with selected fields queryset = Transaction.objects.filter(deleted=False)
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")
)
if filter_type: if filter_type == "expenses":
if filter_type == "expenses": queryset = queryset.filter(type=Transaction.Type.EXPENSE)
queryset = queryset.filter(type=Transaction.Type.EXPENSE) elif filter_type == "income":
elif filter_type == "income": queryset = queryset.filter(type=Transaction.Type.INCOME)
queryset = queryset.filter(type=Transaction.Type.INCOME)
# Apply search if provided
if search_term: if search_term:
queryset = queryset.filter( queryset = queryset.filter(
Q(description__icontains=search_term) Q(description__icontains=search_term)
@@ -1015,4 +995,24 @@ def get_recent_transactions(request, filter_type=None):
| Q(category__name__icontains=search_term) | 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) 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) { 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) { config.load = function (query, callback) {
let url = element.dataset.load + '?q=' + encodeURIComponent(query); let url = element.dataset.load + '?q=' + encodeURIComponent(query);
fetch(url) fetch(url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
// Drop results from the previous query, keeping the selected item
this.clearOptions();
callback(json); callback(json);
}).catch(() => { }).catch(() => {
callback(); callback();