Compare commits

..

7 Commits

Author SHA1 Message Date
b4508a13db Anade una escala tipografica para los encabezados
base.css nunca ha definido tamanos de encabezado, asi que los ponia el
navegador: 32px el h1, 24px el h2 y 18,7px el h3. En una vista densa de
datos como el dashboard eso compite con las propias cifras, y ademas dejaba
los tamanos a merced de que un encabezado cambiara de nivel.

Fija 1.625rem / 1.25rem / 1rem con font-weight 600. Los margenes van en rem
a proposito: los del navegador van en em, escalan con el tamano de fuente y
dejan huecos distintos entre secciones que deberian separarse igual.

Va en la zona de estilos base, antes de los componentes, para que las
reglas que ya ajustan encabezados concretos (.dashboard-context h1,
.settings-card h2, .home-section > h2, .info-panel-content h2) sigan
ganando por orden.

Afecta a toda la aplicacion. Lo que mas se nota es el titulo de pagina:
pierde 21px de aire por arriba, porque el margen superior del h1 pasa a 0 y
el padding de .content impedia que colapsara. Entre secciones de la home no
cambia nada, ahi manda el margin-bottom de 2.5rem de .home-section. Ningun
media query toca tamanos de fuente, asi que en movil la escala es la misma,
y el bloque no declara colores, asi que el tema oscuro no se entera.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:13:49 +02:00
3d47f8bd90 Usa un unico h1 por pagina en el resto de vistas
Cuatro paginas tenian su titulo en <h2> sin ningun <h1> encima: el
formulario de categoria, ajustes, el login y la ayuda de contrasena. Y el
listado de categorias si tenia <h1>, pero colgaba de el dos <h3>, saltandose
el nivel intermedio.

Sube los titulos de pagina a <h1> y los subtitulos al nivel que les toca:
las dos secciones del listado de categorias a <h2> y las tres tarjetas de
ajustes a <h2>. El selector .settings-card h3 pasa a h2 para seguirlas y
mantener su margin: 0 0 0.25rem, que es lo que evita que el titulo abra
hueco dentro de la tarjeta.

En login y ayuda de contrasena el <h1> ademas importa para el layout:
.auth-container tiene padding: 2rem, y el padding impide que colapse el
margen superior del primer hijo. Con la escala que viene despues, un <h2>
ahi sumaria su margen a ese padding y dejaria el titulo a 56px del borde;
el <h1> no lleva margen superior.

Verificado renderizando las 36 vistas: todas con un solo h1 y sin saltos
de nivel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:13:28 +02:00
beeda95d5d Corrige la jerarquia de encabezados del dashboard
El dashboard empezaba en <h2> y bajaba hasta <h4>, sin ningun <h1>. La
pagina no tenia titulo de documento y las secciones colgaban de un nivel
que no existia, asi que un lector de pantalla no podia recorrerla por
encabezados.

Sube cada nivel uno: el nombre de la cuenta (o "Todas las cuentas") pasa a
<h1>, las secciones a <h2> y el nombre de cada cuenta dentro de su tarjeta
de grafica a <h3>. El selector .dashboard-context h2 pasa a h1 para seguir
al elemento.

Solo cambian los niveles. Como base.css no definia tamanos de encabezado,
el aspecto lo daban los valores por defecto del navegador y esto si mueve
los tamanos; el commit de la escala tipografica los fija.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:13:03 +02:00
1cc13bae57 Borra JS muerto y la seccion comparativa duplicada del dashboard
El bloque <script> del formulario de periodo definia clearPeriodPreset y
tres consts, y no lo llamaba nadie: no hay ningun onchange ni listener que
lo use. Debajo habia ademas un segundo <script> entero comentado, de una
version anterior de los presets. Los ids hiddenPeriod, yearSelect y
monthSelect siguen en el formulario, que los usa para las etiquetas.

La seccion "Comparativa" del final mostraba la misma diferencia y el mismo
porcentaje que "Resumen comparativo" ya ensena arriba, y ambas salen bajo
el mismo {% if compare_enabled %}. Es un subconjunto estricto: la de arriba
anade los totales de los dos periodos. La de abajo ademas imprimia
kpi_difference_abs con un "+" condicional, asi que una bajada se veia como
"12,00 € (-13,3%)", sin signo en la cifra y con el porcentaje en negativo.
Se queda la de arriba, que imprime "-12,00 €".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:12:38 +02:00
f074005a7f 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 <noreply@anthropic.com>
2026-09-14 20:12:03 +02:00
b4c9f8507b Anade tests de regresion del dashboard
La vista dashboard concentra los presets de periodo, el filtro por cuenta,
la comparativa y las series de las graficas, y solo tenia cubierto el
filtro por ano. Antes de tocarla conviene fijar por escrito lo que hace
hoy, para que el refactor que viene no pueda cambiarlo sin que salte algo.

