Modificaciones en filtrado y creada opción para marcar que un gasto/ingreso es un traspaso de cuentas #34

Merged
jkuijperm merged 18 commits from dev into main 2026-09-16 07:50:18 +00:00
2 changed files with 104 additions and 11 deletions
Showing only changes of commit 836f3e8524 - Show all commits

View File

@ -16,7 +16,10 @@
</h1>
<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}}
{% else %}
Año {{ selected_year }}
@ -66,6 +69,8 @@
{% endfor %}
</select>
{% include "expenses/_date_range.html" %}
{% if not period %}
<label class="sr-only" for="yearSelect">Año</label>
<select name="year" id="yearSelect">
@ -94,6 +99,15 @@
<button type="submit" class="btn btn-primary">Aplicar Filtros</button>
</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>
<!-- ========================= -->
@ -126,7 +140,7 @@
<span class="kpi-value">{{ daily_average|floatformat:2 }}€ / día</span>
</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);">
<span class="kpi-label">Proyección fin de mes</span>
<span class="kpi-value">{{ projected_end_of_month|floatformat:2 }}€</span>

View File

@ -1,6 +1,6 @@
import logging
import calendar
from datetime import date, datetime
from datetime import date, datetime, timedelta
from decimal import Decimal
from django.contrib import messages
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.db.models import Sum, Count, ProtectedError
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay, Lower
from django.db.models import Sum, Count, Min, ProtectedError
from django.db.models.functions import (
ExtractMonth,
ExtractYear,
ExtractDay,
Lower,
TruncMonth,
)
from django.contrib.auth.decorators import login_required
from django.utils.http import url_has_allowed_host_and_scheme
@ -544,11 +550,19 @@ def dashboard(request):
today = date.today()
account_id = _get_int(request.GET.get("account"))
compare_enabled = request.GET.get("compare") == "1"
compare_requested = request.GET.get("compare") == "1"
# Time presets
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 = list(Account.objects.filter(owner=request.user, active=True))
selected_account_obj = None
@ -571,10 +585,18 @@ def dashboard(request):
if account_id:
expenses = expenses.filter(account_id=account_id)
expenses_filtered = expenses.filter(date__year=selected_year)
if selected_month:
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
if range_active:
expenses_filtered = expenses
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
total_amount = expenses_filtered.aggregate(total=Sum("amount"))["total"] or 0
expense_count = expenses_filtered.count()
@ -605,7 +627,60 @@ def dashboard(request):
#Graphic
daily_average = 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 = (
expenses_filtered.annotate(day=ExtractDay("date"))
.values("day")
@ -718,6 +793,10 @@ def dashboard(request):
"months": list(range(1, 13)),
"selected_year": selected_year,
"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_count": expense_count,
"kpi_categories": category_count,