- 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)
178 lines
4.8 KiB
Python
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)
|