Cubre los tres presets (this_month, last_month, this_year), el filtro por
cuenta con su KPI de saldo, los valores por defecto de la comparativa
cuando no esta activada, la diferencia y la tendencia cuando si lo esta,
el chart_type segun se mire un mes o el ano entero, y el recorte a diez
categorias de la grafica de distribucion.

El test de la grafica de categorias comprueba tambien que by_category
sigue trayendo las doce: el recorte es solo de la grafica, no del listado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:58:31 +02:00
57d44dad8e Pasa chart_type al contexto del dashboard
La vista calculaba chart_type ("day" o "month") pero nunca lo metia en el
contexto, asi que la plantilla lo leia siempre vacio y el
{% if chart_type == 'day' %} del titulo de la grafica no se cumplia nunca.
El encabezado decia "Por Meses" tambien al mirar un mes suelto, cuando la
serie que se dibuja debajo es por dias.

Solo anade la clave que faltaba; el calculo ya estaba bien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:58:22 +02:00
9 changed files with 267 additions and 112 deletions

View File

@ -97,6 +97,31 @@ a.danger:visited,
color: var(--color-danger-accent); color: var(--color-danger-accent);
} }
/* Escala tipográfica. Los márgenes van en rem a propósito: los del navegador
van en em y escalan con el tamaño de fuente, dejando huecos irregulares
entre secciones. */
h1 {
font-size: 1.625rem;
font-weight: 600;
line-height: 1.2;
margin: 0 0 1rem;
}
h2 {
font-size: 1.25rem;
font-weight: 600;
line-height: 1.25;
margin: 1.5rem 0 0.75rem;
}
h3 {
font-size: 1rem;
font-weight: 600;
line-height: 1.3;
margin: 1rem 0 0.5rem;
}
/* ========================= */ /* ========================= */
/* Topbar / navigation */ /* Topbar / navigation */
/* ========================= */ /* ========================= */
@ -273,7 +298,7 @@ a.danger:visited,
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.dashboard-context h2 { .dashboard-context h1 {
margin: 0; margin: 0;
} }
@ -898,7 +923,7 @@ tr:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.settings-card h3 { .settings-card h2 {
margin: 0 0 0.25rem 0; margin: 0 0 0.25rem 0;
} }

View File

@ -5,7 +5,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Editar categoría</h2> <h1>Editar categoría</h1>
<form method="post" class="app-form"> <form method="post" class="app-form">
{% csrf_token %} {% csrf_token %}

View File

@ -8,7 +8,7 @@
<h1>Mis categorías</h1> <h1>Mis categorías</h1>
<h3>Nueva categoría</h3> <h2>Nueva categoría</h2>
<form method="post" class="app-form"> <form method="post" class="app-form">
{% csrf_token %} {% csrf_token %}
{% include "expenses/_form_fields.html" %} {% include "expenses/_form_fields.html" %}
@ -17,7 +17,7 @@
<hr> <hr>
<h3>Listado</h3> <h2>Listado</h2>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>

View File

