finish copying logic to Statistics class
This commit is contained in:
@@ -1,18 +1,58 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from math import floor
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from financeplanner.utils import current_date
|
from financeplanner.utils import current_date
|
||||||
|
|
||||||
|
|
||||||
class ActualTransaction:
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class ActualTransaction:
|
||||||
def __init__(self, date, subject, amount):
|
def __init__(self, date, subject, amount):
|
||||||
self.date = date
|
self.date = date
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
|
|
||||||
|
|
||||||
|
class DailyStat:
|
||||||
|
def __init__(self, date, balance_amount, actual_transactions, resulting_amount):
|
||||||
|
self.date = date
|
||||||
|
self.balance_amount = balance_amount
|
||||||
|
self.actual_transactions = actual_transactions
|
||||||
|
self.resulting_amount = resulting_amount
|
||||||
|
self.percentage = 0
|
||||||
|
self.color_class = ""
|
||||||
|
self.opacity_class = ""
|
||||||
|
|
||||||
|
def generate_graph_bar_attributes(self, amount_scale, today):
|
||||||
|
if self.resulting_amount is not None:
|
||||||
|
self.percentage = int((self.resulting_amount / amount_scale) * 100)
|
||||||
|
if self.percentage >= 60:
|
||||||
|
self.color_class = "green"
|
||||||
|
elif self.percentage >= 45:
|
||||||
|
self.color_class = "olive"
|
||||||
|
elif self.percentage >= 30:
|
||||||
|
self.color_class = "yellow"
|
||||||
|
elif self.percentage >= 15:
|
||||||
|
self.color_class = "orange"
|
||||||
|
else:
|
||||||
|
self.color_class = "red"
|
||||||
|
if self.date == today:
|
||||||
|
self.opacity_class = "hightlighted"
|
||||||
|
elif self.balance_amount or self.actual_transactions:
|
||||||
|
self.opacity_class = "strong"
|
||||||
|
|
||||||
|
|
||||||
class Statistics:
|
class Statistics:
|
||||||
|
_min_daily_amount_scale = 500
|
||||||
|
|
||||||
def __init__(self, user):
|
def __init__(self, user):
|
||||||
self.user = user
|
self.user = user
|
||||||
@@ -23,14 +63,27 @@ class Statistics:
|
|||||||
self.balances = []
|
self.balances = []
|
||||||
self.transactions = []
|
self.transactions = []
|
||||||
self.actual_transactions = []
|
self.actual_transactions = []
|
||||||
|
self.daily_stats = []
|
||||||
|
self.daily_amount_scale = self._min_daily_amount_scale
|
||||||
|
self.avg_monthly_income = 0
|
||||||
|
self.avg_monthly_expenses = 0
|
||||||
|
self.avg_monthly_result = 0
|
||||||
|
|
||||||
|
self._fetch_relevant_balances()
|
||||||
|
self._fetch_relevant_transactions()
|
||||||
|
self._calculate_actual_transactions()
|
||||||
|
self._calculate_daily_stats()
|
||||||
|
self._calculate_daily_amount_scale()
|
||||||
|
self._generate_graph_bar_attributes()
|
||||||
|
self._calculate_analysis()
|
||||||
|
|
||||||
def _fetch_relevant_balances(self):
|
def _fetch_relevant_balances(self):
|
||||||
balances = []
|
self.balances = []
|
||||||
balances_until_start = self.user.balances.filter(date__lte=self.start)
|
balances_until_start = self.user.balances.filter(date__lte=self.start)
|
||||||
if balances_until_start.exists():
|
if balances_until_start.exists():
|
||||||
balances = [balances_until_start.latest("date")]
|
self.balances.append(balances_until_start.latest("date"))
|
||||||
other_balances = self.user.balances.filter(date__gt=self.start, date__lte=self.end).order_by("date")
|
other_balances = self.user.balances.filter(date__gt=self.start, date__lte=self.end).order_by("date")
|
||||||
self.balances = balances + list(other_balances)
|
self.balances += list(other_balances)
|
||||||
if self.balances:
|
if self.balances:
|
||||||
self.calc_start = self.balances[0].date
|
self.calc_start = self.balances[0].date
|
||||||
|
|
||||||
@@ -56,7 +109,7 @@ class Statistics:
|
|||||||
self.transactions = list(transactions)
|
self.transactions = list(transactions)
|
||||||
|
|
||||||
def _calculate_actual_transactions(self):
|
def _calculate_actual_transactions(self):
|
||||||
actual_transaction = []
|
actual_transactions = []
|
||||||
for trans in self.transactions:
|
for trans in self.transactions:
|
||||||
if trans.recurring_months:
|
if trans.recurring_months:
|
||||||
iter_date = trans.booking_date
|
iter_date = trans.booking_date
|
||||||
@@ -64,7 +117,7 @@ class Statistics:
|
|||||||
limit = min(trans.not_recurring_after, self.end) if trans.not_recurring_after else self.end
|
limit = min(trans.not_recurring_after, self.end) if trans.not_recurring_after else self.end
|
||||||
while iter_date < limit:
|
while iter_date < limit:
|
||||||
if iter_date >= self.calc_start:
|
if iter_date >= self.calc_start:
|
||||||
actual_transaction.append(ActualTransaction(
|
actual_transactions.append(ActualTransaction(
|
||||||
date=iter_date,
|
date=iter_date,
|
||||||
subject=f"{trans.subject} #{iter_step}",
|
subject=f"{trans.subject} #{iter_step}",
|
||||||
amount=trans.amount,
|
amount=trans.amount,
|
||||||
@@ -72,9 +125,69 @@ class Statistics:
|
|||||||
iter_date += relativedelta(months=trans.recurring_months)
|
iter_date += relativedelta(months=trans.recurring_months)
|
||||||
iter_step += 1
|
iter_step += 1
|
||||||
else:
|
else:
|
||||||
actual_transaction.append(ActualTransaction(
|
actual_transactions.append(ActualTransaction(
|
||||||
date=trans.booking_date,
|
date=trans.booking_date,
|
||||||
subject=trans.subject,
|
subject=trans.subject,
|
||||||
amount=trans.amount,
|
amount=trans.amount,
|
||||||
))
|
))
|
||||||
self.actual_transactions = actual_transaction
|
self.actual_transactions = sorted(actual_transactions, key=lambda t: t.date)
|
||||||
|
|
||||||
|
def _calculate_daily_stats(self):
|
||||||
|
self.daily_stats = []
|
||||||
|
iter_date = self.calc_start
|
||||||
|
iter_amount = None
|
||||||
|
while iter_date < self.end:
|
||||||
|
balance_amount, actual_transactions = None, []
|
||||||
|
|
||||||
|
relevant_balances = [bal for bal in self.balances if bal.date == iter_date]
|
||||||
|
if relevant_balances:
|
||||||
|
assert len(relevant_balances) == 1, \
|
||||||
|
f"balances should be unique for user and date, but here are {relevant_balances}"
|
||||||
|
balance_amount = relevant_balances[0].amount
|
||||||
|
iter_amount = balance_amount
|
||||||
|
|
||||||
|
for transaction in self.actual_transactions:
|
||||||
|
if transaction.date == iter_date:
|
||||||
|
actual_transactions.append(transaction)
|
||||||
|
if iter_amount is not None:
|
||||||
|
iter_amount -= transaction.amount
|
||||||
|
|
||||||
|
if iter_amount is not None:
|
||||||
|
self.daily_stats.append(DailyStat(
|
||||||
|
date=iter_date,
|
||||||
|
balance_amount=balance_amount,
|
||||||
|
actual_transactions=actual_transactions,
|
||||||
|
resulting_amount=iter_amount,
|
||||||
|
))
|
||||||
|
iter_date += relativedelta(days=1)
|
||||||
|
|
||||||
|
def _calculate_daily_amount_scale(self):
|
||||||
|
amounts = [s.resulting_amount for s in self.daily_stats if s.resulting_amount is not None]
|
||||||
|
if amounts:
|
||||||
|
max_amount = max(amounts)
|
||||||
|
scale = _floor_to_first_two_places(max_amount * Decimal("1.25"))
|
||||||
|
self.daily_amount_scale = max(scale, self._min_daily_amount_scale)
|
||||||
|
|
||||||
|
def _generate_graph_bar_attributes(self):
|
||||||
|
for stat in self.daily_stats:
|
||||||
|
stat.generate_graph_bar_attributes(self.daily_amount_scale, self.today)
|
||||||
|
|
||||||
|
def _calculate_analysis(self):
|
||||||
|
booked = Q(booking_date__lte=self.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=self.today))
|
||||||
|
|
||||||
|
in_trans = self.user.transactions.filter(booked & incoming & recurring)
|
||||||
|
in_trans_per_month = [t.amount / t.recurring_months for t in in_trans]
|
||||||
|
self.avg_monthly_income = -sum(in_trans_per_month)
|
||||||
|
|
||||||
|
out_trans = self.user.transactions.filter(booked & outgoing & recurring)
|
||||||
|
out_trans_per_month = [t.amount / t.recurring_months for t in out_trans]
|
||||||
|
self.avg_monthly_expenses = sum(out_trans_per_month)
|
||||||
|
|
||||||
|
self.avg_monthly_result = self.avg_monthly_income - self.avg_monthly_expenses
|
||||||
|
|
||||||
|
def get_daily_stats_in_range(self):
|
||||||
|
return [s for s in self.daily_stats if self.start <= s.date <= self.end]
|
||||||
|
|||||||
Reference in New Issue
Block a user