Files
kakebo/app/yearly/views.py
Andros Fenollosa 7fa9f5db86 Code quality: fix N+1 queries, remove dead code, improve architecture
- Yearly view: replace N*12 queries with annotate/ExtractMonth (6 queries)
- Fixed expenses: bulk_create instead of get_or_create loop
- Use DB aggregate(Sum) instead of Python sum() for totals
- Remove 10 unused CRUD views, 13 URLs, 6 templates, 3 forms
- Fix overly broad Exception catch in save_expense
- Move update_budget to app/monthly/services.py (no cross-handler imports)
2026-03-29 11:14:07 +02:00

178 lines
4.8 KiB
Python

import json
from collections import defaultdict
from django.db.models import Sum
from django.db.models.functions import ExtractMonth
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from app.expenses.models import Expense
from app.monthly.models import Income, MonthlyFixedExpense
from .models import PlannedExpense
MONTH_LABELS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
MONTH_LABELS_FULL = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
@login_required
def year_view(request):
today = timezone.localdate()
year = int(request.GET.get("year", today.year))
is_current_year = year == today.year
# Income by month (single query)
income_rows = (
Income.objects.filter(date__year=year)
.annotate(m=ExtractMonth("date"))
.values("m")
.annotate(total=Sum("amount"))
)
income_map = {r["m"]: float(r["total"]) for r in income_rows}
income_by_month = [income_map.get(m, 0) for m in range(1, 13)]
# Fixed expenses by month (single query)
fe_rows = (
MonthlyFixedExpense.objects.filter(year=year)
.values("month")
.annotate(total=Sum("amount"))
)
fe_map = {r["month"]: float(r["total"]) for r in fe_rows}
fe_by_month = [fe_map.get(m, 0) for m in range(1, 13)]
# Variable expenses by month (single query)
var_rows = (
Expense.objects.filter(created_at__year=year)
.annotate(m=ExtractMonth("created_at"))
.values("m")
.annotate(total=Sum("amount"))
)
var_map = {r["m"]: float(r["total"]) for r in var_rows}
# Planned expenses by month (single query)
planned_rows = (
PlannedExpense.objects.filter(year=year)
.values("month")
.annotate(total=Sum("amount"))
)
planned_map = {r["month"]: float(r["total"]) for r in planned_rows}
var_expenses_by_month = [
var_map.get(m, 0) + planned_map.get(m, 0) for m in range(1, 13)
]
# Category breakdown (2 queries instead of N*12)
cat_annual = (
Expense.objects.filter(created_at__year=year)
.values("category__id", "category__name")
.annotate(total=Sum("amount"))
.filter(total__gt=0)
)
cat_monthly_rows = (
Expense.objects.filter(created_at__year=year)
.annotate(m=ExtractMonth("created_at"))
.values("category__id", "category__name", "m")
.annotate(total=Sum("amount"))
)
cat_monthly_map = defaultdict(lambda: [0.0] * 12)
for r in cat_monthly_rows:
cat_monthly_map[r["category__id"]][r["m"] - 1] = float(r["total"])
category_totals = []
category_monthly = []
for r in cat_annual:
cat_id = r["category__id"]
cat_name = r["category__name"]
category_totals.append({"name": cat_name, "total": float(r["total"])})
category_monthly.append({"name": cat_name, "data": cat_monthly_map[cat_id]})
total_expenses = sum(c["total"] for c in category_totals)
total_income = sum(income_by_month)
total_fe = sum(fe_by_month)
planned_total = sum(planned_map.values())
# Planned expenses for tables
planned_months_data = []
planned_items = PlannedExpense.objects.filter(year=year)
items_by_month = defaultdict(list)
for item in planned_items:
items_by_month[item.month].append(item)
for m in range(1, 13):
month_items = items_by_month.get(m, [])
planned_months_data.append(
{
"month": m,
"label": MONTH_LABELS_FULL[m - 1],
"items": month_items,
"total": sum(i.amount for i in month_items),
}
)
# Clone years for planned expenses
clone_pe_years = list(
PlannedExpense.objects.exclude(year=year)
.values_list("year", flat=True)
.distinct()
.order_by("-year")
)
# Available years for navigation
expense_years = set(
Expense.objects.values_list("created_at__year", flat=True).distinct()
)
income_years = set(Income.objects.values_list("date__year", flat=True).distinct())
planned_years = set(
PlannedExpense.objects.values_list("year", flat=True).distinct()
)
all_years = expense_years | income_years | planned_years | {today.year}
has_prev_year = (year - 1) in all_years
context = {
"year": year,
"prev_year": year - 1,
"next_year": year + 1,
"is_current_year": is_current_year,
"has_prev_year": has_prev_year,
"month_labels_json": json.dumps(MONTH_LABELS),
"income_by_month_json": json.dumps(income_by_month),
"var_expenses_by_month_json": json.dumps(var_expenses_by_month),
"fe_by_month_json": json.dumps(fe_by_month),
"category_totals": category_totals,
"category_totals_json": json.dumps(category_totals),
"category_monthly_json": json.dumps(category_monthly),
"total_expenses": total_expenses + planned_total,
"total_income": total_income,
"total_fe": total_fe,
"months_data": planned_months_data,
"grand_total": planned_total,
"clone_years": clone_pe_years,
}
return render(request, "pages/yearly/year.html", context)