@ -7,13 +7,13 @@
{% block content %} {% block content %}
<section class="dashboard-context"> <section class="dashboard-context">
<h2> <h1>
{% if selected_account_obj %} {% if selected_account_obj %}
{{ selected_account_obj.name }} {{ selected_account_obj.name }}
{% else %} {% else %}
Todas las cuentas Todas las cuentas
{% endif %} {% endif %}
</h2> </h1>
<p class="muted"> <p class="muted">
{% if selected_month %} {% if selected_month %}
@ -95,34 +95,6 @@
<button type="submit" class="btn btn-primary">Aplicar Filtros</button> <button type="submit" class="btn btn-primary">Aplicar Filtros</button>
</form> </form>
</section> </section>
<script>
const yearSelect = document.getElementById('yearSelect');
const monthSelect = document.getElementById('monthSelect');
const hiddenPeriod = document.getElementById('hiddenPeriod');
function clearPeriodPreset() {
if (hiddenPeriod) {
hiddenPeriod.value = ""
}
document.querySelectorAll('.preset').forEach(btn => btn.classList.remove('active'));
}
</script>
<!-- <script>
document.querySelectorAll('.dashboard-presets a').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
const period = link.dataset.period;
const isActive = link.classList.contains('active');
if (isActive) {
window.location.href = link.href;
} else {
window.location.href = `${link.href}?period=${period}`
}
});
});
</script> -->
<!-- ========================= --> <!-- ========================= -->
<!-- KPIs --> <!-- KPIs -->
@ -165,7 +137,7 @@
{% if compare_enabled %} {% if compare_enabled %}
<section class="comparison"> <section class="comparison">
<h3>Resumen comparativo</h3> <h2>Resumen comparativo</h2>
<p> <p>
Gastos periodo actual: <strong>{{ kpi_total|floatformat:2 }} €</strong><br> Gastos periodo actual: <strong>{{ kpi_total|floatformat:2 }} €</strong><br>
Gastos periodo anterior: <strong>{{ kpi_previous_total|floatformat:2 }} €</strong><br> Gastos periodo anterior: <strong>{{ kpi_previous_total|floatformat:2 }} €</strong><br>
@ -178,7 +150,7 @@
</small> </small>
</p> </p>
<h3>Desglose de cambios por categoría</h3> <h2>Desglose de cambios por categoría</h2>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
@ -212,21 +184,21 @@
<div class="charts-container"> <div class="charts-container">
<section> <section>
<h3>Evolución de Gastos ({% if chart_type == 'day' %}Por día {% else %} Por Meses{% endif %})</h3> <h2>Evolución de Gastos ({% if chart_type == 'day' %}Por día {% else %} Por Meses{% endif %})</h2>
<div class="chart-box"> <div class="chart-box">
<canvas id="mainChart"></canvas> <canvas id="mainChart"></canvas>
</div> </div>
</section> </section>
<section> <section>
<h3>Distribución por Categorías</h3> <h2>Distribución por Categorías</h2>
<div class="chart-box"> <div class="chart-box">
<canvas id="categoryChart"></canvas> <canvas id="categoryChart"></canvas>
</div> </div>
</section> </section>
<section> <section>
<h3>Gastos Recientes</h3> <h2>Gastos Recientes</h2>
<div class="table-wrap"> <div class="table-wrap">
<table class="table"> <table class="table">
<thead> <thead>
@ -290,11 +262,11 @@
}); });
</script> </script>
<h3>Evolución anual por cuenta ({{ selected_year }})</h3> <h2>Evolución anual por cuenta ({{ selected_year }})</h2>
<div class="dashboard-grid"> <div class="dashboard-grid">
{% for acc in accounts_charts %} {% for acc in accounts_charts %}
<div class="card card-chart"> <div class="card card-chart">
<h4>{{ acc.name }}</h4> <h3>{{ acc.name }}</h3>
<p><strong>Saldo actual:</strong> {{ acc.current_balance|floatformat:2 }}€</p> <p><strong>Saldo actual:</strong> {{ acc.current_balance|floatformat:2 }}€</p>
<div class="canvas-wrapper"> <div class="canvas-wrapper">
<canvas id="accountChart{{ acc.id }}"></canvas> <canvas id="accountChart{{ acc.id }}"></canvas>
@ -393,28 +365,11 @@
</script> </script>
{% if compare_enabled %}
<section class="comparison">
<h3>Comparativa</h3>
<p>
Diferencia:
<strong class="{% if kpi_trend == 'up' %}positive{% endif %}">
{% if kpi_trend == "up" %}+{% endif %}
{{ kpi_difference_abs|floatformat:2 }} €
</strong>
{% if kpi_percentage %}
({{ kpi_percentage|floatformat:1 }}%)
{% endif %}
</p>
</section>
{% endif %}
<!-- ========================= --> <!-- ========================= -->
<!-- Goals --> <!-- Goals -->
<!-- ========================= --> <!-- ========================= -->
<h3>Objetivos</h3> <h2>Objetivos</h2>
{% if goals %} {% if goals %}
<div class="goals-widget"> <div class="goals-widget">
{% for goal in goals %} {% for goal in goals %}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<h2>Iniciar sesión</h2> <h1>Iniciar sesión</h1>
<form method="post" class="app-form"> <form method="post" class="app-form">
{% csrf_token %} {% csrf_token %}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<h2>Recuperar contraseña</h2> <h1>Recuperar contraseña</h1>
<p> <p>
Esta aplicación actualmente no envía correos de recuperación automáticos. Esta aplicación actualmente no envía correos de recuperación automáticos.

