import calendar from datetime import date, timedelta from decimal import Decimal from django.db.models import Sum from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.utils import timezone from app.expenses.models import Category, Expense, FixedExpenseConcept from app.yearly.models import PlannedExpense from .models import Income, MonthlyFixedExpense, MonthlyGoal, MonthlyNote def _offset_month(year, month, offset): """Shift year/month by offset months.""" total = (year * 12 + month - 1) + offset return total // 12, total % 12 + 1 def _get_month_weeks(year, month): """Return list of (start_date, end_date) for each week of the month.""" first_day = date(year, month, 1) last_day = date(year, month, calendar.monthrange(year, month)[1]) weeks = [] # Start from the Monday of the first week current = first_day - timedelta(days=first_day.weekday()) while current <= last_day: week_start = max(current, first_day) week_end = min(current + timedelta(days=6), last_day) weeks.append((week_start, week_end)) current += timedelta(days=7) return weeks def _build_month_end_data(year, month): """Build weekly expense breakdown for month end section.""" weeks = _get_month_weeks(year, month) expenses = Expense.objects.filter( created_at__date__year=year, created_at__date__month=month, ).select_related("category", "subcategory") # Weekly totals weekly_totals = [] for start, end in weeks: week_total = sum( e.amount for e in expenses if start <= e.created_at.date() <= end ) weekly_totals.append(week_total) monthly_expense_total = sum(weekly_totals) # Category breakdown by week categories = Category.objects.prefetch_related("subcategories").all() category_weeks = [] for cat in categories: cat_expenses = [e for e in expenses if e.category_id == cat.id] if not cat_expenses: continue cat_weekly = [] for start, end in weeks: wk_total = sum( e.amount for e in cat_expenses if start <= e.created_at.date() <= end ) cat_weekly.append(wk_total) cat_total = sum(cat_weekly) category_weeks.append( { "category": cat, "weekly": cat_weekly, "total": cat_total, } ) # Transpose: rows=weeks, cols=categories week_rows = [] for i in range(len(weeks)): row = [] for cw in category_weeks: row.append(cw["weekly"][i]) week_rows.append({"week_num": i + 1, "values": row}) return { "weeks": weeks, "week_count": len(weeks), "weekly_totals": weekly_totals, "monthly_expense_total": monthly_expense_total, "category_weeks": category_weeks, "week_rows": week_rows, } @login_required def month_view(request): today = timezone.localdate() offset = int(request.GET.get("offset", 0)) year, month = _offset_month(today.year, today.month, offset) current_month = date(year, month, 1) is_current_month = offset == 0 note, _ = MonthlyNote.objects.get_or_create(year=year, month=month) # Income incomes = Income.objects.filter(date__year=year, date__month=month) total = incomes.aggregate(t=Sum("amount"))["t"] or Decimal("0") # Clone months for income available_income_months = ( Income.objects.values_list("date__year", "date__month") .distinct() .order_by("-date__year", "-date__month") ) clone_months = [ (y, m) for y, m in available_income_months if not (y == year and m == month) ] # Fixed expenses (bulk create missing, then fetch all) concepts = FixedExpenseConcept.objects.all() existing_ids = set( MonthlyFixedExpense.objects.filter(year=year, month=month).values_list( "concept_id", flat=True ) ) to_create = [ MonthlyFixedExpense(year=year, month=month, concept=c) for c in concepts if c.id not in existing_ids ] if to_create: MonthlyFixedExpense.objects.bulk_create(to_create) fe_entries = MonthlyFixedExpense.objects.filter( year=year, month=month ).select_related("concept") fe_total = fe_entries.aggregate(t=Sum("amount"))["t"] or Decimal("0") # Clone months for fixed expenses available_fe_months = ( MonthlyFixedExpense.objects.filter(amount__gt=0) .values_list("year", "month") .distinct() .order_by("-year", "-month") ) clone_fe_months = [ (y, m) for y, m in available_fe_months if not (y == year and m == month) ] # Default previous month prev_year, prev_month = _offset_month(year, month, -1) # Planned expenses for this month planned_expenses = PlannedExpense.objects.filter(year=year, month=month) planned_total = sum(pe.amount for pe in planned_expenses) # Budget (income - fixed - savings - planned) budget = total - fe_total - note.savings_target - planned_total # Month end data month_end = _build_month_end_data(year, month) # Include planned expenses in month end total month_end["monthly_expense_total"] += planned_total context = { "note": note, "current_month": current_month, "incomes": incomes, "total": total, "year": year, "month": month, "today": today.isoformat(), "clone_months": clone_months, "clone_fe_months": clone_fe_months, "prev_year": prev_year, "prev_month": prev_month, "fe_entries": fe_entries, "fe_total": fe_total, "savings_target": note.savings_target, "budget": budget, "goals": MonthlyGoal.objects.filter( year=year, month=month, kind=MonthlyGoal.GOAL ), "promises": MonthlyGoal.objects.filter( year=year, month=month, kind=MonthlyGoal.PROMISE ), "offset": offset, "prev_offset": offset - 1, "next_offset": offset + 1, "is_current_month": is_current_month, # Month end "month_end": month_end, "savings_result": budget - month_end["monthly_expense_total"], # Planned expenses "planned_expenses": planned_expenses, "planned_total": planned_total, } return render(request, "pages/monthly/month.html", context)