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)