Aplica el rango de fechas al dashboard
Un rango que solo funciona en el listado obliga a cambiar de pantalla para responder "cuanto gaste en ese viaje". El dashboard acepta ahora los mismos date_from y date_to, con la misma precedencia sobre ano y mes. Dos decisiones que no son obvias: La comparativa se desactiva cuando hay rango. Un rango arbitrario no tiene un "periodo anterior" evidente, y elegir uno por el usuario daria un porcentaje que parece significativo sin serlo. Se distingue entre pedirla y poder aplicarla (compare_requested / compare_enabled) para poder avisar de que se ha ignorado en vez de desmarcar la casilla en silencio. El eje del grafico lo decide la longitud del rango, no el mes: por debajo de 62 dias se agrupa por dia y por encima por mes, que es donde un grafico diario deja de leerse. El eje se rellena con ceros en los huecos, igual que ya hacia el resto, para que un periodo sin gastos no colapse la escala. La agrupacion diaria usa el propio campo date y no TruncDate, porque date ya es un DateField y TruncDate le aplicaria una conversion de zona horaria que en SQLite revienta si no esta instalada la base de datos de husos. La tarjeta de proyeccion a fin de mes se oculta con rango activo: proyectar a fin de mes desde un periodo arbitrario no significa nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c395c638e9
commit
836f3e8524
@ -16,7 +16,10 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
{% if selected_month %}
|
{% if range_active %}
|
||||||
|
Del {{ date_from|date:"d/m/Y"|default:"principio" }}
|
||||||
|
al {{ date_to|date:"d/m/Y"|default:"hoy" }}
|
||||||
|
{% elif selected_month %}
|
||||||
{{ selected_month }}/{{ selected_year}}
|
{{ selected_month }}/{{ selected_year}}
|
||||||
{% else %}
|
{% else %}
|
||||||
Año {{ selected_year }}
|
Año {{ selected_year }}
|
||||||
@ -66,6 +69,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{% include "expenses/_date_range.html" %}
|
||||||
|
|
||||||
{% if not period %}
|
{% if not period %}
|
||||||
<label class="sr-only" for="yearSelect">Año</label>
|
<label class="sr-only" for="yearSelect">Año</label>
|
||||||
<select name="year" id="yearSelect">
|
<select name="year" id="yearSelect">
|
||||||
@ -94,6 +99,15 @@
|
|||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Aplicar Filtros</button>
|
<button type="submit" class="btn btn-primary">Aplicar Filtros</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if range_active %}
|
||||||
|
<p class="range-notice">
|
||||||
|
Mostrando del {{ date_from|date:"d/m/Y"|default:"principio" }} al
|
||||||
|
{{ date_to|date:"d/m/Y"|default:"hoy" }}. Los filtros de año, mes
|
||||||
|
{% if compare_suppressed %}y la comparativa {% endif %}no se aplican.
|
||||||
|
<a href="{% url 'dashboard' %}">Quitar el rango</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
@ -126,7 +140,7 @@
|
|||||||
<span class="kpi-value">{{ daily_average|floatformat:2 }}€ / día</span>
|
<span class="kpi-value">{{ daily_average|floatformat:2 }}€ / día</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if selected_month == today.month and selected_year == today.year %}
|
{% if selected_month == today.month and selected_year == today.year and not range_active %}
|
||||||
<div class="kpi-card" style="border-left: 4px solid var(--color-warning-accent);">
|
<div class="kpi-card" style="border-left: 4px solid var(--color-warning-accent);">
|
||||||
<span class="kpi-label">Proyección fin de mes</span>
|
<span class="kpi-label">Proyección fin de mes</span>
|
||||||
<span class="kpi-value">{{ projected_end_of_month|floatformat:2 }}€</span>
|
<span class="kpi-value">{{ projected_end_of_month|floatformat:2 }}€</span>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import calendar
|
import calendar
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import Account, Category, Expense, FuelEntry, Tag, Income, Goal
|
from .models import Account, Category, Expense, FuelEntry, Tag, Income, Goal
|
||||||
@ -15,8 +15,14 @@ from .forms import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Sum, Count, ProtectedError
|
from django.db.models import Sum, Count, Min, ProtectedError
|
||||||
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay, Lower
|
from django.db.models.functions import (
|
||||||
|
ExtractMonth,
|
||||||
|
ExtractYear,
|
||||||
|
ExtractDay,
|
||||||
|
Lower,
|
||||||
|
TruncMonth,
|
||||||
|
)
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
@ -544,11 +550,19 @@ def dashboard(request):
|
|||||||
today = date.today()
|
today = date.today()
|
||||||
|
|
||||||
account_id = _get_int(request.GET.get("account"))
|
account_id = _get_int(request.GET.get("account"))
|
||||||
compare_enabled = request.GET.get("compare") == "1"
|
compare_requested = request.GET.get("compare") == "1"
|
||||||
|
|
||||||
# Time presets
|
# Time presets
|
||||||
period, selected_year, selected_month = _resolve_period(request, today)
|
period, selected_year, selected_month = _resolve_period(request, today)
|
||||||
|
|
||||||
|
# Rango de fechas libre. Si está activo manda sobre año y mes, y además
|
||||||
|
# desactiva la comparativa: no hay un "periodo anterior" obvio de un rango
|
||||||
|
# arbitrario. compare_suppressed sirve para decírselo al usuario.
|
||||||
|
date_from, date_to = _resolve_date_range(request)
|
||||||
|
range_active = bool(date_from or date_to)
|
||||||
|
compare_enabled = compare_requested and not range_active
|
||||||
|
compare_suppressed = compare_requested and range_active
|
||||||
|
|
||||||
# Accounts
|
# Accounts
|
||||||
accounts = list(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
|
||||||
@ -571,9 +585,17 @@ def dashboard(request):
|
|||||||
if account_id:
|
if account_id:
|
||||||
expenses = expenses.filter(account_id=account_id)
|
expenses = expenses.filter(account_id=account_id)
|
||||||
|
|
||||||
expenses_filtered = expenses.filter(date__year=selected_year)
|
if range_active:
|
||||||
if selected_month:
|
expenses_filtered = expenses
|
||||||
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
|
if date_from:
|
||||||
|
expenses_filtered = expenses_filtered.filter(date__gte=date_from)
|
||||||
|
if date_to:
|
||||||
|
expenses_filtered = expenses_filtered.filter(date__lte=date_to)
|
||||||
|
else:
|
||||||
|
expenses_filtered = expenses.filter(date__year=selected_year)
|
||||||
|
if selected_month:
|
||||||
|
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
|
||||||
|
|
||||||
|
|
||||||
# Basic KPIs
|
# Basic KPIs
|
||||||
total_amount = expenses_filtered.aggregate(total=Sum("amount"))["total"] or 0
|
total_amount = expenses_filtered.aggregate(total=Sum("amount"))["total"] or 0
|
||||||
@ -605,7 +627,60 @@ def dashboard(request):
|
|||||||
#Graphic
|
#Graphic
|
||||||
daily_average = 0
|
daily_average = 0
|
||||||
projected_end_of_month = 0
|
projected_end_of_month = 0
|
||||||
if selected_month:
|
if range_active:
|
||||||
|
# Con rango libre el eje no lo decide el mes sino la longitud del rango.
|
||||||
|
# Si falta un extremo se acota para poder medirlo: por abajo con el gasto
|
||||||
|
# más antiguo del conjunto, por arriba con hoy.
|
||||||
|
span_from = date_from or expenses_filtered.aggregate(first=Min("date"))["first"]
|
||||||
|
span_to = date_to or today
|
||||||
|
|
||||||
|
if span_from is None:
|
||||||
|
# Sin gastos y sin fecha inicial no hay longitud que medir.
|
||||||
|
chart_labels = []
|
||||||
|
chart_totals = []
|
||||||
|
chart_type = "month"
|
||||||
|
else:
|
||||||
|
span_days = (span_to - span_from).days + 1
|
||||||
|
daily_average = total_amount / span_days if span_days > 0 else 0
|
||||||
|
# projected_end_of_month se queda en 0: proyectar a fin de mes desde
|
||||||
|
# un rango arbitrario no significa nada.
|
||||||
|
|
||||||
|
if span_days <= RANGE_DAY_CHART_MAX_DAYS:
|
||||||
|
# Agrupar por el propio campo, no con TruncDate: "date" ya es un
|
||||||
|
# DateField y TruncDate le aplicaría una conversión de zona
|
||||||
|
# horaria que en SQLite falla si no está la base de datos tz.
|
||||||
|
by_day_qs = expenses_filtered.values("date").annotate(
|
||||||
|
total=Sum("amount")
|
||||||
|
)
|
||||||
|
day_totals = {row["date"]: float(row["total"]) for row in by_day_qs}
|
||||||
|
|
||||||
|
axis = [span_from + timedelta(days=i) for i in range(span_days)]
|
||||||
|
chart_labels = [d.strftime("%d/%m") for d in axis]
|
||||||
|
chart_totals = [day_totals.get(d, 0) for d in axis]
|
||||||
|
chart_type = "day"
|
||||||
|
else:
|
||||||
|
by_month_qs = (
|
||||||
|
expenses_filtered.annotate(bucket=TruncMonth("date"))
|
||||||
|
.values("bucket")
|
||||||
|
.annotate(total=Sum("amount"))
|
||||||
|
)
|
||||||
|
month_totals = {
|
||||||
|
(row["bucket"].year, row["bucket"].month): float(row["total"])
|
||||||
|
for row in by_month_qs
|
||||||
|
}
|
||||||
|
|
||||||
|
axis = []
|
||||||
|
cursor = (span_from.year, span_from.month)
|
||||||
|
last = (span_to.year, span_to.month)
|
||||||
|
while cursor <= last:
|
||||||
|
axis.append(cursor)
|
||||||
|
y, m = cursor
|
||||||
|
cursor = (y + 1, 1) if m == 12 else (y, m + 1)
|
||||||
|
|
||||||
|
chart_labels = [f"{m:02d}/{y}" for y, m in axis]
|
||||||
|
chart_totals = [month_totals.get(key, 0) for key in axis]
|
||||||
|
chart_type = "month"
|
||||||
|
elif selected_month:
|
||||||
by_day_qs = (
|
by_day_qs = (
|
||||||
expenses_filtered.annotate(day=ExtractDay("date"))
|
expenses_filtered.annotate(day=ExtractDay("date"))
|
||||||
.values("day")
|
.values("day")
|
||||||
@ -718,6 +793,10 @@ def dashboard(request):
|
|||||||
"months": list(range(1, 13)),
|
"months": list(range(1, 13)),
|
||||||
"selected_year": selected_year,
|
"selected_year": selected_year,
|
||||||
"selected_month": selected_month,
|
"selected_month": selected_month,
|
||||||
|
"date_from": date_from,
|
||||||
|
"date_to": date_to,
|
||||||
|
"range_active": range_active,
|
||||||
|
"compare_suppressed": compare_suppressed,
|
||||||
"kpi_total": total_amount,
|
"kpi_total": total_amount,
|
||||||
"kpi_count": expense_count,
|
"kpi_count": expense_count,
|
||||||
"kpi_categories": category_count,
|
"kpi_categories": category_count,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user