adjust index view, template and tags to work with the new Statistics class
This commit is contained in:
@@ -19,29 +19,26 @@
|
||||
</p>
|
||||
|
||||
<div class="main-container flex-col-centering">
|
||||
{% if graph_data %}
|
||||
{% if daily_stats %}
|
||||
<div id="date-panel">-</div>
|
||||
<div id="graph">
|
||||
{% for date, stat in graph_data.items %}
|
||||
{% if stat.div_percentage is None %}
|
||||
{% for stat in daily_stats %}
|
||||
{% if stat.percentage is None %}
|
||||
<div class="graph-bar weak grey"
|
||||
onmouseover="showGraphBarDate('{{ date|date:"d.m.y" }}')"
|
||||
onmouseover="showGraphBarDate('{{ stat.date|date:"d.m.y" }}')"
|
||||
onmouseleave="clearGraphBarDate()">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="graph-bar {{ stat.div_opacity_class }} {{ stat.div_color_class }}"
|
||||
style="height: {{ stat.div_percentage }}%"
|
||||
onmouseover="showGraphBarDate('{{ date|date:"d.m.y" }}')"
|
||||
<div class="graph-bar {{ stat.opacity_class }} {{ stat.color_class }}"
|
||||
style="height: {{ stat.percentage }}%"
|
||||
onmouseover="showGraphBarDate('{{ stat.date|date:"d.m.y" }}')"
|
||||
onmouseleave="clearGraphBarDate()">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
No enough data from {{ range_start|date:"d.m.y" }} til
|
||||
{{ range_end|date:"d.m.y" }} to show a graph.
|
||||
</p>
|
||||
<p>No enough data from {{ start|date:"d.m.y" }} til {{ end|date:"d.m.y" }} to show a graph.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -51,15 +48,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>current average monthly income</td>
|
||||
<td>{{ analysis.avg_monthly_in|euro }}</td>
|
||||
<td>{{ avg_monthly_income|euro }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>current average monthly expenses</td>
|
||||
<td>{{ analysis.avg_monthly_out|euro }}</td>
|
||||
<td>{{ avg_monthly_expenses|euro }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>current average monthly result</td>
|
||||
<td>{{ analysis.avg_monthly_res|euro }}</td>
|
||||
<td>{{ avg_monthly_result|euro }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -84,7 +81,7 @@
|
||||
|
||||
<div class="main-container">
|
||||
<h2>Relevant Balances</h2>
|
||||
{% if balance_list %}
|
||||
{% if balances %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -93,7 +90,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for balance in balance_list %}
|
||||
{% for balance in balances %}
|
||||
<tr>
|
||||
<td>{{ balance.date|date:"d.m.y" }}</td>
|
||||
<td>{{ balance.amount|euro }}</td>
|
||||
@@ -108,7 +105,7 @@
|
||||
|
||||
<div class="main-container">
|
||||
<h2>Relevant Stored Transactions</h2>
|
||||
{% if transaction_list %}
|
||||
{% if transactions %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -120,7 +117,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in transaction_list %}
|
||||
{% for transaction in transactions %}
|
||||
<tr>
|
||||
<td>{{ transaction.subject }}</td>
|
||||
<td>{{ transaction.amount|euro }}</td>
|
||||
@@ -148,11 +145,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for date, subject, amount in actual_transactions %}
|
||||
{% for trans in actual_transactions %}
|
||||
<tr>
|
||||
<td>{{ date|date:"d.m.y" }}</td>
|
||||
<td>{{ subject }}</td>
|
||||
<td>{{ amount|euro }}</td>
|
||||
<td>{{ trans.date|date:"d.m.y" }}</td>
|
||||
<td>{{ trans.subject }}</td>
|
||||
<td>{{ trans.amount|euro }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django import template
|
||||
|
||||
from financeplanner.utils import format_price
|
||||
@@ -9,5 +7,4 @@ register = template.Library()
|
||||
|
||||
@register.filter(name="euro")
|
||||
def euro(value):
|
||||
assert isinstance(value, Decimal)
|
||||
return format_price(value) or "-"
|
||||
|
||||
@@ -1,228 +1,22 @@
|
||||
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 current_date
|
||||
|
||||
|
||||
def _get_relevant_balances(user, range_start, range_end):
|
||||
balance_list = []
|
||||
balances_until_start = user.balances.filter(date__lte=range_start)
|
||||
if balances_until_start.exists():
|
||||
balance_list = [balances_until_start.latest("date")]
|
||||
other_balances = user.balances.filter(date__gt=range_start, date__lte=range_end).order_by("date")
|
||||
return balance_list + list(other_balances)
|
||||
|
||||
|
||||
def _get_relevant_transactions(user, calculation_start, range_end):
|
||||
one_time_in_range = Q(
|
||||
booking_date__gte=calculation_start,
|
||||
booking_date__lte=range_end,
|
||||
recurring_months__isnull=True,
|
||||
)
|
||||
endlessly_recurring = Q(
|
||||
booking_date__lte=range_end,
|
||||
recurring_months__isnull=False,
|
||||
not_recurring_after__isnull=True,
|
||||
)
|
||||
recurring_and_ending_in_range = Q(
|
||||
booking_date__lte=range_end,
|
||||
recurring_months__isnull=False,
|
||||
not_recurring_after__gte=calculation_start,
|
||||
)
|
||||
transactions = user.transactions.filter(
|
||||
one_time_in_range | endlessly_recurring | recurring_and_ending_in_range
|
||||
).order_by("booking_date")
|
||||
return list(transactions)
|
||||
|
||||
|
||||
def _calculate_actual_transactions(calculation_start, range_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, range_end) if trans.not_recurring_after else range_end
|
||||
while current_date < limit:
|
||||
if current_date >= calculation_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(calculation_start, range_end, balance_list, actual_transactions):
|
||||
stats = {}
|
||||
|
||||
current_date = calculation_start
|
||||
current_amount = None
|
||||
while current_date < range_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 _trim_stats(stats, range_start):
|
||||
return {date: stat for date, stat in stats.items() if date >= range_start}
|
||||
|
||||
|
||||
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(range_start, range_end, calculation_start, 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 = current_date()
|
||||
stats = _calculate_daily_stats(calculation_start, range_end, balance_list, actual_transactions)
|
||||
stats = _trim_stats(stats, range_start)
|
||||
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
|
||||
|
||||
|
||||
def _calculate_analysis(user):
|
||||
result = {}
|
||||
|
||||
today = current_date()
|
||||
booked = Q(booking_date__lte=today)
|
||||
incoming = Q(amount__lt=0)
|
||||
outgoing = Q(amount__gt=0)
|
||||
recurring = Q(recurring_months__isnull=False) & (
|
||||
Q(not_recurring_after__isnull=True) | Q(not_recurring_after__gte=today))
|
||||
|
||||
in_trans = user.transactions.filter(booked & incoming & recurring)
|
||||
in_trans_per_month = [t.amount / t.recurring_months for t in in_trans]
|
||||
result["avg_monthly_in"] = -sum(in_trans_per_month)
|
||||
|
||||
out_trans = user.transactions.filter(booked & outgoing & recurring)
|
||||
out_trans_per_month = [t.amount / t.recurring_months for t in out_trans]
|
||||
result["avg_monthly_out"] = sum(out_trans_per_month)
|
||||
|
||||
result["avg_monthly_res"] = result["avg_monthly_in"] - result["avg_monthly_out"]
|
||||
|
||||
return result
|
||||
from financeplanner.calc import Statistics
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
user = request.user
|
||||
today = current_date()
|
||||
range_start = today - relativedelta(months=6)
|
||||
range_end = today + relativedelta(months=6)
|
||||
balance_list = _get_relevant_balances(user, range_start, range_end)
|
||||
calculation_start = balance_list[0].date if balance_list else range_start
|
||||
transaction_list = _get_relevant_transactions(user, calculation_start, range_end)
|
||||
actual_transactions = _calculate_actual_transactions(calculation_start, range_end, transaction_list)
|
||||
if balance_list:
|
||||
graph_data = _build_graph_data(range_start, range_end, calculation_start, balance_list, actual_transactions)
|
||||
else:
|
||||
graph_data = None
|
||||
analysis = _calculate_analysis(user)
|
||||
statistics = Statistics(user)
|
||||
context = {
|
||||
"today": today,
|
||||
"range_start": range_start,
|
||||
"range_end": range_end,
|
||||
"balance_list": balance_list,
|
||||
"transaction_list": transaction_list,
|
||||
"actual_transactions": sorted(actual_transactions, key=lambda t: t[0]),
|
||||
"graph_data": graph_data,
|
||||
"analysis": analysis,
|
||||
"start": statistics.start,
|
||||
"end": statistics.end,
|
||||
"balances": statistics.balances,
|
||||
"transactions": statistics.transactions,
|
||||
"actual_transactions": statistics.actual_transactions,
|
||||
"daily_stats": statistics.get_daily_stats_in_range(),
|
||||
"avg_monthly_income": statistics.avg_monthly_income,
|
||||
"avg_monthly_expenses": statistics.avg_monthly_expenses,
|
||||
"avg_monthly_result": statistics.avg_monthly_result,
|
||||
}
|
||||
return render(request, "financeplanner/index.html", context)
|
||||
|
||||
Reference in New Issue
Block a user