Compare commits

..

No commits in common. "fdc9214b10cdf61361cf257499e1142dcbd6e56e" and "ee7e6e60a788ab37fbad3b085da7d87c854c5d78" have entirely different histories.

22 changed files with 301 additions and 300 deletions

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.0.1\n" "Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-17 18:15+0200\n" "POT-Creation-Date: 2023-01-01 10:02+0100\n"
"PO-Revision-Date: 2022-12-21 17:30+0100\n" "PO-Revision-Date: 2022-12-21 17:30+0100\n"
"Last-Translator: edpibu <git@edgarpierre.fr>\n" "Last-Translator: edpibu <git@edgarpierre.fr>\n"
"Language-Team: edpibu <git@edgarpierre.fr>\n" "Language-Team: edpibu <git@edgarpierre.fr>\n"
@ -21,28 +21,20 @@ msgstr ""
msgid "Add transactions" msgid "Add transactions"
msgstr "Ajouter des transactions" msgstr "Ajouter des transactions"
#: .\main\forms.py:97 .\main\templates\main\base.html:59
#: .\main\templates\main\form\search.html:5
#: .\main\templates\main\list\transaction.html:9
#: .\main\templates\main\list\transaction.html:26
#: .\main\templates\main\search.html:6 .\main\templates\main\search.html:15
msgid "Search"
msgstr "Rechercher"
#: .\main\models.py:17 #: .\main\models.py:17
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
#: .\main\models.py:45 .\main\models.py:79 .\main\models.py:87 #: .\main\models.py:45 .\main\models.py:79 .\main\models.py:87
#: .\main\models.py:237 .\main\templates\main\base.html:39 #: .\main\models.py:237 .\main\templates\main\base.html:39
#: .\main\templates\main\table\snapshot.html:10 #: .\main\templates\main\table\snapshot.html:7
#: .\main\templates\main\table\transaction.html:15 #: .\main\templates\main\table\transaction.html:11
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: .\main\models.py:45 .\main\models.py:97 .\main\models.py:208 #: .\main\models.py:45 .\main\models.py:97 .\main\models.py:208
#: .\main\models.py:278 .\main\templates\main\table\transaction.html:8 #: .\main\models.py:278 .\main\templates\main\table\transaction.html:7
#: .\main\templates\main\transaction_form.html:28 #: .\main\templates\main\transaction_form.html:27
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
@ -54,13 +46,13 @@ msgstr "Icône"
msgid "Default" msgid "Default"
msgstr "Défaut" msgstr "Défaut"
#: .\main\models.py:80 .\main\templates\main\index.html:19 #: .\main\models.py:80 .\main\templates\main\index.html:16
msgid "Accounts" msgid "Accounts"
msgstr "Comptes" msgstr "Comptes"
#: .\main\models.py:97 .\main\models.py:121 .\main\models.py:227 #: .\main\models.py:97 .\main\models.py:121 .\main\models.py:227
#: .\main\templates\main\base.html:49 #: .\main\templates\main\base.html:49
#: .\main\templates\main\table\transaction.html:12 #: .\main\templates\main\table\transaction.html:10
msgid "Category" msgid "Category"
msgstr "Catégorie" msgstr "Catégorie"
@ -68,8 +60,8 @@ msgstr "Catégorie"
msgid "Budget" msgid "Budget"
msgstr "Budget" msgstr "Budget"
#: .\main\models.py:122 .\main\templates\main\index.html:35 #: .\main\models.py:122 .\main\templates\main\index.html:30
#: .\main\templates\main\snapshot_form.html:35 #: .\main\templates\main\snapshot_form.html:34
msgid "Categories" msgid "Categories"
msgstr "Catégories" msgstr "Catégories"
@ -89,7 +81,7 @@ msgstr "Valeur de fin"
msgid "Start value" msgid "Start value"
msgstr "Valeur de début" msgstr "Valeur de début"
#: .\main\models.py:141 .\main\templates\main\table\snapshot.html:13 #: .\main\models.py:141 .\main\templates\main\table\snapshot.html:9
msgid "Difference" msgid "Difference"
msgstr "Différence" msgstr "Différence"
@ -110,27 +102,26 @@ msgstr "Relevé du %(date)s"
msgid "Statement" msgid "Statement"
msgstr "Relevé" msgstr "Relevé"
#: .\main\models.py:202 .\main\templates\main\account_form.html:24 #: .\main\models.py:202 .\main\templates\main\account_form.html:23
#: .\main\templates\main\list\snapshot.html:6 #: .\main\templates\main\list\snapshot.html:15
#: .\main\templates\main\list\snapshot.html:20
msgid "Statements" msgid "Statements"
msgstr "Relevés" msgstr "Relevés"
#: .\main\models.py:208 .\main\models.py:271 .\main\templates\main\base.html:54 #: .\main\models.py:208 .\main\models.py:271 .\main\templates\main\base.html:44
msgid "Transaction" msgid "Transaction"
msgstr "Transaction" msgstr "Transaction"
#: .\main\models.py:210 .\main\templates\main\table\transaction.html:17 #: .\main\models.py:210 .\main\templates\main\table\transaction.html:12
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
#: .\main\models.py:212 .\main\templates\main\table\snapshot.html:12 #: .\main\models.py:212 .\main\templates\main\table\snapshot.html:8
#: .\main\templates\main\table\transaction.html:9 #: .\main\templates\main\table\transaction.html:8
msgid "Value" msgid "Value"
msgstr "Valeur" msgstr "Valeur"
#: .\main\models.py:214 .\main\templates\main\table\snapshot.html:8 #: .\main\models.py:214 .\main\templates\main\table\snapshot.html:6
#: .\main\templates\main\table\transaction.html:7 #: .\main\templates\main\table\transaction.html:6
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -138,7 +129,7 @@ msgstr "Date"
msgid "Real date" msgid "Real date"
msgstr "Date réelle" msgstr "Date réelle"
#: .\main\models.py:217 .\main\templates\main\table\transaction.html:10 #: .\main\models.py:217 .\main\templates\main\table\transaction.html:9
msgid "Trader" msgid "Trader"
msgstr "Commerçant" msgstr "Commerçant"
@ -146,13 +137,12 @@ msgstr "Commerçant"
msgid "Payment" msgid "Payment"
msgstr "Paiement" msgstr "Paiement"
#: .\main\models.py:272 .\main\templates\main\account_form.html:28 #: .\main\models.py:272 .\main\templates\main\account_form.html:27
#: .\main\templates\main\category_form.html:24 #: .\main\templates\main\category_form.html:25
#: .\main\templates\main\index.html:29 #: .\main\templates\main\index.html:26
#: .\main\templates\main\list\transaction.html:6 #: .\main\templates\main\list\transaction.html:15
#: .\main\templates\main\list\transaction.html:23 #: .\main\templates\main\snapshot_form.html:75
#: .\main\templates\main\snapshot_form.html:76 #: .\main\templates\main\table\snapshot.html:10
#: .\main\templates\main\table\snapshot.html:14
msgid "Transactions" msgid "Transactions"
msgstr "Transactions" msgstr "Transactions"
@ -160,15 +150,19 @@ msgstr "Transactions"
msgid "Invoice" msgid "Invoice"
msgstr "Facture" msgstr "Facture"
#: .\main\models.py:318 .\main\templates\main\transaction_form.html:24 #: .\main\models.py:318 .\main\templates\main\transaction_form.html:23
msgid "Invoices" msgid "Invoices"
msgstr "Factures" msgstr "Factures"
#: .\main\templates\main\base.html:44 #: .\main\templates\main\base.html:54
msgid "Snapshot" msgid "Snapshot"
msgstr "Relevé" msgstr "Relevé"
#: .\main\templates\main\base.html:61 #: .\main\templates\main\base.html:60 .\main\templates\main\base.html:62
msgid "Search"
msgstr "Rechercher"
#: .\main\templates\main\base.html:64
msgid "Log out" msgid "Log out"
msgstr "Se déconnecter" msgstr "Se déconnecter"
@ -187,7 +181,7 @@ msgstr "Confirmer"
#: .\main\templates\main\form\base.html:17 #: .\main\templates\main\form\base.html:17
#: .\main\templates\main\tag\form_buttons.html:4 #: .\main\templates\main\tag\form_buttons.html:4
#: .\main\templates\main\transaction_form.html:35 #: .\main\templates\main\transaction_form.html:34
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
@ -196,19 +190,15 @@ msgstr "Supprimer"
msgid "Save" msgid "Save"
msgstr "Enregistrer" msgstr "Enregistrer"
#: .\main\templates\main\index.html:31 #: .\main\templates\main\index.html:40
msgid "History"
msgstr "Historique"
#: .\main\templates\main\index.html:45
msgid "Snapshots" msgid "Snapshots"
msgstr "Relevés" msgstr "Relevés"
#: .\main\templates\main\list\snapshot.html:40 #: .\main\templates\main\list\snapshot.html:35
msgid "No snapshots to show" msgid "No snapshots to show"
msgstr "Aucun relevé à afficher" msgstr "Aucun relevé à afficher"
#: .\main\templates\main\list\transaction.html:46 #: .\main\templates\main\list\transaction.html:35
msgid "No transactions to show" msgid "No transactions to show"
msgstr "Aucune transaction à afficher" msgstr "Aucune transaction à afficher"
@ -216,37 +206,26 @@ msgstr "Aucune transaction à afficher"
msgid "Log In" msgid "Log In"
msgstr "Se connecter" msgstr "Se connecter"
#: .\main\templates\main\plot\history.html:7 #: .\main\templates\main\table\snapshot.html:11
msgid "Month" msgid "Valid"
msgstr "Mois" msgstr "Valide"
#: .\main\templates\main\plot\history.html:8 #: .\main\templates\main\table\snapshot.html:15
msgid "Expenses"
msgstr "Dépenses"
#: .\main\templates\main\plot\history.html:9
msgid "Income"
msgstr "Revenus"
#: .\main\templates\main\table\snapshot.html:18
msgid "New statement" msgid "New statement"
msgstr "Nouveau relevé" msgstr "Nouveau relevé"
#: .\main\templates\main\table\snapshot.html:53 #: .\main\templates\main\table\snapshot.html:50
msgid "View all statements" msgid "View all statements"
msgstr "Voir tous les relevés" msgstr "Voir tous les relevés"
#: .\main\templates\main\table\transaction.html:21 #: .\main\templates\main\table\transaction.html:16
msgid "New transaction" msgid "New transaction"
msgstr "Ajouter une transaction" msgstr "Ajouter une transaction"
#: .\main\templates\main\table\transaction.html:64 #: .\main\templates\main\table\transaction.html:55
msgid "View all transactions" msgid "View all transactions"
msgstr "Voir toutes les transactions" msgstr "Voir toutes les transactions"
#: .\main\templates\main\transaction_form.html:40 #: .\main\templates\main\transaction_form.html:39
msgid "New invoice" msgid "New invoice"
msgstr "Nouvelle facture" msgstr "Nouvelle facture"
#~ msgid "Valid"
#~ msgstr "Valide"

