adjust index view, template and tags to work with the new Statistics class
This commit is contained in:
@@ -19,29 +19,26 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="main-container flex-col-centering">
|
<div class="main-container flex-col-centering">
|
||||||
{% if graph_data %}
|
{% if daily_stats %}
|
||||||
<div id="date-panel">-</div>
|
<div id="date-panel">-</div>
|
||||||
<div id="graph">
|
<div id="graph">
|
||||||
{% for date, stat in graph_data.items %}
|
{% for stat in daily_stats %}
|
||||||
{% if stat.div_percentage is None %}
|
{% if stat.percentage is None %}
|
||||||
<div class="graph-bar weak grey"
|
<div class="graph-bar weak grey"
|
||||||
onmouseover="showGraphBarDate('{{ date|date:"d.m.y" }}')"
|
onmouseover="showGraphBarDate('{{ stat.date|date:"d.m.y" }}')"
|
||||||
onmouseleave="clearGraphBarDate()">
|
onmouseleave="clearGraphBarDate()">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="graph-bar {{ stat.div_opacity_class }} {{ stat.div_color_class }}"
|
<div class="graph-bar {{ stat.opacity_class }} {{ stat.color_class }}"
|
||||||
style="height: {{ stat.div_percentage }}%"
|
style="height: {{ stat.percentage }}%"
|
||||||
onmouseover="showGraphBarDate('{{ date|date:"d.m.y" }}')"
|
onmouseover="showGraphBarDate('{{ stat.date|date:"d.m.y" }}')"
|
||||||
onmouseleave="clearGraphBarDate()">
|
onmouseleave="clearGraphBarDate()">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>No enough data from {{ start|date:"d.m.y" }} til {{ end|date:"d.m.y" }} to show a graph.</p>
|
||||||
No enough data from {{ range_start|date:"d.m.y" }} til
|
|
||||||
{{ range_end|date:"d.m.y" }} to show a graph.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,15 +48,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>current average monthly income</td>
|
<td>current average monthly income</td>
|
||||||
<td>{{ analysis.avg_monthly_in|euro }}</td>
|
<td>{{ avg_monthly_income|euro }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>current average monthly expenses</td>
|
<td>current average monthly expenses</td>
|
||||||
<td>{{ analysis.avg_monthly_out|euro }}</td>
|
<td>{{ avg_monthly_expenses|euro }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>current average monthly result</td>
|
<td>current average monthly result</td>
|
||||||
<td>{{ analysis.avg_monthly_res|euro }}</td>
|
<td>{{ avg_monthly_result|euro }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -84,7 +81,7 @@
|
|||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h2>Relevant Balances</h2>
|
<h2>Relevant Balances</h2>
|
||||||
{% if balance_list %}
|
{% if balances %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -93,7 +90,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for balance in balance_list %}
|
{% for balance in balances %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ balance.date|date:"d.m.y" }}</td>
|
<td>{{ balance.date|date:"d.m.y" }}</td>
|
||||||
<td>{{ balance.amount|euro }}</td>
|
<td>{{ balance.amount|euro }}</td>
|
||||||
@@ -108,7 +105,7 @@
|
|||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h2>Relevant Stored Transactions</h2>
|
<h2>Relevant Stored Transactions</h2>
|
||||||
{% if transaction_list %}
|
{% if transactions %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -120,7 +117,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for transaction in transaction_list %}
|
{% for transaction in transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ transaction.subject }}</td>
|
<td>{{ transaction.subject }}</td>
|
||||||
<td>{{ transaction.amount|euro }}</td>
|
<td>{{ transaction.amount|euro }}</td>
|
||||||
@@ -148,11 +145,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for date, subject, amount in actual_transactions %}
|
{% for trans in actual_transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ date|date:"d.m.y" }}</td>
|
<td>{{ trans.date|date:"d.m.y" }}</td>
|
||||||
<td>{{ subject }}</td>
|
<td>{{ trans.subject }}</td>
|
||||||
<td>{{ amount|euro }}</td>
|
<td>{{ trans.amount|euro }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
from financeplanner.utils import format_price
|
from financeplanner.utils import format_price
|
||||||
@@ -9,5 +7,4 @@ register = template.Library()
|
|||||||
|
|
||||||
@register.filter(name="euro")
|
@register.filter(name="euro")
|
||||||
def euro(value):
|
def euro(value):
|
||||||
assert isinstance(value, Decimal)
|
|
||||||
return format_price(value) or "-"
|
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.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Q
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from financeplanner.utils import current_date
|
from financeplanner.calc import Statistics
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
user = request.user
|
user = request.user
|
||||||
today = current_date()
|
statistics = Statistics(user)
|
||||||
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)
|
|
||||||
context = {
|
context = {
|
||||||
"today": today,
|
"start": statistics.start,
|
||||||
"range_start": range_start,
|
"end": statistics.end,
|
||||||
"range_end": range_end,
|
"balances": statistics.balances,
|
||||||
"balance_list": balance_list,
|
"transactions": statistics.transactions,
|
||||||
"transaction_list": transaction_list,
|
"actual_transactions": statistics.actual_transactions,
|
||||||
"actual_transactions": sorted(actual_transactions, key=lambda t: t[0]),
|
"daily_stats": statistics.get_daily_stats_in_range(),
|
||||||
"graph_data": graph_data,
|
"avg_monthly_income": statistics.avg_monthly_income,
|
||||||
"analysis": analysis,
|
"avg_monthly_expenses": statistics.avg_monthly_expenses,
|
||||||
|
"avg_monthly_result": statistics.avg_monthly_result,
|
||||||
}
|
}
|
||||||
return render(request, "financeplanner/index.html", context)
|
return render(request, "financeplanner/index.html", context)
|
||||||
|
|||||||
Reference in New Issue
Block a user