Modificaciones en filtrado y creada opción para marcar que un gasto/ingreso es un traspaso de cuentas #34
@ -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