View file

@ -0,0 +1,59 @@
.chart {
display: grid;
grid-template-columns: auto auto 1fr 1fr auto;
grid-gap: var(--gap) 0;
}
.chart > div {
position: relative;
height: 2rem;
line-height: 2rem;
}
.chart .left {
text-align: right;
}
.chart .bar,
.chart .value {
display: inline-block;
height: 2rem;
line-height: 2rem;
}
.chart .value {
padding: 0 var(--gap);
font-feature-settings: var(--num);
text-align: right;
}
.chart .bar {
width: 0;
box-sizing: border-box;
z-index: 1;
}
.chart .bar.tot {
position: absolute;
z-index: 10;
height: .5rem;
background: black;
}
.chart .left .bar.tot {right: 0}
.chart .right .bar.tot {left: 0}
.chart .left .bar {border-radius: var(--radius) 0 0 var(--radius)}
.chart .right .bar {border-radius: 0 var(--radius) var(--radius) 0}
.chart .bar_m {background: var(--red-1)}
.chart .bar_p {background: var(--green-1)}
.chart .bar span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: .5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num);
}
.chart .right .bar span {left: 0}
.chart .left .bar span {right: 0}

View file

@ -11,7 +11,6 @@
--theme-2: var(--theme); --theme-2: var(--theme);
--theme-3: #802653; --theme-3: #802653;
--theme-4: #cc6699; --theme-4: #cc6699;
--gray: #dedede;
--text-theme: var(--text); --text-theme: var(--text);
@ -37,7 +36,7 @@
--border: .5em; --border: .5em;
--radius: .25em; --radius: .25em;
--num: "tnum", "ss01", "ss02", "case"; --num: "tnum", "ss01", "case";
} }
body { body {

View file

@ -1,87 +0,0 @@
.plot table {
border-collapse: collapse;
width: 100%;
table-layout: auto;
}
.plot thead {
background: var(--bg-01);
}
.plot col.desc, .plot col.value {width: 8rem}
.plot col.icon {width: 1ch}
.plot tr {
padding-bottom: .5rem;
border: .1rem solid var(--gray);
}
.plot th {text-align: left}
.plot th.r {text-align: right}
.plot th.l {text-align: left}
.plot td.c {text-align: center}
.plot td, .plot th, .plot td.bar div {
position: relative;
height: 2rem;
line-height: 2rem;
white-space: nowrap;
}
.plot td, .plot th {
padding: .5rem var(--gap);
}
.plot td.bar {
position: relative;
padding: 0;
}
.plot td.bar div {
position: absolute;
top: .5rem;
}
.plot td.m {
text-align: right;
}
.plot tbody th {
font-feature-settings: var(--num);
}
.plot td.value {
font-feature-settings: var(--num);
text-align: right;
}
.plot td.bar div:not(.tot) {
width: 0;
box-sizing: border-box;
z-index: 1;
display: inline-block;
}
.plot td.bar.p div {
left: 0;
border-radius: 0 var(--radius) var(--radius) 0;
}
.plot td.bar.m div {
right: 0;
border-radius: var(--radius) 0 0 var(--radius);
}
.plot td.bar.m div:not(.tot) {
background: var(--red-1);
}
.plot td.bar.p div:not(.tot) {
background: var(--green-1);
}
.plot td.bar div.tot {
z-index: 10;
height: .5rem;
background: black;
}
.plot td.bar div.tot span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: .5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num);
}
.plot td.bar.p div.tot span {left: 0}
.plot td.bar.m div.tot span {right: 0}

