From f074005a7f23e27424e02fcefffb748f3cf65bd2 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Mon, 14 Sep 2026 20:12:03 +0200 Subject: [PATCH] Extrae helpers del dashboard y elimina el N+1 de saldos La vista pasaba de 226 lineas y mezclaba tres cosas distintas: traducir los parametros de la URL a un periodo, calcular los KPIs y montar las series de las graficas por cuenta. Saca las dos que son autonomas a _resolve_period y _build_account_charts y deja dashboard en 170. De paso quita el N+1: la vista llamaba a current_balance() una vez por cuenta para el KPI y otra vez por cuenta dentro del bucle de graficas, y cada llamada son dos queries. Ahora los saldos salen de _account_balances, que los calcula con dos agregados, y se reparten por un dict indexado por id. Medido con CaptureQueriesContext: con 1/3/6 cuentas se pasa de 25/53/95 queries a 21/37/61. accounts deja de ser un QuerySet y pasa a lista, porque ahora se recorre mas de una vez, y la cuenta seleccionada se busca en memoria en vez de con otra query. Lo que sigue costando una query por cuenta es monthly_balance(), que no se toca aqui. Co-Authored-By: Claude Opus 5 --- expenses_manager/expenses/views.py | 121 ++++++++++++++++++----------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/expenses_manager/expenses/views.py b/expenses_manager/expenses/views.py index 772ab13..4ad3976 100644 --- a/expenses_manager/expenses/views.py +++ b/expenses_manager/expenses/views.py @@ -112,6 +112,67 @@ def _account_balances(accounts): return account_balances, negative_accounts, total_balance +def _resolve_period(request, today): + """Traduce los parámetros de la URL en el periodo mirado por el dashboard. + + Devuelve (period, selected_year, selected_month), donde selected_month es + None cuando se está mirando el año completo.""" + period = request.GET.get("period", "") + + if period == "this_month": + return period, today.year, today.month + + if period == "last_month": + selected_year, selected_month = sub_months(today.year, today.month, 1) + return period, selected_year, selected_month + + if period == "this_year": + return period, today.year, None + + selected_year = _get_int(request.GET.get("year")) or today.year + selected_month = _get_int(request.GET.get("month")) or None + return period, selected_year, selected_month + + +def _build_account_charts(accounts, balance_by_account, selected_year, today): + """Serie mensual de saldo de cada cuenta para las gráficas del dashboard. + + En el año en curso la serie se corta en el mes actual y ese último punto + se sustituye por el saldo real de la cuenta, que llega ya calculado en + balance_by_account para no repetir current_balance() por cuenta.""" + accounts_charts = [] + + for acc in accounts: + try: + monthly_data = acc.monthly_balance(selected_year) + m_balance = [float(row["balance"]) for row in monthly_data] + except Exception: + logger.exception( + "Error calculando monthly_balance para la cuenta %s (año %s)", + acc.id, selected_year + ) + m_balance = [0] * 12 + + current_balance = balance_by_account[acc.id] + + if selected_year == today.year: + current_month_index = today.month - 1 + + if current_month_index < len(m_balance): + m_balance[current_month_index] = float(current_balance) + + m_balance = m_balance[:today.month] + + accounts_charts.append({ + "id": acc.id, + "name": acc.name, + "data": m_balance, + "current_balance": current_balance, + }) + + return accounts_charts + + @login_required def home(request): today = date.today() @@ -407,34 +468,27 @@ def expense_delete(request, pk): def dashboard(request): today = date.today() - period = request.GET.get("period", "") account_id = _get_int(request.GET.get("account")) compare_enabled = request.GET.get("compare") == "1" - + # Time presets - if period == "this_month": - selected_year, selected_month = today.year, today.month - elif period == "last_month": - selected_year, selected_month = sub_months(today.year, today.month, 1) - elif period == "this_year": - selected_year, selected_month = today.year, None - else: - selected_year = _get_int(request.GET.get("year")) or _get_int(today.year) - selected_month = _get_int(request.GET.get("month")) - selected_month = _get_int(selected_month) if selected_month else None - + period, selected_year, selected_month = _resolve_period(request, today) + # Accounts - accounts = Account.objects.filter(owner=request.user, active=True) + accounts = list(Account.objects.filter(owner=request.user, active=True)) selected_account_obj = None if account_id: - selected_account_obj = accounts.filter(id=account_id).first() - + selected_account_obj = next((a for a in accounts if a.id == account_id), None) + # Calculate the KPI + account_balances, _, total_balance = _account_balances(accounts) + balance_by_account = {row["account"].id: row["balance"] for row in account_balances} + if selected_account_obj: - kpi_balance = selected_account_obj.current_balance() + kpi_balance = balance_by_account[selected_account_obj.id] else: - kpi_balance = sum(account.current_balance() for account in accounts) - + kpi_balance = total_balance + # Filter by base expenses expenses = Expense.objects.filter(owner=request.user) if account_id: @@ -561,33 +615,10 @@ def dashboard(request): }) # Anual evolution by accounts - accounts_charts = [] - for acc in accounts: - try: - monthly_data = acc.monthly_balance(selected_year) - m_balance = [float(row["balance"]) for row in monthly_data] - except Exception: - logger.exception( - "Error calculando monthly_balance para la cuenta %s (año %s)", - acc.id, selected_year - ) - m_balance = [0] * 12 - - if selected_year == today.year: - current_month_index = today.month - 1 - - if current_month_index < len(m_balance): - m_balance[current_month_index] = float(acc.current_balance()) + accounts_charts = _build_account_charts( + accounts, balance_by_account, selected_year, today + ) - m_balance = m_balance[:today.month] - - accounts_charts.append({ - "id": acc.id, - "name": acc.name, - "data": m_balance, - "current_balance": acc.current_balance(), - }) - # Goals goals = Goal.objects.filter(owner=request.user)