View File

@ -5,23 +5,23 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Configuración</h2> <h1>Configuración</h1>
<p>Gestiona las categorías, etiquetas y objetivos de tu cuenta</p> <p>Gestiona las categorías, etiquetas y objetivos de tu cuenta</p>
<div class="settings-grid"> <div class="settings-grid">
<a class="settings-card" href="{% url 'category_list' %}"> <a class="settings-card" href="{% url 'category_list' %}">
<h3>Categorías</h3> <h2>Categorías</h2>
<p>Organiza tus gastos por tipo.</p> <p>Organiza tus gastos por tipo.</p>
</a> </a>
<a class="settings-card" href="{% url 'tag_list' %}"> <a class="settings-card" href="{% url 'tag_list' %}">
<h3>Etiquetas</h3> <h2>Etiquetas</h2>
<p>Añade etiquetas libres a tus gastos.</p> <p>Añade etiquetas libres a tus gastos.</p>
</a> </a>
<a class="settings-card" href="{% url 'goal_list' %}"> <a class="settings-card" href="{% url 'goal_list' %}">
<h3>Objetivos</h3> <h2>Objetivos</h2>
<p>Define y controla tus metas de gasto.</p> <p>Define y controla tus metas de gasto.</p>
</a> </a>
</div> </div>

View File

@ -2,7 +2,7 @@ import pytest
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from django.urls import reverse from django.urls import reverse
from expenses.models import Expense, Category from expenses.models import Account, Expense, Category, Income
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -80,3 +80,146 @@ def test_dashboard_filters_by_year(auth_client, user, account, category):
assert len(chart_data) == 12 assert len(chart_data) == 12
assert chart_data[4] == 20.0 # May = month 5 -> index 4 assert chart_data[4] == 20.0 # May = month 5 -> index 4
assert sum(chart_data) == 20.0 # only the 2024 expense contributes assert sum(chart_data) == 20.0 # only the 2024 expense contributes
def test_dashboard_preset_this_month(auth_client):
today = date.today()
response = auth_client.get(reverse('dashboard'), {'period': 'this_month'})
assert response.context['selected_year'] == today.year
assert response.context['selected_month'] == today.month
def test_dashboard_preset_last_month(auth_client):
today = date.today()
expected_month = today.month - 1 or 12
expected_year = today.year if today.month > 1 else today.year - 1
response = auth_client.get(reverse('dashboard'), {'period': 'last_month'})
assert response.context['selected_year'] == expected_year
assert response.context['selected_month'] == expected_month
def test_dashboard_preset_this_year_has_no_month(auth_client):
today = date.today()
response = auth_client.get(reverse('dashboard'), {'period': 'this_year'})
assert response.context['selected_year'] == today.year
assert response.context['selected_month'] is None
def test_dashboard_filters_by_account(auth_client, user, account, category):
other = Account.objects.create(
owner=user, name="Otra", initial_balance=Decimal("100"), active=True
)
Expense.objects.create(
owner=user, account=account, category=category,
amount=Decimal("10"), date=date(2024, 3, 1),
)
Expense.objects.create(
owner=user, account=other, category=category,
amount=Decimal("25"), date=date(2024, 3, 1),
)
response = auth_client.get(
reverse('dashboard'),
{'year': 2024, 'account': other.id},
)
assert response.context['kpi_total'] == Decimal('25')
assert response.context['kpi_count'] == 1
assert response.context['kpi_balance'] == Decimal('75') # 100 - 25
def test_dashboard_without_compare_keeps_comparison_defaults(auth_client):
response = auth_client.get(reverse('dashboard'), {'year': 2024})
assert response.context['compare_enabled'] is False
assert response.context['kpi_previous_total'] == 0
assert response.context['kpi_difference'] == 0
assert response.context['kpi_percentage'] == 0
assert response.context['kpi_trend'] == 'equal'
assert response.context['category_comparison'] == []
def test_dashboard_compare_fills_difference_and_trend(auth_client, user, account, category):
Expense.objects.create(
owner=user, account=account, category=category,
amount=Decimal("20"), date=date(2024, 2, 15),
)
Expense.objects.create(
owner=user, account=account, category=category,
amount=Decimal("30"), date=date(2024, 3, 10),
)
response = auth_client.get(
reverse('dashboard'),
{'year': 2024, 'month': 3, 'compare': '1'},
)
assert response.context['compare_enabled'] is True
assert response.context['kpi_previous_total'] == Decimal('20')
assert response.context['kpi_difference'] == Decimal('10')
assert response.context['kpi_trend'] == 'up'
assert float(response.context['kpi_percentage']) == 50.0
rows = {row['category']: row for row in response.context['category_comparison']}
assert rows[category.name]['current'] == 30.0
assert rows[category.name]['previous'] == 20.0
assert rows[category.name]['difference'] == 10.0
def test_dashboard_chart_type_is_day_when_a_month_is_selected(auth_client):
response = auth_client.get(reverse('dashboard'), {'year': 2024, 'month': 3})
assert response.context['chart_type'] == 'day'
def test_dashboard_chart_type_is_month_for_the_whole_year(auth_client):
response = auth_client.get(reverse('dashboard'), {'year': 2024})
assert response.context['chart_type'] == 'month'
def test_dashboard_category_chart_keeps_only_top_ten(auth_client, user, account):
for i in range(12):
cat = Category.objects.create(name=f"Cat {i:02d}", owner=user)
Expense.objects.create(
owner=user, account=account, category=cat,
amount=Decimal(str(i + 1)), date=date(2024, 6, 1),
)
response = auth_client.get(reverse('dashboard'), {'year': 2024})
assert len(response.context['by_category_chart']) == 10
assert len(list(response.context['by_category'])) == 12
def test_dashboard_kpi_balance_matches_sum_of_current_balances(auth_client, user, account, category):
savings = Account.objects.create(
owner=user, name="Ahorro", initial_balance=Decimal("500"), active=True
)
Account.objects.create(
owner=user, name="Cerrada", initial_balance=Decimal("999"), active=False
)
Expense.objects.create(
owner=user, account=account, category=category,
amount=Decimal("40"), date=date(2024, 4, 1),
)
Income.objects.create(
owner=user, account=savings, name="Nomina",
amount=Decimal("60"), date=date(2024, 4, 2),
)
response = auth_client.get(reverse('dashboard'), {'year': 2024})
expected = sum(
acc.current_balance()
for acc in Account.objects.filter(owner=user, active=True)
)
assert expected == Decimal('520') # (0 - 40) + (500 + 60), sin la inactiva
assert response.context['kpi_balance'] == expected