View file

@ -21,6 +21,8 @@
{{ form }} {{ form }}
</form> </form>
{% if form.instance.transactions %} {% if form.instance.transactions %}
<img src="{% url "plot-category" form.instance.id %}"
alt="Graph representing value over time"/>
<h2>{% translate "Transactions" %}</h2> <h2>{% translate "Transactions" %}</h2>
{% include "main/table/transaction.html" %} {% include "main/table/transaction.html" %}
{% endif %} {% endif %}

View file

@ -10,9 +10,6 @@
<link rel="stylesheet" <link rel="stylesheet"
href="{% static 'main/css/table.css' %}" href="{% static 'main/css/table.css' %}"
type="text/css"/> type="text/css"/>
<link rel="stylesheet"
href="{% static 'main/css/plot.css' %}"
type="text/css"/>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if accounts %} {% if accounts %}
@ -28,8 +25,6 @@
{% if transactions %} {% if transactions %}
<h2>{% translate "Transactions" %}</h2> <h2>{% translate "Transactions" %}</h2>
{% include "main/table/transaction.html" %} {% include "main/table/transaction.html" %}
<h2>{% translate "History" %}</h2>
{% include "main/plot/history.html" %}
{% endif %} {% endif %}
{% if categories %} {% if categories %}
<h2>{% translate "Categories" %}</h2> <h2>{% translate "Categories" %}</h2>

