From b3d610cff6dea0479581783b006a568a15192540 Mon Sep 17 00:00:00 2001 From: Florian Hartmann Date: Wed, 26 Feb 2020 23:28:27 +0100 Subject: [PATCH] finish copying logic to Statistics class --- financeplanner/calc.py | 129 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/financeplanner/calc.py b/financeplanner/calc.py index ac2e85e..9af722b 100644 --- a/financeplanner/calc.py +++ b/financeplanner/calc.py @@ -1,18 +1,58 @@ +from decimal import Decimal +from math import floor + from dateutil.relativedelta import relativedelta from django.db.models import Q 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): self.date = date self.subject = subject 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: + _min_daily_amount_scale = 500 def __init__(self, user): self.user = user @@ -23,14 +63,27 @@ class Statistics: self.balances = [] self.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): - balances = [] + self.balances = [] balances_until_start = self.user.balances.filter(date__lte=self.start) 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") - self.balances = balances + list(other_balances) + self.balances += list(other_balances) if self.balances: self.calc_start = self.balances[0].date @@ -56,7 +109,7 @@ class Statistics: self.transactions = list(transactions) def _calculate_actual_transactions(self): - actual_transaction = [] + actual_transactions = [] for trans in self.transactions: if trans.recurring_months: 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 while iter_date < limit: if iter_date >= self.calc_start: - actual_transaction.append(ActualTransaction( + actual_transactions.append(ActualTransaction( date=iter_date, subject=f"{trans.subject} #{iter_step}", amount=trans.amount, @@ -72,9 +125,69 @@ class Statistics: iter_date += relativedelta(months=trans.recurring_months) iter_step += 1 else: - actual_transaction.append(ActualTransaction( + actual_transactions.append(ActualTransaction( date=trans.booking_date, subject=trans.subject, 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]