View File

@ -112,6 +112,67 @@ def _account_balances(accounts):
return account_balances, negative_accounts, total_balance 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 @login_required
def home(request): def home(request):
today = date.today() today = date.today()
@ -407,33 +468,26 @@ def expense_delete(request, pk):
def dashboard(request): def dashboard(request):
today = date.today() today = date.today()
period = request.GET.get("period", "")
account_id = _get_int(request.GET.get("account")) account_id = _get_int(request.GET.get("account"))
compare_enabled = request.GET.get("compare") == "1" compare_enabled = request.GET.get("compare") == "1"
# Time presets # Time presets
if period == "this_month": period, selected_year, selected_month = _resolve_period(request, today)
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
# Accounts # Accounts
accounts = Account.objects.filter(owner=request.user, active=True) accounts = list(Account.objects.filter(owner=request.user, active=True))
selected_account_obj = None selected_account_obj = None
if account_id: 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 # 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: if selected_account_obj:
kpi_balance = selected_account_obj.current_balance() kpi_balance = balance_by_account[selected_account_obj.id]
else: else:
kpi_balance = sum(account.current_balance() for account in accounts) kpi_balance = total_balance
# Filter by base expenses # Filter by base expenses
expenses = Expense.objects.filter(owner=request.user) expenses = Expense.objects.filter(owner=request.user)
@ -561,32 +615,9 @@ def dashboard(request):
}) })
# Anual evolution by accounts # Anual evolution by accounts
accounts_charts = [] accounts_charts = _build_account_charts(
for acc in accounts: accounts, balance_by_account, selected_year, today
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())
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
goals = Goal.objects.filter(owner=request.user) goals = Goal.objects.filter(owner=request.user)
@ -605,6 +636,7 @@ def dashboard(request):
"by_category_chart": by_category_chart, "by_category_chart": by_category_chart,
"chart_labels": chart_labels, "chart_labels": chart_labels,
"chart_data": chart_totals, "chart_data": chart_totals,
"chart_type": chart_type,
"year_list": year_list, "year_list": year_list,
"months": list(range(1, 13)), "months": list(range(1, 13)),
"selected_year": selected_year, "selected_year": selected_year,