View file

@ -1,58 +0,0 @@
{% load main_extras %}
{% load i18n %}
<div class="plot">
<table>
<colgroup>
<col class="desc">
<col class="icon">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col" colspan="2">{% translate "Category" %}</th>
<th class="l" scope="col" colspan="2">{% translate "Expenses" %}</th>
<th class="r" scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% spaceless %}
{% for cat in categories.data %}
<tr>
<th scope="row">
{% if cat.category %}{{ cat.category__name }}{% endif %}
</th>
<td class="c">
{% if cat.category %}
<i class="fa fa-{{ cat.category__icon }}"></i>
{% else %}
<i class="fa fa-wallet"></i>
{% endif %}
</td>
<td class="value">{{ cat.sum_m|pmrvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio cat.sum_m categories.max -100 %}%"></div>
{% if cat.sum < 0 %}
<div class="tot"
style="width:{% widthratio cat.sum categories.max -100 %}%">
<span>{{ cat.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio cat.sum_p categories.max 100 %}%"></div>
{% if cat.sum > 0 %}
<div class="tot"
style="width:{% widthratio cat.sum categories.max 100 %}%">
<span>{{ cat.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ cat.sum_p|pmrvalue }}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
</table>
</div>

View file

@ -1,46 +0,0 @@
{% load main_extras %}
{% load i18n %}
<div class="plot">
<table>
<colgroup>
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{% translate "Month" %}</th>
<th class="l" scope="col" colspan="2">{% translate "Expenses" %}</th>
<th class="r" scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% spaceless %}
{% for date in history.data %}
<tr>
<th scope="row">{{ date.month|date:"Y-m" }}</th>
<td class="value">{{ date.sum_m|pmrvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio date.sum_m history.max -100 %}%"></div>
{% if date.sum < 0 %}
<div class="tot" style="width:{% widthratio date.sum history.max -100 %}%">
<span>{{ date.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio date.sum_p history.max 100 %}%"></div>
{% if date.sum > 0 %}
<div class="tot" style="width:{% widthratio date.sum history.max 100 %}%">
<span>{{ date.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ date.sum_p|pmrvalue }}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
</table>
</div>

View file

@ -12,7 +12,7 @@
href="{% static 'main/css/table.css' %}" href="{% static 'main/css/table.css' %}"
type="text/css"/> type="text/css"/>
<link rel="stylesheet" <link rel="stylesheet"
href="{% static 'main/css/plot.css' %}" href="{% static 'main/css/chart.css' %}"
type="text/css"/> type="text/css"/>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
@ -33,7 +33,44 @@
</form> </form>
{% if categories %} {% if categories %}
<h2>{% translate "Categories" %}</h2> <h2>{% translate "Categories" %}</h2>
{% include "main/plot/category.html" %} <div class="chart">
{% for cat in categories %}
<div class="name">
{% if cat.category %}
<i class="fa fa-{{ cat.category__icon }}"></i>
<a href="{% url 'category' cat.category %}">{{ cat.category__name }}</a>
{% else %}
<i class="fa fa-wallet"></i></i>
{% endif %}
</div>
<div class="value left">{{ cat.sum_m|pmvalue }}</div>
<div class="left">
<div class="bar bar_m"
style="width:{% widthratio cat.sum_m cat_lim_m 100 %}%"
title="{{ cat.sum_m }}"></div>
{% if cat.sum < 0 %}
<div class="bar tot"
style="width:{% widthratio cat.sum cat_lim_m 100 %}%"
title="{{ cat.sum }}">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</div>
<div class="right">
<div class="bar bar_p"
style="width:{% widthratio cat.sum_p cat_lim 100 %}%"
title="{{ cat.sum_p }}"></div>
{% if cat.sum >= 0 %}
<div class="bar tot"
style="width:{% widthratio cat.sum cat_lim 100 %}%"
title="{{ cat.sum }}">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</div>
<div class="value right">{{ cat.sum_p|pmvalue }}</div>
{% endfor %}
</div>
{% endif %} {% endif %}
{% if not snapshot.adding %} {% if not snapshot.adding %}
<h2>{% translate "Transactions" %} ({{ snapshot.sum|pmvalue }} / {{ snapshot.diff|pmvalue }})</h2> <h2>{% translate "Transactions" %} ({{ snapshot.sum|pmvalue }} / {{ snapshot.diff|pmvalue }})</h2>

View file

@ -6,12 +6,12 @@ register = template.Library()
@register.filter @register.filter
def value(val, pm=False, r=2): def value(val, pm=False):
if not val: if not val:
return mark_safe("&ndash;") return mark_safe("&ndash;")
_prefix = "" _prefix = ""
_suffix = "&nbsp;€" _suffix = "&nbsp;€"
_val = formats.number_format(val, r, use_l10n=True, force_grouping=True) _val = formats.number_format(val, 2, use_l10n=True, force_grouping=True)
if val > 0: if val > 0:
if pm: if pm:
@ -28,11 +28,6 @@ def pmvalue(val):
return value(val, True) return value(val, True)
@register.filter
def pmrvalue(val):
return value(val, True, r=0)
@register.inclusion_tag("main/tag/form_buttons.html") @register.inclusion_tag("main/tag/form_buttons.html")
def form_buttons(instance): def form_buttons(instance):
return { return {

View file

@ -41,31 +41,12 @@ class IndexView(LoginRequiredMixin, TemplateView):
_max = 8 _max = 8
_transactions = Transaction.objects.filter(user=self.request.user) _transactions = Transaction.objects.filter(user=self.request.user)
_snapshots = Snapshot.objects.filter(user=self.request.user) _snapshots = Snapshot.objects.filter(user=self.request.user)
_history = (
_transactions.filter(category__budget=True)
.values(month=models.functions.TruncMonth("date"))
.annotate(
sum_p=models.Sum("value", filter=models.Q(value__gt=0)),
sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
sum=models.Sum("value"),
)
.order_by("-month")
)
res = { res = {
"accounts": Account.objects.filter(user=self.request.user), "accounts": Account.objects.filter(user=self.request.user),
"transactions": _transactions[:_max], "transactions": _transactions[:_max],
"categories": Category.objects.filter(user=self.request.user), "categories": Category.objects.filter(user=self.request.user),
"snapshots": _snapshots[:_max], "snapshots": _snapshots[:_max],
"history": {
"data": _history,
"max": max(
_history.aggregate(
max=models.Max("sum_p"),
min=-models.Min("sum_m"),
).values(),
),
},
} }
if _transactions.count() > _max: if _transactions.count() > _max:
res["transactions_url"] = reverse_lazy("transactions") res["transactions_url"] = reverse_lazy("transactions")
@ -251,8 +232,10 @@ class SnapshotUpdateView(NummiUpdateView):
"snapshot_transactions", args=(snapshot.pk,) "snapshot_transactions", args=(snapshot.pk,)
) )
if _transactions: if _transactions:
_categories = ( data["categories"] = (
_transactions.values("category", "category__name", "category__icon") _transactions.values(
"category", "category__name", "category__icon"
)
.annotate( .annotate(
sum=models.Sum("value"), sum=models.Sum("value"),
sum_m=models.Sum("value", filter=models.Q(value__lt=0)), sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
@ -260,15 +243,18 @@ class SnapshotUpdateView(NummiUpdateView):
) )
.order_by("-sum") .order_by("-sum")
) )
data["categories"] = { data["cat_lim"] = max(
"data": _categories, map(
"max": max( lambda x: abs(x) if x else 0,
_categories.aggregate( data["categories"]
.aggregate(
max=models.Max("sum_p"), max=models.Max("sum_p"),
min=models.Min("sum_m"), min=models.Min("sum_m"),
).values(), )
), .values(),
} )
)
data["cat_lim_m"] = -data["cat_lim"]
return data | { return data | {
"new_transaction_url": reverse_lazy( "new_transaction_url": reverse_lazy(

View file

@ -19,6 +19,7 @@ from django.urls import include, path
urlpatterns = i18n_patterns( urlpatterns = i18n_patterns(
path("", include("main.urls")), path("", include("main.urls")),
path("plot/", include("plot.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
prefix_default_language=False, prefix_default_language=False,
) )

0
nummi/plot/__init__.py Normal file
View file

1
nummi/plot/admin.py Normal file
View file

@ -0,0 +1 @@
# Register your models here.

6
nummi/plot/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PlotConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "plot"

View file

1
nummi/plot/models.py Normal file
View file

@ -0,0 +1 @@
# Create your models here.

13
nummi/plot/nummi.mplstyle Normal file
View file

@ -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.axisbelow: True
axes.grid: True
svg.fonttype: none

1
nummi/plot/tests.py Normal file
View file

@ -0,0 +1 @@
# Create your tests here.

9
nummi/plot/urls.py Normal file
View file

@ -0,0 +1,9 @@
from django.urls import path
from . import views
urlpatterns = [
path("timeline", views.timeline, name="plot-timeline"),
path("categories", views.categories, name="plot-categories"),
path("category/<uuid>", views.category, name="plot-category"),
]

108
nummi/plot/views.py Normal file
View file

@ -0,0 +1,108 @@
import io
import matplotlib
import matplotlib.pyplot as plt
from django.contrib.auth.decorators import login_required
from django.db import models
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from matplotlib import dates as mdates
from main.models import Category, Snapshot, Transaction
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())
)
ax.autoscale(True, "x", True)
_io = io.StringIO()
fig.savefig(_io, format="svg")
return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})
@login_required
def categories(request):
_categories = Category.objects.filter(budget=True)
fig, ax = plt.subplots(figsize=(8, _categories.count() / 4))
ax.barh(
[str(c) for c in _categories][::-1],
[
Transaction.objects.filter(category=c).aggregate(sum=models.Sum("value"))[
"sum"
]
for c in _categories
][::-1],
)
_io = io.StringIO()
fig.savefig(_io, format="svg")
return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})
@login_required
def category(request, uuid):
_category = get_object_or_404(Category, id=uuid)
_values_p = (
Transaction.objects.filter(category=_category)
.filter(value__gt=0)
.annotate(m=models.functions.TruncMonth("date"))
.values("m")
.annotate(sum=models.Sum("value"))
.order_by("m")
)
_values_m = (
Transaction.objects.filter(category=_category)
.filter(value__lt=0)
.annotate(m=models.functions.TruncMonth("date"))
.values("m")
.annotate(sum=models.Sum("value"))
.order_by("m")
)
fig, ax = plt.subplots()
ax.bar(
[v["m"] for v in _values_p],
[v["sum"] for v in _values_p],
width=12,
color="#66cc66",
)
ax.bar(
[v["m"] for v in _values_m],
[v["sum"] for v in _values_m],
width=12,
color="#cc6699",
)
ax.xaxis.set_major_formatter(
mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())
)
ax.autoscale(True, "x", True)
_ym, _yp = ax.get_ylim()
ax.set(ylim=(min(_ym, 0), max(_yp, 0)))
ax.set(ylabel=f"{_category.name} (€)")
_io = io.StringIO()
fig.savefig(_io, format="svg")
return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})