- Export all expenses as CSV from Settings (semicolon, UTF-8 BOM) - Budget calculation now includes planned expenses: income - fixed - savings - planned - Budget live update also accounts for planned expenses
199 lines
5.6 KiB
Python
199 lines
5.6 KiB
Python
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)
|