1
1

adjust index view, template and tags to work with the new Statistics class

This commit is contained in:
2020-02-26 23:29:25 +01:00
parent b3d610cff6
commit eee65c63fc
3 changed files with 30 additions and 242 deletions

View File

@@ -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>

View File

@@ -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 "-"

View File

@@ -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)