No enough data from {{ start|date:"d.m.y" }} til {{ end|date:"d.m.y" }} to show a graph.
- {% endif %} -diff --git a/financeplanner/__init__.py b/financeplanner/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/financeplanner/admin.py b/financeplanner/admin.py deleted file mode 100644 index 93a2a41..0000000 --- a/financeplanner/admin.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.contrib import admin -from django.urls import reverse_lazy - -from financeplanner.models import Transaction, Balance -from financeplanner.utils import format_price - - -class AdminSite(admin.AdminSite): - index_title = "FinancePlanner" - site_title = "Admin Panel" - site_header = "Admin Panel" - site_url = reverse_lazy("finance:index") - - -admin_site = AdminSite() - - -def amount(obj): - return format_price(obj.amount) - - -amount.short_description = "amount" - - -@admin.register(Transaction, site=admin_site) -class TransactionAdmin(admin.ModelAdmin): - list_display = ("subject", amount, "booking_date", "recurring_months", "not_recurring_after") - - -@admin.register(Balance, site=admin_site) -class BalanceAdmin(admin.ModelAdmin): - list_display = ("date", amount) - ordering = ("-date",) diff --git a/financeplanner/apps.py b/financeplanner/apps.py deleted file mode 100644 index e2f55bb..0000000 --- a/financeplanner/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class FinancePlannerConfig(AppConfig): - name = "financeplanner" - verbose_name = "FinancePlanner" diff --git a/financeplanner/calc.py b/financeplanner/calc.py deleted file mode 100644 index 3746f5d..0000000 --- a/financeplanner/calc.py +++ /dev/null @@ -1,245 +0,0 @@ -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 - -days_per_year = Decimal("365.25") -days_per_month = days_per_year / Decimal("12") -days_per_week = Decimal("7") - - -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 - - def __str__(self): - return f"ActualTransaction({self.date}, {self.subject}, {self.amount})" - - -class DailyStat: - def __init__(self, stat_id, date, balance_amount, actual_transactions, resulting_amount): - self.id = stat_id - self.date = date - self.balance_amount = balance_amount - self.actual_transactions = actual_transactions - self.resulting_amount = resulting_amount - self.highlighted = False - self.percentage = 40 - self.resulting_amount_irregular = self.resulting_amount - self.percentage_irregular = self.percentage - - def __str__(self): - return f"DailyStat({self.date}, {self.balance_amount}, " \ - f"{len(self.actual_transactions)} a. transactions, {self.resulting_amount})" - - def generate_graph_bar_attributes(self, amount_scale, today): - self.highlighted = self.date == today - if self.resulting_amount is not None: - self.percentage = int((self.resulting_amount / amount_scale) * 100) - self.percentage_irregular = self.percentage - - def generate_irregular_graph_bar_attributes(self, amount_scale): - if self.resulting_amount_irregular is not None: - self.percentage_irregular = int((self.resulting_amount_irregular / amount_scale) * 100) - - -class Statistics: - _min_daily_amount_scale = 500 - - def __init__(self, user): - self.user = user - self.today = current_date() - self.calc_start = self.today - relativedelta(months=6) - self.calc_end = self.today + relativedelta(months=6) - self.real_calc_start = self.calc_start - self.display_start = self.today - relativedelta(months=1) - self.display_end = self.today + relativedelta(months=2) - 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.avg_daily_irregular_expenses = 0 - self.avg_weekly_irregular_expenses = 0 - self.avg_monthly_irregular_expenses = 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() - self._add_statistics_considering_analysis() - - def _fetch_relevant_balances(self): - self.balances = [] - balances_until_start = self.user.balances.filter(date__lte=self.calc_start) - if balances_until_start.exists(): - self.balances.append(balances_until_start.latest("date")) - other_balances = self.user.balances.filter(date__gt=self.calc_start, date__lte=self.calc_end).order_by("date") - self.balances += list(other_balances) - if self.balances: - self.real_calc_start = self.balances[0].date - - def _fetch_relevant_transactions(self): - one_time_in_range = Q( - booking_date__gte=self.real_calc_start, - booking_date__lte=self.calc_end, - recurring_months__isnull=True, - ) - endlessly_recurring = Q( - booking_date__lte=self.calc_end, - recurring_months__isnull=False, - not_recurring_after__isnull=True, - ) - recurring_and_ending_in_range = Q( - booking_date__lte=self.calc_end, - recurring_months__isnull=False, - not_recurring_after__gte=self.real_calc_start, - ) - transactions = self.user.transactions.filter( - one_time_in_range | endlessly_recurring | recurring_and_ending_in_range - ).order_by("booking_date") - self.transactions = list(transactions) - - def _calculate_actual_transactions(self): - actual_transactions = [] - for trans in self.transactions: - if trans.recurring_months: - iter_date = trans.booking_date - iter_step = 1 - limit = min(trans.not_recurring_after, self.calc_end) if trans.not_recurring_after else self.calc_end - while iter_date < limit: - if iter_date >= self.real_calc_start: - actual_transactions.append(ActualTransaction( - date=iter_date, - subject=f"{trans.subject} #{iter_step}", - amount=trans.amount, - )) - iter_date += relativedelta(months=trans.recurring_months) - iter_step += 1 - else: - actual_transactions.append(ActualTransaction( - date=trans.booking_date, - subject=trans.subject, - amount=trans.amount, - )) - self.actual_transactions = sorted(actual_transactions, key=lambda t: t.date) - - def _calculate_daily_stats(self): - self.daily_stats = [] - iter_id = 0 - iter_date = self.real_calc_start - iter_amount = None - while iter_date < self.calc_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( - stat_id=iter_id, - date=iter_date, - balance_amount=balance_amount, - actual_transactions=actual_transactions, - resulting_amount=iter_amount, - )) - iter_id += 1 - 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 - and self.display_start <= s.date <= self.display_end] - 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__gt=0) - outgoing = Q(amount__lt=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 - - daily_irregular_expenses = [] - if len(self.balances) > 1: - for i in range(1, len(self.balances)): - previous_balance = self.balances[i - 1] - current_balance = self.balances[i] - day_before = current_balance.date - relativedelta(days=1) - relevant_stats = [s for s in self.daily_stats if s.date == day_before] - if relevant_stats and day_before > previous_balance.date: - assert len(relevant_stats) == 1, \ - f"daily stats should be unique per date, but here are {relevant_stats}" - amount_diff = current_balance.amount - relevant_stats[0].resulting_amount - time_diff = current_balance.date - previous_balance.date - daily_irregular_expenses.append(amount_diff / time_diff.days) - if daily_irregular_expenses: - self.avg_daily_irregular_expenses = sum(daily_irregular_expenses) / len(daily_irregular_expenses) - self.avg_weekly_irregular_expenses = self.avg_daily_irregular_expenses * days_per_week - self.avg_monthly_irregular_expenses = self.avg_daily_irregular_expenses * days_per_month - - self.avg_monthly_result_complete = self.avg_monthly_result + self.avg_monthly_irregular_expenses - - def _add_statistics_considering_analysis(self): - previous_original_amount = None - iter_amount = None - for stat in self.daily_stats: - if stat.resulting_amount is not None: - relevant_balances = [bal for bal in self.balances if bal.date == stat.date] - if relevant_balances or previous_original_amount is None: - iter_amount = stat.resulting_amount + self.avg_daily_irregular_expenses - else: - orig_diff = stat.resulting_amount - previous_original_amount - new_diff = orig_diff + self.avg_daily_irregular_expenses - iter_amount += new_diff - stat.resulting_amount_irregular = iter_amount - stat.generate_irregular_graph_bar_attributes(self.daily_amount_scale) - previous_original_amount = stat.resulting_amount - - def get_daily_stats_in_range(self): - return [s for s in self.daily_stats if self.display_start <= s.date <= self.display_end] diff --git a/financeplanner/management/__init__.py b/financeplanner/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/financeplanner/management/commands/__init__.py b/financeplanner/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/financeplanner/management/commands/createadmin.py b/financeplanner/management/commands/createadmin.py deleted file mode 100644 index 5200f9c..0000000 --- a/financeplanner/management/commands/createadmin.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib.auth.management.commands import createsuperuser -from django.db import transaction - -USERNAME = "admin" - - -class Command(createsuperuser.Command): - help = "Used to create the superuser 'admin' with password '123'" - - @transaction.atomic() - def handle(self, *args, **options): - options.update({ - self.UserModel.USERNAME_FIELD: USERNAME, - "email": "admin@test.com", - "interactive": False, - }) - super().handle(*args, **options) - user = self.UserModel.objects.latest("pk") - user.set_password("123") - user.save() diff --git a/financeplanner/management/commands/filldummydata.py b/financeplanner/management/commands/filldummydata.py deleted file mode 100644 index cddd167..0000000 --- a/financeplanner/management/commands/filldummydata.py +++ /dev/null @@ -1,68 +0,0 @@ -from decimal import Decimal - -from dateutil.relativedelta import relativedelta -from django.contrib.auth import get_user_model -from django.core.management import BaseCommand - -from financeplanner.management.commands.createadmin import USERNAME -from financeplanner.models import Transaction, Balance -from financeplanner.utils import current_date - -User = get_user_model() - - -class Command(BaseCommand): - - def handle(self, *args, **options): - user = User.objects.get(**{User.USERNAME_FIELD: USERNAME}) - - today = current_date() - - Transaction.objects.create( - user=user, - subject="simple transaction some days ago", - amount=Decimal("34.95"), - booking_date=today - relativedelta(days=5), - ) - - Transaction.objects.create( - user=user, - subject="simple transaction a week ahead", - amount=Decimal("66.66"), - booking_date=today + relativedelta(weeks=1), - ) - - Transaction.objects.create( - user=user, - subject="recurring stuff ending soon", - amount=Decimal("49"), - booking_date=today - relativedelta(days=180), - recurring_months=1, - not_recurring_after=today + relativedelta(weeks=6) - ) - - Transaction.objects.create( - user=user, - subject="recurring stuff beginning soon", - amount=Decimal("30"), - recurring_months=2, - booking_date=today + relativedelta(weeks=2), - ) - - Balance.objects.create( - user=user, - date=today - relativedelta(weeks=7), - amount=Decimal("600"), - ) - - Balance.objects.create( - user=user, - date=today - relativedelta(weeks=3), - amount=Decimal("380"), - ) - - Balance.objects.create( - user=user, - date=today - relativedelta(days=2), - amount=Decimal("450"), - ) diff --git a/financeplanner/migrations/0001_initial.py b/financeplanner/migrations/0001_initial.py deleted file mode 100644 index afe0f6b..0000000 --- a/financeplanner/migrations/0001_initial.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.0.3 on 2020-02-18 08:56 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import financeplanner.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Transaction', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subject', models.CharField(max_length=100, verbose_name='subject')), - ('amount', financeplanner.models.PriceField(decimal_places=2, max_digits=8, verbose_name='amount')), - ('booking_date', models.DateField(verbose_name='booking date')), - ('recurring_months', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='recurring months')), - ('not_recurring_after', models.DateField(blank=True, null=True, verbose_name='not recurring after')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Balance', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField(verbose_name='date')), - ('amount', financeplanner.models.PriceField(decimal_places=2, max_digits=8, verbose_name='amount')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='balances', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'unique_together': {('user', 'date')}, - }, - ), - ] diff --git a/financeplanner/migrations/__init__.py b/financeplanner/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/financeplanner/models.py b/financeplanner/models.py deleted file mode 100644 index ae63f27..0000000 --- a/financeplanner/models.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError -from django.db import models - -from financeplanner.utils import round_with_dec_places - -User = get_user_model() - - -class PriceField(models.DecimalField): - - def to_python(self, value): - value = super(PriceField, self).to_python(value) - return round_with_dec_places(value, self.decimal_places) - - def __init__(self, *args, **kwargs): - # allowing numbers until 999,999.99 - kwargs['max_digits'] = 8 - kwargs['decimal_places'] = 2 - super().__init__(*args, **kwargs) - - -class BaseModel(models.Model): - class Meta: - abstract = True - - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - self.full_clean() - super().save(force_insert, force_update, using, update_fields) - - -class Transaction(models.Model): - user = models.ForeignKey(to=User, related_name="transactions", on_delete=models.CASCADE) - subject = models.CharField("subject", max_length=100) - amount = PriceField("amount") - booking_date = models.DateField("booking date") - recurring_months = models.PositiveSmallIntegerField("recurring months", null=True, blank=True) - not_recurring_after = models.DateField("not recurring after", null=True, blank=True) - - def __str__(self): - return f"{self.subject}: {self.amount:.2f}€" - - def clean(self): - if self.recurring_months is not None and self.recurring_months < 1: - raise ValidationError("recurring_months must be at least 1, if present") - - if self.not_recurring_after and self.not_recurring_after <= self.booking_date: - raise ValidationError("not_recurring_after must be later than booking_date") - - -class Balance(models.Model): - class Meta: - unique_together = ("user", "date") - - user = models.ForeignKey(to=User, related_name="balances", on_delete=models.CASCADE) - date = models.DateField("date") - amount = PriceField("amount") - - def __str__(self): - return f"{self.date}: {self.amount:.2f}€" diff --git a/financeplanner/static/financeplanner/style.css b/financeplanner/static/financeplanner/style.css deleted file mode 100644 index cd82756..0000000 --- a/financeplanner/static/financeplanner/style.css +++ /dev/null @@ -1,203 +0,0 @@ -* { - color: #FFFCF9; -} - -html, body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; - border: none; -} - -body { - display: flex; - flex-direction: column; - align-items: center; - background-color: #131211; - padding-bottom: 24px; -} - -h1 { - margin-top: 18px; - margin-bottom: 6px; - font-size: 1.4em; - font-weight: normal; - color: #FFCC99; -} - -h2 { - margin-top: 6px; - margin-bottom: 12px; - font-size: 1.1em; - font-weight: normal; - color: #FFCC99; -} - -a { - color: #FFCC99; - text-decoration: none; - transition: color 150ms; -} - -a:hover, a:focus { - color: #CC6622; -} - -table { - margin: 6px; - border-collapse: collapse; -} - -td { - padding: 3px 12px 3px 6px; - border: 1px solid #3F3D3B; -} - -thead { - font-variant: small-caps; -} - -tr { - background-color: rgba(255, 255, 255, 0); - transition: background-color 100ms; -} - -tr:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.mini-link { - font-size: 0.9em; -} - -.flex-col-centering { - display: flex; - flex-direction: column; - align-items: center; -} - -.text-centering { - text-align: center; -} - -.main-container { - flex-shrink: 1; - width: 90%; - max-width: 1200px; - padding: 12px; - margin-top: 12px; - background-color: #282624; - border-radius: 4px; - font-size: 16px; -} - -.login-form { - flex: 1; - width: 95%; - max-width: 400px; -} - -.login-line { - flex: 1; - width: 95%; - margin: 6px; - padding: 3px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.login-label { - flex-shrink: 1; - margin-right: 6px; - padding: 2px; - text-align: right; -} - -.login-textbox { - flex-grow: 1; - padding: 2px; - min-width: 60px; - background-color: #DDDDDD; - color: #131211; - border: none; - border-radius: 4px; -} - -.login-warning { - flex: 1; - padding: 2px; - text-align: center; - color: #CC6622; -} - -.login-button { - flex: 0 0 120px; - padding: 4px; - background-color: #202020; - border: 1px solid #666666; - border-radius: 4px; - color: #FFCC99; - transition: color 150ms, background-color 150ms; -} - -.login-button:hover, .login-button:focus { - background-color: #131211; - color: #CC6622; -} - -#big-stat-panel { - width: 100%; - opacity: 50%; - font-size: 1.4em; - height: 1.4em; - text-align: center; -} - -#small-stat-panel { - width: 100%; - opacity: 50%; - font-size: 0.8em; - height: 0.8em; - text-align: center; -} - -#graph { - flex: 0 0 300px; - height: 300px; - width: 100%; - display: flex; - flex-direction: row; - align-items: end; - padding-bottom: 4px; - overflow-x: auto; - transition: opacity 100ms; -} - -#graph.scrolling { - opacity: 75%; - cursor: grabbing; -} - -.graph-bar { - flex: 1 1 0; - min-width: 10px; - min-height: 10px; - margin-right: 1px; - border-radius: 2px; - background-color: #FFCC99; - z-index: 10; - transition: height 250ms; -} - -.graph-bar.highlighted { - background-color: #CC6622; -} - -.graph-bar:hover { - background-color: #FFFCF9; - box-shadow: #FFFCF9 0 0 12px; - z-index: 11; -} diff --git a/financeplanner/templates/financeplanner/base.html b/financeplanner/templates/financeplanner/base.html deleted file mode 100644 index 8891135..0000000 --- a/financeplanner/templates/financeplanner/base.html +++ /dev/null @@ -1,22 +0,0 @@ -{% load static %} - - - -
- - -- {% block navi %}{% endblock %} -
- -{% block content %}{% endblock %} - - - diff --git a/financeplanner/templates/financeplanner/index.html b/financeplanner/templates/financeplanner/index.html deleted file mode 100644 index 68a0a97..0000000 --- a/financeplanner/templates/financeplanner/index.html +++ /dev/null @@ -1,223 +0,0 @@ -{% extends "financeplanner/base.html" %} - -{% load custom_tags %} - -{% block navi %} - Admin Panel - - - Logout -{% endblock %} - -{% block content %} -No enough data from {{ start|date:"d.m.y" }} til {{ end|date:"d.m.y" }} to show a graph.
- {% endif %} -current average amounts per month:
-| income | -{{ avg_monthly_income|euro }} | -
| expenses | -{{ avg_monthly_expenses|euro }} | -
| result | -{{ avg_monthly_result|euro }} | -
average irregular expenses based on the last half-a-year:
-| monthly | -{{ avg_monthly_irregular_expenses|euro }} | -
| weekly | -{{ avg_weekly_irregular_expenses|euro }} | -
| daily | -{{ avg_daily_irregular_expenses|euro }} | -
- - -
- -monthly result after irregular expenses: {{ avg_monthly_result_complete|euro }}
-| date | -amount | -
| {{ balance.date|date:"d.m.y" }} | -{{ balance.amount|euro }} | -
-
- {% endif %} -| subject | -amount | -booking date | -recurring months | -not recurring after | -
| {{ transaction.subject }} | -{{ transaction.amount|euro }} | -{{ transaction.booking_date|date:"d.m.y" }} | -{{ transaction.recurring_months }} | -{{ transaction.not_recurring_after|date:"d.m.y" }} | -
-
- {% endif %} -| date | -subject | -amount | -
| {{ trans.date|date:"d.m.y" }} | -{{ trans.subject }} | -{{ trans.amount|euro }} | -
-
- {% endif %} -You have been logged out.
-