178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
from datetime import datetime
|
|
from decimal import Decimal
|
|
from math import floor
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db.models import Q
|
|
from django.shortcuts import render
|
|
|
|
from financeplanner.utils import format_date
|
|
|
|
|
|
def _get_relevant_balances(user, start, end):
|
|
balances = user.balances.filter(date__range=(start, end)).order_by("date")
|
|
return list(balances)
|
|
|
|
|
|
def _get_relevant_transactions(user, start, end):
|
|
transactions = user.transactions.filter(
|
|
Q(booking_date__range=(start, end)) |
|
|
Q(booking_date__lte=end, recurring_months__isnull=False, not_recurring_after__gte=start)
|
|
).order_by("booking_date")
|
|
return list(transactions)
|
|
|
|
|
|
def _calculate_actual_transactions(start, end, transaction_list):
|
|
result = []
|
|
for trans in transaction_list:
|
|
current_calc_trans = {}
|
|
if trans.recurring_months:
|
|
current_date = trans.booking_date
|
|
step = 1
|
|
limit = min(trans.not_recurring_after, end) if trans.not_recurring_after else end
|
|
while current_date < limit:
|
|
if current_date >= start:
|
|
result.append((
|
|
current_date,
|
|
f"{trans.subject} #{step}",
|
|
trans.amount,
|
|
))
|
|
current_date += relativedelta(months=trans.recurring_months)
|
|
step += 1
|
|
else:
|
|
current_calc_trans[trans.booking_date] = trans.amount
|
|
result.append((
|
|
trans.booking_date,
|
|
trans.subject,
|
|
trans.amount,
|
|
))
|
|
return result
|
|
|
|
|
|
def _calculate_daily_stats(start, end, balance_list, actual_transactions):
|
|
stats = {}
|
|
|
|
current_date = start
|
|
current_amount = None
|
|
while current_date < end:
|
|
current_stats = {
|
|
"balance": None,
|
|
"transactions": [],
|
|
"amount": None,
|
|
}
|
|
|
|
relevant_balances = [bal for bal in balance_list if bal.date == current_date]
|
|
if relevant_balances:
|
|
assert len(relevant_balances) == 1, f"balances should be unique for user and date, " \
|
|
f"but here are {relevant_balances}"
|
|
amount = relevant_balances[0].amount
|
|
current_stats["balance"] = relevant_balances[0].amount
|
|
current_amount = amount
|
|
|
|
relevant_transactions = [tra for tra in actual_transactions if tra[0] == current_date]
|
|
for tra in relevant_transactions:
|
|
subject, amount = tra[1], tra[2]
|
|
current_stats["transactions"].append({
|
|
"subject": subject,
|
|
"amount": amount,
|
|
})
|
|
if current_amount is not None:
|
|
current_amount -= amount
|
|
|
|
current_stats["amount"] = current_amount
|
|
stats[current_date] = current_stats
|
|
current_date += relativedelta(days=1)
|
|
|
|
return stats
|
|
|
|
|
|
def _floor_to_first_two_places(dec):
|
|
if dec < 100:
|
|
return 100
|
|
exp = floor(dec.log10()) - 1
|
|
factor = 10 ** exp
|
|
return factor * int(dec / factor)
|
|
|
|
|
|
def _calculate_scale(stats):
|
|
amounts = [stat["amount"] for stat in stats.values() if stat["amount"] is not None]
|
|
if not amounts:
|
|
return 100
|
|
max_amount = max(amounts)
|
|
return _floor_to_first_two_places(max_amount * Decimal("1.3"))
|
|
|
|
|
|
def _build_graph_data(start, end, balance_list, actual_transactions):
|
|
"""
|
|
result has the format:
|
|
{
|
|
<date>: {
|
|
"balance": <decimal>,
|
|
"transactions": [
|
|
{
|
|
"subject": <str>,
|
|
"amount": <decimal>,
|
|
},
|
|
...
|
|
],
|
|
"amount": <decimal>,
|
|
"div_percentage": <int>,
|
|
"div_opacity_class": <str>,
|
|
"div_color_class": <str>,
|
|
},
|
|
...
|
|
}
|
|
"""
|
|
today = datetime.now().date()
|
|
stats = _calculate_daily_stats(start, end, balance_list, actual_transactions)
|
|
amount_limit = _calculate_scale(stats)
|
|
for date in stats.keys():
|
|
amount = stats[date]["amount"]
|
|
if amount is None:
|
|
stats[date]["div_percentage"] = None
|
|
else:
|
|
percentage = int((amount / amount_limit) * 100)
|
|
stats[date]["div_percentage"] = percentage
|
|
if percentage >= 60:
|
|
stats[date]["div_color_class"] = "green"
|
|
elif percentage >= 45:
|
|
stats[date]["div_color_class"] = "olive"
|
|
elif percentage >= 30:
|
|
stats[date]["div_color_class"] = "yellow"
|
|
elif percentage >= 15:
|
|
stats[date]["div_color_class"] = "orange"
|
|
else:
|
|
stats[date]["div_color_class"] = "red"
|
|
|
|
if date == today:
|
|
stats[date]["div_opacity_class"] = "highlighted"
|
|
elif stats[date]["balance"] or stats[date]["transactions"]:
|
|
stats[date]["div_opacity_class"] = "strong"
|
|
else:
|
|
stats[date]["div_opacity_class"] = ""
|
|
|
|
return stats
|
|
|
|
|
|
@login_required
|
|
def index(request):
|
|
user = request.user
|
|
today = datetime.now().date()
|
|
range_start = today - relativedelta(days=30)
|
|
range_end = today + relativedelta(days=50)
|
|
balance_list = _get_relevant_balances(user, range_start, range_end)
|
|
transaction_list = _get_relevant_transactions(user, range_start, range_end)
|
|
actual_transactions = _calculate_actual_transactions(range_start, range_end, transaction_list)
|
|
graph_data = _build_graph_data(range_start, range_end, balance_list, actual_transactions)
|
|
context = {
|
|
"range_start": format_date(range_start),
|
|
"range_end": format_date(range_end),
|
|
"today": today,
|
|
"balance_list": balance_list,
|
|
"transaction_list": transaction_list,
|
|
"actual_transactions": actual_transactions,
|
|
"graph_data": graph_data,
|
|
}
|
|
return render(request, "financeplanner/index.html", context)
|