diff --git a/nummi/main/models.py b/nummi/main/models.py index d23b8e7..7af054c 100644 --- a/nummi/main/models.py +++ b/nummi/main/models.py @@ -194,7 +194,7 @@ class Snapshot(models.Model): @property def sum(self): if self.previous is None: - return None + return 0 trans = self.transactions.aggregate(sum=models.Sum("value")) return trans["sum"] or 0 @@ -206,6 +206,24 @@ class Snapshot(models.Model): date__lte=self.date, date__gt=self.previous.date ) + @property + def pos(self): + return ( + self.transactions.filter(value__gt=0).aggregate(sum=models.Sum("value"))[ + "sum" + ] + or 0 + ) + + @property + def neg(self): + return ( + self.transactions.filter(value__lt=0).aggregate(sum=models.Sum("value"))[ + "sum" + ] + or 0 + ) + class Meta: ordering = ["-date"] verbose_name = _("Snapshot") diff --git a/nummi/nummi/urls.py b/nummi/nummi/urls.py index 264396e..37a7a5e 100644 --- a/nummi/nummi/urls.py +++ b/nummi/nummi/urls.py @@ -20,6 +20,7 @@ from django.views.generic.base import RedirectView urlpatterns = i18n_patterns( path("", include("main.urls")), + path("plot/", include("plot.urls")), path("admin/", admin.site.urls), prefix_default_language=False, ) diff --git a/nummi/plot/__init__.py b/nummi/plot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nummi/plot/admin.py b/nummi/plot/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nummi/plot/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nummi/plot/apps.py b/nummi/plot/apps.py new file mode 100644 index 0000000..1f7a601 --- /dev/null +++ b/nummi/plot/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PlotConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "plot" diff --git a/nummi/plot/migrations/__init__.py b/nummi/plot/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nummi/plot/models.py b/nummi/plot/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/nummi/plot/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/nummi/plot/nummi.mplstyle b/nummi/plot/nummi.mplstyle new file mode 100644 index 0000000..5fd1a08b --- /dev/null +++ b/nummi/plot/nummi.mplstyle @@ -0,0 +1,13 @@ +font.family: Inter + +lines.linewidth: 2 + +figure.autolayout: True +figure.figsize: 8, 4 +figure.dpi: 300 + +axes.prop_cycle: cycler('color', ["66cc66", "#338033", "#99ff99", "#802653", "#cc6699"]) +axes.xmargin: 0 +axes.grid: True + +svg.fonttype: none diff --git a/nummi/plot/tests.py b/nummi/plot/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nummi/plot/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nummi/plot/urls.py b/nummi/plot/urls.py new file mode 100644 index 0000000..dde7b92 --- /dev/null +++ b/nummi/plot/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("timeline", views.timeline, name="timeline"), +] diff --git a/nummi/plot/views.py b/nummi/plot/views.py new file mode 100644 index 0000000..fe417d8 --- /dev/null +++ b/nummi/plot/views.py @@ -0,0 +1,40 @@ +import io + +import matplotlib +import matplotlib.pyplot as plt +from matplotlib import dates as mdates +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.utils.translation import gettext as _ + +from main.models import ( + Transaction, + Invoice, + Category, + Snapshot, +) + + +matplotlib.use("Agg") +plt.style.use("./plot/nummi.mplstyle") + + +@login_required +def timeline(request): + _snapshots = Snapshot.objects.all() + + fig, ax = plt.subplots() + ax.step( + [s.date for s in _snapshots], + [s.value for s in _snapshots], + where="post", + ) + ax.set(ylabel=_("Snapshots"), ylim=0) + ax.xaxis.set_major_formatter( + mdates.ConciseDateFormatter(ax.xaxis.get_major_locator()) + ) + _io = io.StringIO() + + fig.savefig(_io, format="svg") + + return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})