Compare commits

..

No commits in common. "main" and "segmentation" have entirely different histories.

146 changed files with 9116 additions and 4527 deletions

View file

@ -1,29 +0,0 @@
name: Build and push Docker image
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: code.edgarpierre.fr
username: ${{ vars.DOCKER_PUSH_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_PASSWORD }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
code.edgarpierre.fr/${{ github.repository }}:${{ github.ref_name }}
code.edgarpierre.fr/${{ github.repository }}:latest

6
.gitignore vendored
View file

@ -1,3 +1,7 @@
/env* env*
__pycache__ __pycache__
*.pkg.tar.zst
/nummi-git/
/pkg/
/src/
/media /media

View file

@ -1,10 +1,4 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: no-commit-to-branch
args: ["--branch", "main"]
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.12.0 rev: 5.12.0
hooks: hooks:
@ -23,9 +17,9 @@ repos:
rev: v1.23.3 rev: v1.23.3
hooks: hooks:
- id: djlint-django - id: djlint-django
args: ["--reformat", "--lint", "--quiet"] args: ["--reformat"]
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.0-alpha.6" rev: "v3.0.0-alpha.6"
hooks: hooks:
- id: prettier - id: prettier
types_or: ["css", "javascript", "svg"] types_or: ["css", "javascript"]

View file

@ -1 +0,0 @@
endOfLine = "auto"

View file

@ -1 +0,0 @@
3.12

22
.vscode/launch.json vendored
View file

@ -1,22 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}\\nummi\\manage.py",
"args": [
"runserver"
],
"env": {
"NUMMI_CONFIG": "${workspaceFolder}\\env\\config.toml"
},
"django": true
}
]
}

View file

@ -1,17 +0,0 @@
FROM ghcr.io/astral-sh/uv:debian-slim
ADD . /app
WORKDIR /app
RUN useradd -m -r nummi && \
chown -R nummi /app
USER nummi
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV NUMMI_CONFIG=/nummi/config.toml
RUN uv sync --locked
CMD ["/app/entrypoint.sh"]

View file

@ -1,5 +1,5 @@
pkgname=nummi pkgname=nummi-git
pkgver=v0.9.1 pkgver=r298.2c95379
pkgrel=1 pkgrel=1
pkgdesc="Web-based accounting interface" pkgdesc="Web-based accounting interface"
arch=("any") arch=("any")
@ -9,20 +9,17 @@ depends=(
"gunicorn" "gunicorn"
"python-django" "python-django"
"python-toml" "python-toml"
"python-psycopg" "python-psycopg2"
"python-dateutil"
"python-pypdf"
) )
makedepends=("git") makedepends=("git")
optdepends=("postgresql: database") optdepends=("postgresql: database")
backup=("etc/${pkgname}/config.toml") backup=("etc/${pkgname%-git}/config.toml")
_tag=bd6447606b8fb6a8f5006ef504f73618b179f9ac
source=( source=(
"${pkgname}::git+https://git.edgarpierre.fr/edpibu/nummi?signed#tag=$_tag" "${pkgname}::git+https://git.edgarpierre.fr/edpibu/nummi"
"${pkgname}.service" "${pkgname%-git}.service"
"${pkgname}.tmpfiles" "${pkgname%-git}.tmpfiles"
"${pkgname}.sysusers" "${pkgname%-git}.sysusers"
"${pkgname}.nginx" "${pkgname%-git}.nginx"
"config.toml" "config.toml"
) )
b2sums=('SKIP' b2sums=('SKIP'
@ -34,16 +31,16 @@ b2sums=('SKIP'
pkgver() { pkgver() {
cd "$pkgname" cd "$pkgname"
git describe printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
} }
package() { package() {
install -Dm644 ${pkgname}.service -t "${pkgdir}"/usr/lib/systemd/system/ install -Dm644 ${pkgname%-git}.service -t "${pkgdir}"/usr/lib/systemd/system/
install -Dm644 ${pkgname}.tmpfiles "${pkgdir}"/usr/lib/tmpfiles.d/${pkgname}.conf install -Dm644 ${pkgname%-git}.tmpfiles "${pkgdir}"/usr/lib/tmpfiles.d/${pkgname%-git}.conf
install -Dm644 ${pkgname}.sysusers "${pkgdir}"/usr/lib/sysusers.d/${pkgname}.conf install -Dm644 ${pkgname%-git}.sysusers "${pkgdir}"/usr/lib/sysusers.d/${pkgname%-git}.conf
install -Dm644 ${pkgname}.nginx "${pkgdir}"/etc/nginx/sites-enabled/${pkgname}.conf install -Dm644 ${pkgname%-git}.nginx "${pkgdir}"/etc/nginx/sites-enabled/${pkgname%-git}.conf
install -Dm600 -o nummi -g nummi config.toml -t "${pkgdir}"/etc/${pkgname}/ install -Dm750 -o nummi -g nummi config.toml -t "${pkgdir}"/etc/${pkgname%-git}/
cd ${pkgname}/${pkgname} cd ${pkgname}/${pkgname%-git}
find * -type f -exec install -Dm0644 "{}" "${pkgdir}/usr/share/webapps/${pkgname}/{}" \; find * -type f -exec install -Dm0644 "{}" "${pkgdir}/usr/share/webapps/${pkgname%-git}/{}" \;
} }

View file

@ -1,23 +0,0 @@
services:
nummi:
image: code.edgarpierre.fr/edpibu/nummi
container_name: nummi
restart: unless-stopped
ports:
- 33001:8000
volumes:
- /docker/nummi/config:/nummi
- /docker/nummi/static:/app/static
- /docker/nummi/media:/app/media
depends_on:
- postgres
postgres:
image: postgres:17-alpine
container_name: nummi_postgres
restart: unless-stopped
environment:
POSTGRES_USER: nummi
POSTGRES_PASSWORD:
volumes:
- /docker/nummi/postgres:/var/lib/postgresql/data

View file

@ -1,5 +0,0 @@
#!/usr/bin/env sh
cd /app/nummi
uv run manage.py collectstatic --noinput
uv run manage.py migrate --noinput
uv run gunicorn --bind :8000 --workers 2 nummi.wsgi:application

View file

@ -1,5 +1,4 @@
from django.forms.widgets import Select from main.forms import NummiForm
from main.forms import IconInput, NummiForm
from .models import Account from .models import Account
@ -11,12 +10,4 @@ class AccountForm(NummiForm):
"name", "name",
"icon", "icon",
"default", "default",
"archived",
] ]
widgets = {
"icon": IconInput(),
}
class AccountSelect(Select):
template_name = "account/forms/widgets/account.html"

View file

@ -1,75 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:17+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\account\models.py:12 .\account\models.py:45 .\account\models.py:53
#: .\account\templates\account\account_list.html:9
msgid "Account"
msgstr "Compte"
#: .\account\models.py:12
msgid "Name"
msgstr "Nom"
#: .\account\models.py:16
msgid "Icon"
msgstr "Icône"
#: .\account\models.py:18
msgid "Default"
msgstr "Défaut"
#: .\account\models.py:19
msgid "Archived"
msgstr "Archivé"
#: .\account\models.py:46 .\account\templates\account\account_list.html:12
msgid "Accounts"
msgstr "Comptes"
#: .\account\templates\account\account_detail.html:15
msgid "Edit account"
msgstr "Modifier le compte"
#: .\account\templates\account\account_detail.html:18
msgid "Statements"
msgstr "Relevés"
#: .\account\templates\account\account_detail.html:24
msgid "History"
msgstr "Historique"
#: .\account\templates\account\account_form.html:5
#: .\account\templates\account\account_table.html:35
msgid "Create account"
msgstr "Créer un compte"
#: .\account\templates\account\account_form.html:8
msgid "New account"
msgstr "Nouveau compte"
#: .\account\templates\account\account_table.html:16
msgid "All accounts"
msgstr "Tous les comptes"
#: .\account\templates\account\account_table.html:26
msgid "Show archived"
msgstr "Afficher archivés"
#~ msgid "Transactions"
#~ msgstr "Transactions"

View file

@ -1,17 +0,0 @@
# Generated by Django 4.2 on 2024-12-29 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("account", "0002_alter_account_table"),
]
operations = [
migrations.AddField(
model_name="account",
name="archived",
field=models.BooleanField(default=False, verbose_name="Archived"),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.7 on 2025-01-04 17:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("account", "0003_account_archived"),
]
operations = [
migrations.AlterModelOptions(
name="account",
options={
"ordering": ["-default", "archived", "name"],
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
},
),
]

View file

@ -1,13 +1,12 @@
from uuid import uuid4 from uuid import uuid4
from django.apps import apps
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import NummiModel from main.models import UserModel
class Account(NummiModel): class Account(UserModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name")) name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
icon = models.SlugField( icon = models.SlugField(
@ -16,7 +15,6 @@ class Account(NummiModel):
verbose_name=_("Icon"), verbose_name=_("Icon"),
) )
default = models.BooleanField(default=False, verbose_name=_("Default")) default = models.BooleanField(default=False, verbose_name=_("Default"))
archived = models.BooleanField(default=False, verbose_name=_("Archived"))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.default: if self.default:
@ -34,19 +32,13 @@ class Account(NummiModel):
def get_delete_url(self): def get_delete_url(self):
return reverse("del_account", args=(self.pk,)) return reverse("del_account", args=(self.pk,))
@property
def transactions(self):
return apps.get_model("transaction", "Transaction").objects.filter(
statement__account=self
)
class Meta: class Meta:
ordering = ["-default", "archived", "name"] ordering = ["-default", "name"]
verbose_name = _("Account") verbose_name = _("Account")
verbose_name_plural = _("Accounts") verbose_name_plural = _("Accounts")
class AccountModel(NummiModel): class AccountModel(UserModel):
account = models.ForeignKey( account = models.ForeignKey(
Account, Account,
on_delete=models.CASCADE, on_delete=models.CASCADE,

View file

@ -1,27 +0,0 @@
{% extends "main/base.html" %}
{% load main_extras history_extras statement_extras %}
{% load i18n %}
{% block title %}
{{ account }} {{ block.super }}
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
<h2>{{ account.icon|remix }}{{ account }}</h2>
<p>
<a href="{% url "edit_account" account.pk %}">{{ "edit"|remix }}{% translate "Edit account" %}</a>
</p>
<section>
<h3>{% translate "Statements" %}</h3>
{% url "new_statement" account=account.pk as ns_url %}
{% url "account_statements" account=account.pk as s_url %}
{% statement_table account.statement_set.all statements_url=s_url new_statement_url=ns_url n_max=6 %}
</section>
<section>
<h3>{% translate "History" %}</h3>
{% history_plot account.transactions account=account %}
</section>
{% endblock body %}

View file

@ -2,9 +2,21 @@
{% load main_extras %} {% load main_extras %}
{% load i18n %} {% load i18n %}
{% block title_new %} {% block title_new %}
{% translate "Create account" %} {% translate "Create account" %}
{% endblock %} {% endblock %}
{% block h2_new %} {% block h2_new %}
{% translate "New account" %} {% translate "New account" %}
{% endblock %} {% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %} {% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
{% block tables %}
{% if not form.instance|adding %}
<h3>{% translate "Statements" %}</h3>
{% include "main/table/statement.html" %}
{% endif %}
{% if transactions %}
<h3>{% translate "Transactions" %}</h3>
{% include "main/table/transaction.html" %}
<h3>{% translate "History" %}</h3>
{% include "main/plot/history.html" %}
{% endif %}
{% endblock %}

View file

@ -1,16 +0,0 @@
{% extends "main/list.html" %}
{% load i18n main_extras account_extras %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block name %}
{% translate "Account" %}
{% endblock name %}
{% block h2 %}
{% translate "Accounts" %}
{% endblock h2 %}
{% block table %}
<div class="split">{% account_table accounts %}</div>
{% endblock table %}

View file

@ -1,39 +0,0 @@
{% load i18n main_extras %}
<dl class="accounts">
{% for acc in accounts %}
<div class="account {% if not search and acc.archived %}archived{% endif %}">
<dt>
<a href="{{ acc.get_absolute_url }}">{{ acc.icon|remix }}{{ acc }}</a>
</dt>
<dd class="value">
{% if acc.statement_set.first %}{{ acc.statement_set.first.value|value }}{% endif %}
</dd>
</div>
{% endfor %}
{% if index %}
<div class="more account">
<dt>
<a href="{% url "accounts" %}">{{ "gallery-view"|remixnl }}{% translate "All accounts" %}</a>
</dt>
<dd class="value">
{{ accounts|balance|value }}
</dd>
</div>
{% elif not search %}
<div class="more account">
<dt>
<label class="wi" for="show-archived-accounts">
{{ "archive"|remix }}{% translate "Show archived" %}
</label>
</dt>
<dd>
<input type="checkbox" class="show-archived" id="show-archived-accounts" />
</dd>
</div>
<div class="new account">
<dt>
<a href="{% url "new_account" %}">{{ "add-box"|remix }}{% translate "Create account" %}</a>
</dt>
</div>
{% endif %}
</dl>

View file

@ -1,5 +0,0 @@
{% load main_extras %}
<span class="ico-input account-select">
{{ "bank"|remix }}
{% include "django/forms/widgets/select.html" %}
</span>

View file

@ -1,10 +0,0 @@
from django import template
register = template.Library()
@register.inclusion_tag("account/account_table.html")
def account_table(accounts, **kwargs):
return kwargs | {
"accounts": accounts,
}

View file

@ -1,13 +1,12 @@
from django.urls import path from django.urls import path
from statement.views import StatementCreateView from statement.views import StatementCreateView
from transaction.views import TransactionMonthView
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("list", views.AccountListView.as_view(), name="accounts"), path("", views.AccountCreateView.as_view(), name="new_account"),
path("new", views.AccountCreateView.as_view(), name="new_account"), path("<account>", views.AccountUpdateView.as_view(), name="account"),
path("<account>", views.AccountDetailView.as_view(), name="account"),
path("<account>/edit", views.AccountUpdateView.as_view(), name="edit_account"),
path( path(
"<account>/transactions", "<account>/transactions",
views.AccountTListView.as_view(), views.AccountTListView.as_view(),
@ -28,4 +27,9 @@ urlpatterns = [
views.AccountDeleteView.as_view(), views.AccountDeleteView.as_view(),
name="del_account", name="del_account",
), ),
path(
"<account>/history/<int:year>/<int:month>",
TransactionMonthView.as_view(),
name="transaction_month",
),
] ]

View file

@ -1,12 +1,8 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from main.views import ( from django.urls import reverse_lazy
NummiCreateView, from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
NummiDeleteView,
NummiDetailView,
NummiListView,
NummiUpdateView,
)
from statement.views import StatementListView from statement.views import StatementListView
from transaction.utils import history
from transaction.views import TransactionListView from transaction.views import TransactionListView
from .forms import AccountForm from .forms import AccountForm
@ -23,18 +19,37 @@ class AccountUpdateView(NummiUpdateView):
form_class = AccountForm form_class = AccountForm
pk_url_kwarg = "account" pk_url_kwarg = "account"
def get_context_data(self, **kwargs):
_max = 8
data = super().get_context_data(**kwargs)
account = data["form"].instance
_transactions = account.transaction_set.all()
if _transactions.count() > _max:
data["transactions_url"] = reverse_lazy(
"account_transactions", args=(account.pk,)
)
_statements = account.statement_set.all()
if _statements.count() > _max:
data["statements_url"] = reverse_lazy(
"account_statements", args=(account.pk,)
)
return data | {
"transactions": _transactions[:8],
"new_statement_url": reverse_lazy(
"new_statement", kwargs={"account": account.pk}
),
"statements": _statements[:8],
"history": history(account.transaction_set),
}
class AccountDeleteView(NummiDeleteView): class AccountDeleteView(NummiDeleteView):
model = Account model = Account
pk_url_kwarg = "account" pk_url_kwarg = "account"
class AccountDetailView(NummiDetailView):
model = Account
pk_url_kwarg = "account"
context_object_name = "account"
class AccountMixin: class AccountMixin:
def get_queryset(self): def get_queryset(self):
self.account = get_object_or_404( self.account = get_object_or_404(
@ -47,11 +62,6 @@ class AccountMixin:
return super().get_context_data(**kwargs) | {"account": self.account} return super().get_context_data(**kwargs) | {"account": self.account}
class AccountListView(NummiListView):
model = Account
context_object_name = "accounts"
class AccountTListView(AccountMixin, TransactionListView): class AccountTListView(AccountMixin, TransactionListView):
pass pass

View file

@ -1,5 +1,4 @@
from django.forms.widgets import Select from main.forms import NummiForm
from main.forms import IconInput, NummiForm
from .models import Category from .models import Category
@ -12,10 +11,3 @@ class CategoryForm(NummiForm):
"icon", "icon",
"budget", "budget",
] ]
widgets = {
"icon": IconInput,
}
class CategorySelect(Select):
template_name = "category/forms/widgets/category.html"

View file

@ -1,83 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:18+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\category\models.py:12 .\category\models.py:32
#: .\category\templates\category\category_plot.html:13
msgid "Category"
msgstr "Catégorie"
#: .\category\models.py:12
msgid "Name"
msgstr "Nom"
#: .\category\models.py:17
msgid "Icon"
msgstr "Icône"
#: .\category\models.py:19
msgid "Budget"
msgstr "Budget"
#: .\category\models.py:33
msgid "Categories"
msgstr "Catégories"
#: .\category\templates\category\category_detail.html:14
msgid "Edit category"
msgstr "Modifier la catégorie"
#: .\category\templates\category\category_detail.html:17
msgid "Transactions"
msgstr "Transactions"
#: .\category\templates\category\category_detail.html:20
msgid "View all transactions"
msgstr "Voir toutes les transactions"
#: .\category\templates\category\category_detail.html:25
msgid "History"
msgstr "Historique"
#: .\category\templates\category\category_form.html:5
msgid "Create category"
msgstr "Créer une catégorie"
#: .\category\templates\category\category_form.html:8
msgid "New category"
msgstr "Nouvelle catégorie"
#: .\category\templates\category\category_plot.html:14
msgid "Expenses"
msgstr "Dépenses"
#: .\category\templates\category\category_plot.html:15
msgid "Income"
msgstr "Revenus"
#: .\category\templates\category\category_plot.html:58
msgid "No transaction"
msgstr "Aucune transaction"
#: .\category\templates\category\category_plot.html:66
msgid "Total"
msgstr "Total"
#: .\category\templates\category\category_plot.html:89
msgid "Expected total"
msgstr "Total attendu"

View file

@ -3,10 +3,10 @@ from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import NummiModel from main.models import UserModel
class Category(NummiModel): class Category(UserModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField( name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name") max_length=64, default=_("Category"), verbose_name=_("Name")

View file

@ -1,28 +0,0 @@
{% extends "main/base.html" %}
{% load i18n main_extras history_extras transaction_extras %}
{% block title %}
{{ category }} {{ block.super }}
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
<h2>{{ category.icon|remix }}{{ category }}</h2>
<p>
<a href="{% url "edit_category" category.pk %}">{{ "edit"|remix }}{% translate "Edit category" %}</a>
</p>
<section>
<h3>{% translate "Transactions" %}</h3>
{% url_get "transactions" category=category.id as t_url %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "View all transactions" %}</a>
</p>
{% transaction_table category.transaction_set.all n_max=8 transactions_url=t_url %}
</section>
<section>
<h3>{% translate "History" %}</h3>
{% history_plot category.transaction_set category=category %}
</section>
{% endblock body %}

View file

@ -2,9 +2,19 @@
{% load main_extras %} {% load main_extras %}
{% load i18n %} {% load i18n %}
{% block title_new %} {% block title_new %}
{% translate "Create category" %} {% translate "Create category" %}
{% endblock %} {% endblock %}
{% block h2_new %} {% block h2_new %}
{% translate "New category" %} {% translate "New category" %}
{% endblock %} {% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %} {% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
{% block tables %}
{% if transactions %}
<h3>{% translate "Transactions" %}</h3>
{% include "main/table/transaction.html" %}
{% endif %}
{% if history.data %}
<h3>{% translate "History" %}</h3>
{% include "main/plot/history.html" %}
{% endif %}
{% endblock %}

View file

@ -1,114 +0,0 @@
{% load main_extras statement_extras history_extras %}
{% load i18n %}
<div class="plot">
<table class="full-width">
<colgroup>
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{% translate "Category" %}</th>
<th scope="col" colspan="2">{% translate "Expenses" %}</th>
<th scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% spaceless %}
{% for cat in categories %}
<tr>
<th scope="row" class="l wi">
{% if cat.category %}
{% if year %}
<a href="{% history_url year=year category=cat.category account=account.id %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% elif month %}
<a href="{% url_get "transactions" start_date=month end_date=month|end_of_month account=account.id category=cat.category %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% elif statement %}
<a href="{% url_get "transactions" account=statement.account.id statement=statement.id category=cat.category %}">{{ cat.category_.icon|remix }}{{ cat.category__name }}</a>
{% elif account %}
<a href="{% url_get "transactions" account=account.id category=cat.category %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% else %}
{{ cat.category__icon|remix }}{{ cat.category__name }}
{% endif %}
{% endif %}
</th>
<td class="value">{{ cat.sum_m|pmvalue }}</td>
<td class="bar m">
{% if cat.sum_m %}
<div style="width: {% widthratio cat.sum_m max -100 %}%"></div>
{% endif %}
{% if cat.sum < 0 %}
<div class="tot" style="width:{% widthratio cat.sum max -100 %}%">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
{% if cat.sum_p %}
<div style="width: {% widthratio cat.sum_p max 100 %}%"></div>
{% endif %}
{% if cat.sum > 0 %}
<div class="tot" style="width:{% widthratio cat.sum max 100 %}%">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ cat.sum_p|pmvalue }}</td>
</tr>
{% empty %}
<tr>
<td class="empty" colspan="5">{% translate "No transaction" %}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
<tfoot>
{% if categories %}
<tr>
<th scope="row" class="l">{% translate "Total" %}</th>
<td class="value">{{ total_m|pmvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio total_m max -100 %}%"></div>
{% if total < 0 %}
<div class="tot" style="width:{% widthratio total max -100 %}%">
<span>{{ total|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio total_p max 100 %}%"></div>
{% if total > 0 %}
<div class="tot" style="width:{% widthratio total max 100 %}%">
<span>{{ total|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ total_p|pmvalue }}</td>
</tr>
{% endif %}
{% if statement and statement.diff != statement.sum %}
<tr>
<th scope="row" class="l">{% translate "Expected total" %}</th>
<td class="c">{{ total|check:statement.diff }}</td>
<td class="bar m">
{% if statement.diff < 0 %}
<div class="tot" style="width:{% widthratio statement.diff max -100 %}%">
<span>{{ statement.diff|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
{% if statement.diff >= 0 %}
<div class="tot" style="width:{% widthratio statement.diff max 100 %}%">
<span>{{ statement.diff|pmvalue }}</span>
</div>
{% endif %}
</td>
<td></td>
</tr>
{% endif %}
</tfoot>
</table>
</div>

View file

@ -1,5 +0,0 @@
{% load main_extras %}
<span class="ico-input category-select">
{{ "folder"|remix }}
{% include "django/forms/widgets/select.html" %}
</span>

View file

@ -1,34 +0,0 @@
from django import template
from django.db import models
from django.db.models.functions import Greatest
register = template.Library()
@register.inclusion_tag("category/category_plot.html", takes_context=True)
def category_plot(context, transactions, **kwargs):
kwargs.setdefault("account", context.get("account"))
if not kwargs.get("account"):
transactions = transactions.exclude(category__budget=False)
categories = (
transactions.values("category", "category__name", "category__icon")
.annotate(
sum=models.Sum("value"),
sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
sum_p=models.Sum("value", filter=models.Q(value__gt=0)),
)
.order_by("-sum")
)
return (
kwargs
| {
"categories": categories,
}
| categories.aggregate(
max=Greatest(-models.Sum("sum_m"), models.Sum("sum_p")),
total_m=models.Sum("sum_m"),
total_p=models.Sum("sum_p"),
total=models.Sum("sum"),
)
)

View file

@ -1,15 +1,20 @@
from django.urls import path from django.urls import path
from transaction.views import TransactionMonthView
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("new", views.CategoryCreateView.as_view(), name="new_category"), path("", views.CategoryCreateView.as_view(), name="new_category"),
path("<category>", views.CategoryDetailView.as_view(), name="category"), path("<category>", views.CategoryUpdateView.as_view(), name="category"),
path("<category>/edit", views.CategoryUpdateView.as_view(), name="edit_category"),
path( path(
"<category>/transactions", "<category>/transactions",
views.CategoryTListView.as_view(), views.CategoryTListView.as_view(),
name="category_transactions", name="category_transactions",
), ),
path("<category>/delete", views.CategoryDeleteView.as_view(), name="del_category"), path("<category>/delete", views.CategoryDeleteView.as_view(), name="del_category"),
path(
"<category>/history/<int:year>/<int:month>",
TransactionMonthView.as_view(),
name="transaction_month",
),
] ]

View file

@ -1,10 +1,7 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from main.views import ( from django.urls import reverse_lazy
NummiCreateView, from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
NummiDeleteView, from transaction.utils import history
NummiDetailView,
NummiUpdateView,
)
from transaction.views import TransactionListView from transaction.views import TransactionListView
from .forms import CategoryForm from .forms import CategoryForm
@ -21,11 +18,17 @@ class CategoryUpdateView(NummiUpdateView):
form_class = CategoryForm form_class = CategoryForm
pk_url_kwarg = "category" pk_url_kwarg = "category"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
category = data["form"].instance
class CategoryDetailView(NummiDetailView): return data | {
model = Category "transactions": category.transaction_set.all()[:8],
pk_url_kwarg = "category" "transactions_url": reverse_lazy(
context_object_name = "category" "category_transactions", args=(category.pk,)
),
"history": history(category.transaction_set),
}
class CategoryDeleteView(NummiDeleteView): class CategoryDeleteView(NummiDeleteView):

View file

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

View file

@ -1,38 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:18+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\history\templates\history\plot.html:10
msgid "Year"
msgstr "Année"
#: .\history\templates\history\plot.html:13
msgid "Total"
msgstr "Total"
#: .\history\templates\history\plot.html:56
msgid "Month"
msgstr "Mois"
#: .\history\templates\history\plot.html:57
msgid "Expenses"
msgstr "Dépenses"
#: .\history\templates\history\plot.html:58
msgid "Income"
msgstr "Revenus"

View file

@ -1,25 +0,0 @@
{% load history_extras %}
{% if month %}
<p class="pagination">
<a href="{% history_url year=month %}">{{ month.year }}</a>
</p>
{% endif %}
<p class="pagination n3">
{% if month %}
{% if previous_month %}
<a href="{% history_url month=previous_month %}">{{ previous_month|date:"F Y"|capfirst }}</a>
{% endif %}
<a class="cur" href="{% history_url month=month %}">{{ month|date:"F Y"|capfirst }}</a>
{% if next_month %}
<a href="{% history_url month=next_month %}">{{ next_month|date:"F Y"|capfirst }}</a>
{% endif %}
{% elif year %}
{% if previous_year %}
<a href="{% history_url year=previous_year %}">{{ previous_year|date:"Y" }}</a>
{% endif %}
<a class="cur" href="{% history_url year=year %}">{{ year|date:"Y" }}</a>
{% if next_year %}
<a href="{% history_url year=next_year %}">{{ next_year|date:"Y" }}</a>
{% endif %}
{% endif %}
</p>

View file

@ -1,92 +0,0 @@
{% load main_extras %}
{% load history_extras %}
{% load transaction_extras %}
{% load i18n %}
<div class="calendar">
<table>
<thead>
<tr>
{% if not year %}
<th scope="col">{% translate "Year" %}</th>
{% endif %}
{% calendar_head %}
<th scope="col">{% translate "Total" %}</th>
</tr>
</thead>
<tbody>
{% regroup history.data by month.year as years_list %}
{% for y, y_data in years_list reversed %}
<tr>
{% if not year %}
<th class="date" scope="row">
<a href="{% history_url year=y account=account.id category=category.id %}">{{ y }}</a>
</th>
{% endif %}
{% for m in y_data %}
{% if forloop.parentloop.last and forloop.first %}
{% empty_calendar_cells_start m.month.month %}
{% endif %}
{% if m %}
<td class="month {% if m.sum > 0 %}p{% else %}m{% endif %}"
style="--opacity: {% calendar_opacity m.sum history.max.sum %}"
title="{{ m.sum|pmrvalue }}">{% up_down_icon m.sum %}</td>
{% else %}
<td class="month"></td>
{% endif %}
{% if forloop.parentloop.first and forloop.last %}
{% empty_calendar_cells_end m.month.month %}
{% endif %}
{% endfor %}
<td class="total">{{ y_data|sum_year|pmrvalue }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="history plot">
<table class="full-width">
<colgroup>
<col class="icon">
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{{ "expand-up-down"|remix }}</th>
<th scope="col">{% translate "Month" %}</th>
<th scope="col" colspan="2">{% translate "Expenses" %}</th>
<th scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% for date in history.data reversed %}
{% ifchanged %}
{% if date.sum_m or date.sum_p %}
<tr {% if not date.month.month|divisibleby:"2" %}class="even"{% endif %}>
<td class="icon">{% up_down_icon date.sum %}</td>
<th class="date" scope="row">
<a href="{% history_url year=date.month.year month=date.month.month account=account.id category=category.id %}">
{% if year %}
{{ date.month|date:"F"|capfirst }}
{% else %}
{{ date.month|date:"Y-m" }}
{% endif %}
</a>
</th>
<td class="value">{{ date.sum_m|pmrvalue }}</td>
<td class="bar m">{% plot_bar date.sum date.sum_m history.max.pm %}</td>
<td class="bar p">{% plot_bar date.sum date.sum_p history.max.pm %}</td>
<td class="value">{{ date.sum_p|pmrvalue }}</td>
</tr>
{% else %}
<tr class="empty">
<td colspan="6" class="empty"></td>
</tr>
{% endif %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
</div>

View file

@ -1,51 +0,0 @@
{% extends "main/base.html" %}
{% load i18n static main_extras transaction_extras category history_extras %}
{% block link %}
{{ block.super }}
{% css "main/css/plot.css" %}
{% css "main/css/table.css" %}
{% endblock link %}
{% block body %}
<h2>
{% block h2 %}
{% endblock h2 %}
</h2>
{% history_pagination %}
{% if account or category %}
<p class="back">
<a class="big-link"
href="{% history_url year=year month=month clear=True %}">{{ "arrow-go-back"|remix }}{% translate "Back" %}</a>
{% if account %}
<a class="big-link" href="{% url "account" account.id %}">{{ account.icon|remix }}{{ account }}</a>
{% endif %}
{% if category %}
<a class="big-link" href="{% url "category" category.id %}">{{ category.icon|remix }}{{ category }}</a>
{% endif %}
</p>
{% endif %}
{% if history %}
<section>
<h3>{% translate "History" %}</h3>
{% include "history/plot.html" %}
</section>
{% endif %}
{% if not category %}
<section>
<h3>{% translate "Categories" %}</h3>
{% category_plot transactions month=month year=year %}
</section>
{% endif %}
<section>
<h3>{% translate "Transactions" %}</h3>
{% if month %}
{% url_get "transactions" start_date=month end_date=month|end_of_month category=category.id account=account.id as t_url %}
{% elif year %}
{% url_get "transactions" start_date=year end_date=year|end_of_year category=category.id account=account.id as t_url %}
{% endif %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "View all transactions" %}</a>
</p>
{% transaction_table transactions n_max=8 transactions_url=t_url %}
</section>
{% history_pagination %}
{% endblock body %}

View file

@ -1,8 +0,0 @@
{% extends "history/transaction_archive.html" %}
{% load i18n static main_extras transaction_extras category %}
{% block title %}
{{ month|date:"F Y"|capfirst }} {{ block.super }}
{% endblock title %}
{% block h2 %}
{{ month|date:"F Y"|capfirst }}
{% endblock h2 %}

View file

@ -1,8 +0,0 @@
{% extends "history/transaction_archive.html" %}
{% load i18n static main_extras transaction_extras category %}
{% block title %}
{{ year|date:"Y" }} {{ block.super }}
{% endblock title %}
{% block h2 %}
{{ year|date:"Y" }}
{% endblock h2 %}

View file

@ -1,119 +0,0 @@
import datetime
import math
from urllib import parse
from django import template
from django.urls import reverse
from django.utils.safestring import mark_safe
from history.utils import history
from main.templatetags.main_extras import pmrvalue, remix
register = template.Library()
@register.inclusion_tag("history/plot.html", takes_context=True)
def history_plot(context, transactions, **kwargs):
kwargs.setdefault("account", context.get("account"))
kwargs.setdefault("category", context.get("category"))
if kwargs.get("category") or kwargs.get("account"):
kwargs["history"] = history(transactions.all())
else:
kwargs["history"] = history(transactions.exclude(category__budget=False))
return kwargs
@register.simple_tag
def calendar_opacity(v, vmax):
if v is None:
return "0%"
return f"{math.sin(min(1, math.fabs(v/vmax))*math.pi/2): .0%}"
@register.simple_tag
def empty_calendar_cells(n):
return mark_safe(n * "<td></td>")
@register.simple_tag
def empty_calendar_cells_start(n):
return empty_calendar_cells(n - 1)
@register.simple_tag
def empty_calendar_cells_end(n):
return empty_calendar_cells(12 - n)
@register.simple_tag
def up_down_icon(val):
if val is None:
return ""
if val > 0:
return remix("arrow-up-s", "green")
elif val < 0:
return remix("arrow-down-s", "red")
return remix("equal", "white")
@register.simple_tag
def plot_bar(s, sum_pm, s_max):
_res = ""
if s_max:
if sum_pm:
_w = abs(sum_pm / s_max)
_res += f"""<div style="width: {_w: .1%}"></div>"""
if sum_pm is not None and s * sum_pm > 0:
_w = abs(s / s_max)
_res += (
f"""<div class="tot" style="width: {_w: .1%}">"""
f"""<span>{pmrvalue(s)}</span></div>"""
)
else:
_res += "<div></div>"
return mark_safe(_res)
@register.simple_tag
def calendar_head():
months = range(1, 13)
th = (f"""<th>{month: 02d}</th>""" for month in months)
return mark_safe("".join(th))
@register.filter
def sum_year(y_data):
return sum(y["sum"] or 0 for y in y_data)
@register.inclusion_tag("history/pagination.html", takes_context=True)
def history_pagination(context):
return context
@register.simple_tag(takes_context=True)
def history_url(context, month=None, year=None, clear=False, **kwargs):
if not clear:
kwargs.setdefault("account", getattr(context.get("account"), "id", None))
kwargs.setdefault("category", getattr(context.get("category"), "id", None))
if month:
if isinstance(month, datetime.date):
year = month.year
month = month.month
url = reverse("history:month", kwargs={"year": year, "month": month})
elif year:
if isinstance(year, datetime.date):
year = year.year
url = reverse("history:year", kwargs={"year": year})
kwargs = {k: v for k, v in kwargs.items() if v}
if kwargs:
return f"{url}?{parse.urlencode(kwargs)}"
return url

View file

@ -1,13 +0,0 @@
from django.urls import path
from . import views
app_name = "history"
urlpatterns = [
path(
"month/<int:year>/<int:month>",
views.TransactionMonthView.as_view(),
name="month",
),
path("year/<int:year>", views.TransactionYearView.as_view(), name="year"),
]

View file

@ -1,54 +0,0 @@
import datetime
from django.db.models import Q, Sum
from django.db.models.functions import Abs, Greatest, TruncMonth
def history(transaction_set):
if not transaction_set.exists():
return None
_transaction_month = transaction_set.values(month=TruncMonth("date")).order_by(
"-date"
)
_first_month = _transaction_month.last()["month"]
_last_month = _transaction_month.first()["month"]
_history = (
_transaction_month.annotate(
sum_p=Sum("value", filter=Q(value__gt=0), default=0),
sum_m=Sum("value", filter=Q(value__lt=0), default=0),
sum=Sum("value"),
)
.annotate(max_sum=Greatest("sum_p", Abs("sum_m")))
.order_by("-month")
)
_data = [
_history.filter(month=datetime.date(y, m + 1, 1)).first()
or {"month": datetime.date(y, m + 1, 1), "sum": None}
for y in range(
_first_month.year,
_last_month.year + 1,
)
for m in range(
_first_month.month - 1 if _first_month.year == y else 0,
_last_month.month if _last_month.year == y else 12,
)
]
return {
"data": _data,
"max": {
"pm": 125
* _history.order_by("-max_sum")[len(_history.exclude(max_sum=0)) // 10][
"max_sum"
]
/ 100,
"sum": 125
* _history.annotate(abs_sum=Abs("sum")).order_by("-abs_sum")[
len(_history) // 10
]["abs_sum"]
/ 100,
},
}

View file

@ -1,54 +0,0 @@
from django.shortcuts import get_object_or_404
from django.views.generic.dates import MonthArchiveView, YearArchiveView
from history.utils import history
from main.views import UserMixin
from transaction.models import Transaction
class ACFilterMixin:
def get_queryset(self):
queryset = super().get_queryset()
if account := self.request.GET.get("account"):
queryset = queryset.filter(statement__account=account)
if category := self.request.GET.get("category"):
queryset = queryset.filter(category=category)
return queryset
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
if account := self.request.GET.get("account"):
context_data["account"] = get_object_or_404(
self.request.user.account_set, pk=account
)
if category := self.request.GET.get("category"):
context_data["category"] = get_object_or_404(
self.request.user.category_set, pk=category
)
return context_data
class TransactionMonthView(UserMixin, ACFilterMixin, MonthArchiveView):
model = Transaction
date_field = "date"
context_object_name = "transactions"
month_format = "%m"
template_name = "history/transaction_month.html"
class TransactionYearView(UserMixin, ACFilterMixin, YearArchiveView):
model = Transaction
date_field = "date"
context_object_name = "transactions"
make_object_list = True
template_name = "history/transaction_year.html"
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
h_data = context_data.get("transactions")
if not (context_data.get("account") or context_data.get("category")):
h_data = h_data.exclude(category__budget=False)
context_data["history"] = history(h_data)
return context_data

View file

@ -1,7 +1,4 @@
from django import forms from django import forms
from django.forms.widgets import TextInput
from .utils import get_icons
class NummiFileInput(forms.ClearableFileInput): class NummiFileInput(forms.ClearableFileInput):
@ -10,42 +7,6 @@ class NummiFileInput(forms.ClearableFileInput):
class NummiForm(forms.ModelForm): class NummiForm(forms.ModelForm):
template_name = "main/form/form_base.html" template_name = "main/form/form_base.html"
meta_fieldsets = []
def __init__(self, *args, **kwargs): def __init__(self, *args, user, **kwargs):
kwargs.pop("user", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property
def fieldsets(self):
if self.meta_fieldsets:
for group in self.meta_fieldsets:
yield ((self[f] for f in fieldset) for fieldset in group)
else:
yield ([f] for f in self)
class DatalistInput(TextInput):
template_name = "main/forms/widgets/datalist.html"
def __init__(self, *args, options=[]):
self.options = options
super().__init__(*args)
def get_context(self, *args):
context = super().get_context(*args)
name = context["widget"]["name"]
context["widget"]["attrs"]["list"] = f"{name}-list"
context["widget"]["attrs"]["autocomplete"] = "off"
context["widget"]["options"] = self.options
return context
class IconInput(DatalistInput):
template_name = "main/forms/widgets/icon.html"
icon_list = get_icons()
def get_context(self, *args):
context = super().get_context(*args)
context["widget"]["options"] = self.icon_list
return context

Binary file not shown.

View file

@ -0,0 +1,324 @@
# NUMMI.
# Copyright (C) 2022
# This file is distributed under the same license as the nummi package.
# edpibu <git@edgarpierre.fr>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-22 09:41+0200\n"
"PO-Revision-Date: 2022-12-21 17:30+0100\n"
"Last-Translator: edpibu <git@edgarpierre.fr>\n"
"Language-Team: edpibu <git@edgarpierre.fr>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\forms.py:95
msgid "Add transactions"
msgstr "Ajouter des transactions"
#: .\forms.py:113 .\templates\main\base.html:70
#: .\templates\main\form\search.html:5 .\templates\main\list\transaction.html:9
#: .\templates\main\list\transaction.html:26 .\templates\main\search.html:6
#: .\templates\main\search.html:18
msgid "Search"
msgstr "Rechercher"
#: .\models.py:18
msgid "User"
msgstr "Utilisateur"
#: .\models.py:37 .\models.py:71 .\models.py:79 .\models.py:229
#: .\templates\main\table\snapshot.html:24
#: .\templates\main\table\transaction.html:35
msgid "Account"
msgstr "Compte"
#: .\models.py:37 .\models.py:89 .\models.py:200 .\models.py:270
#: .\templates\main\table\invoice.html:9
#: .\templates\main\table\transaction.html:28
msgid "Name"
msgstr "Nom"
#: .\models.py:41 .\models.py:94
msgid "Icon"
msgstr "Icône"
#: .\models.py:43
msgid "Default"
msgstr "Défaut"
#: .\models.py:72 .\templates\main\index.html:16
msgid "Accounts"
msgstr "Comptes"
#: .\models.py:89 .\models.py:113 .\models.py:219
#: .\templates\main\plot\category.html:14
#: .\templates\main\table\transaction.html:32
msgid "Category"
msgstr "Catégorie"
#: .\models.py:96
msgid "Budget"
msgstr "Budget"
#: .\models.py:114 .\templates\main\form\snapshot.html:23
#: .\templates\main\index.html:30
msgid "Categories"
msgstr "Catégories"
#: .\models.py:119
msgid "End date"
msgstr "Date de fin"
#: .\models.py:121
msgid "Start date"
msgstr "Date de début"
#: .\models.py:124
msgid "End value"
msgstr "Valeur de fin"
#: .\models.py:127
msgid "Start value"
msgstr "Valeur de début"
#: .\models.py:133 .\templates\main\table\snapshot.html:27
msgid "Difference"
msgstr "Différence"
#: .\models.py:140
msgid "Transaction difference"
msgstr "Différence des transactions"
#: .\models.py:146 .\models.py:275 .\templates\main\form\fileinput.html:8
#: .\templates\main\table\invoice.html:10
#: .\templates\main\table\invoice.html:21
msgid "File"
msgstr "Fichier"
#: .\models.py:153
#, python-format
msgid "%(date)s statement"
msgstr "Relevé du %(date)s"
#: .\models.py:193 .\models.py:224
msgid "Statement"
msgstr "Relevé"
#: .\models.py:194 .\templates\main\form\account.html:13
#: .\templates\main\list\snapshot.html:6 .\templates\main\list\snapshot.html:20
msgid "Statements"
msgstr "Relevés"
#: .\models.py:200 .\models.py:263
msgid "Transaction"
msgstr "Transaction"
#: .\models.py:202
msgid "Description"
msgstr "Description"
#: .\models.py:204 .\templates\main\table\snapshot.html:26
#: .\templates\main\table\transaction.html:29
msgid "Value"
msgstr "Valeur"
#: .\models.py:206 .\templates\main\table\snapshot.html:22
#: .\templates\main\table\transaction.html:27
msgid "Date"
msgstr "Date"
#: .\models.py:207
msgid "Real date"
msgstr "Date réelle"
#: .\models.py:209 .\templates\main\table\transaction.html:30
msgid "Trader"
msgstr "Commerçant"
#: .\models.py:212
msgid "Payment"
msgstr "Paiement"
#: .\models.py:264 .\templates\main\base.html:44
#: .\templates\main\form\account.html:17 .\templates\main\form\category.html:13
#: .\templates\main\form\snapshot.html:27 .\templates\main\index.html:26
#: .\templates\main\list\transaction.html:6
#: .\templates\main\list\transaction.html:23
#: .\templates\main\month\transaction.html:15
#: .\templates\main\table\snapshot.html:28
msgid "Transactions"
msgstr "Transactions"
#: .\models.py:270 .\models.py:307
msgid "Invoice"
msgstr "Facture"
#: .\models.py:308 .\templates\main\form\transaction.html:18
msgid "Invoices"
msgstr "Factures"
#: .\templates\main\base.html:27
msgid "Skip to main content"
msgstr "Aller au contenu principal"
#: .\templates\main\base.html:33
msgid "Home"
msgstr "Accueil"
#: .\templates\main\base.html:38 .\templates\main\index.html:40
msgid "Snapshots"
msgstr "Relevés"
#: .\templates\main\base.html:50 .\templates\main\form\account.html:5
msgid "Create account"
msgstr "Créer un compte"
#: .\templates\main\base.html:55 .\templates\main\form\snapshot.html:5
msgid "Create snapshot"
msgstr "Créer un relevé"
#: .\templates\main\base.html:60 .\templates\main\form\category.html:5
msgid "Create category"
msgstr "Créer une catégorie"
#: .\templates\main\base.html:65 .\templates\main\form\transaction.html:4
#: .\templates\main\table\transaction.html:5
msgid "Create transaction"
msgstr "Créer une transaction"
#: .\templates\main\base.html:73
msgid "Log out"
msgstr "Se déconnecter"
#: .\templates\main\base.html:78 .\templates\main\form\login.html:6
msgid "Log in"
msgstr "Se connecter"
#: .\templates\main\base.html:83
msgid "Logged in as <strong>%(user)s</strong>"
msgstr "Connecté en tant que <strong>%(user)s</strong>"
#: .\templates\main\confirm_delete.html:19
#, python-format
msgid "Are you sure you want do delete <strong>%(object)s</strong> ?"
msgstr "Êtes-vous sûr de vouloir supprimer <strong>%(object)s</strong> ?"
#: .\templates\main\confirm_delete.html:23
msgid "Cancel"
msgstr "Annuler"
#: .\templates\main\confirm_delete.html:24
msgid "Confirm"
msgstr "Confirmer"
#: .\templates\main\form\account.html:8
msgid "New account"
msgstr "Nouveau compte"
#: .\templates\main\form\account.html:19 .\templates\main\form\category.html:15
#: .\templates\main\index.html:44
msgid "History"
msgstr "Historique"
#: .\templates\main\form\category.html:8
msgid "New category"
msgstr "Nouvelle catégorie"
#: .\templates\main\form\form_base.html:29
#: .\templates\main\table\invoice.html:11
#: .\templates\main\table\invoice.html:24
msgid "Delete"
msgstr "Supprimer"
#: .\templates\main\form\form_base.html:31
msgid "Reset"
msgstr "Réinitialiser"
#: .\templates\main\form\form_base.html:33
msgid "Create"
msgstr "Créer"
#: .\templates\main\form\form_base.html:35
msgid "Save"
msgstr "Enregistrer"
#: .\templates\main\form\invoice.html:4 .\templates\main\table\invoice.html:37
msgid "Create invoice"
msgstr "Créer une facture"
#: .\templates\main\form\invoice.html:7
msgid "New invoice"
msgstr "Nouvelle facture"
#: .\templates\main\form\snapshot.html:8
msgid "New snapshot"
msgstr "Nouveau relevé"
#: .\templates\main\form\transaction.html:7
msgid "New transaction"
msgstr "Nouvelle transaction"
#: .\templates\main\list\snapshot.html:27
msgid "No snapshots to show"
msgstr "Aucun relevé à afficher"
#: .\templates\main\list\transaction.html:33
msgid "No transactions to show"
msgstr "Aucune transaction à afficher"
#: .\templates\main\login.html:14
msgid "Log In"
msgstr "Se connecter"
#: .\templates\main\plot\category.html:15 .\templates\main\plot\history.html:16
msgid "Expenses"
msgstr "Dépenses"
#: .\templates\main\plot\category.html:16 .\templates\main\plot\history.html:17
msgid "Income"
msgstr "Revenus"
#: .\templates\main\plot\history.html:15
msgid "Month"
msgstr "Mois"
#: .\templates\main\table\invoice.html:30
msgid "No invoice"
msgstr "Aucune facture"
#: .\templates\main\table\snapshot.html:5
msgid "Create statement"
msgstr "Créer un relevé"
#: .\templates\main\table\snapshot.html:60
msgid "View all statements"
msgstr "Voir tous les relevés"
#: .\templates\main\table\transaction.html:73
msgid "View all transactions"
msgstr "Voir toutes les transactions"
#~ msgid "New statement"
#~ msgstr "Nouveau relevé"
#~ msgid "Create Account"
#~ msgstr "Créer Compte"
#~ msgid "Create Snapshot"
#~ msgstr "Créer Relevé"
#~ msgid "Create Category"
#~ msgstr "Créer Catégorie"
#~ msgid "Create Transaction"
#~ msgstr "Créer Transaction"
#, python-format
#~ msgid "Create %(name)s"
#~ msgstr "Créer %(name)s"

View file

@ -1,134 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-23 08:03+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\main\models.py:10
msgid "User"
msgstr "Utilisateur"
#: .\main\templates\main\base.html:28
msgid "Skip to main content"
msgstr "Aller au contenu principal"
#: .\main\templates\main\base.html:35
msgid "Home"
msgstr "Accueil"
#: .\main\templates\main\base.html:42 .\main\templates\main\index.html:17
msgid "Statements"
msgstr "Relevés"
#: .\main\templates\main\base.html:49
msgid "Transactions"
msgstr "Transactions"
#: .\main\templates\main\base.html:57 .\main\templates\main\list.html:10
#: .\main\templates\main\list.html:37
msgid "Search"
msgstr "Rechercher"
#: .\main\templates\main\base.html:62
#, python-format
msgid "Logged in as <strong>%(user)s</strong>"
msgstr "Connecté en tant que <strong>%(user)s</strong>"
#: .\main\templates\main\base.html:66
msgid "Log out"
msgstr "Se déconnecter"
#: .\main\templates\main\base.html:74 .\main\templates\main\form\login.html:5
#: .\main\templates\main\login.html:11
msgid "Log in"
msgstr "Se connecter"
#: .\main\templates\main\confirm_delete.html:15
#, python-format
msgid "Are you sure you want do delete <strong>%(object)s</strong> ?"
msgstr "Êtes-vous sûr de vouloir supprimer <strong>%(object)s</strong> ?"
#: .\main\templates\main\confirm_delete.html:20
msgid "Cancel"
msgstr "Annuler"
#: .\main\templates\main\confirm_delete.html:21
msgid "Confirm"
msgstr "Confirmer"
#: .\main\templates\main\form\fileinput.html:6
msgid "File"
msgstr "Fichier"
#: .\main\templates\main\form\form_base.html:46
msgid "Create"
msgstr "Créer"
#: .\main\templates\main\form\form_base.html:48
msgid "Save"
msgstr "Enregistrer"
#: .\main\templates\main\form\form_base.html:50
msgid "Reset"
msgstr "Réinitialiser"
#: .\main\templates\main\form\form_base.html:52
msgid "Delete"
msgstr "Supprimer"
#: .\main\templates\main\index.html:13
msgid "Accounts"
msgstr "Comptes"
#: .\main\templates\main\index.html:23
msgid "Categories"
msgstr "Catégories"
#: .\main\templates\main\index.html:29
msgid "Create category"
msgstr "Créer une catégorie"
#: .\main\templates\main\index.html:34
msgid "History"
msgstr "Historique"
#: .\main\views.py:54
msgid "was created successfully"
msgstr "a été créé avec succès"
#~ msgid "Account"
#~ msgstr "Compte"
#~ msgid "Balance"
#~ msgstr "Solde"
#~ msgid "Edit"
#~ msgstr "Modifier"
#~ msgid "No account"
#~ msgstr "Aucun compte"
#~ msgid "Create account"
#~ msgstr "Créer un compte"
#~ msgid "Create statement"
#~ msgstr "Créer un relevé"
#~ msgid "Create transaction"
#~ msgstr "Créer une transaction"
#~ msgid "No category"
#~ msgstr "Aucune catégorie"

View file

@ -1,46 +1,15 @@
from django.conf import settings from django.conf import settings
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class NummiQuerySet(models.QuerySet): class UserModel(models.Model):
main_field = "name"
fields = dict()
def search(self, search):
return (
self.annotate(
rank=SearchRank(
sum(
(
SearchVector(field, weight=weight)
for field, weight in self.fields.items()
),
start=SearchVector(self.main_field, weight="A"),
),
SearchQuery(search, search_type="websearch"),
),
similarity=TrigramSimilarity(self.main_field, search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank")
)
class NummiModel(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_("User"), verbose_name=_("User"),
editable=False, editable=False,
) )
objects = NummiQuerySet.as_manager()
class Meta: class Meta:
abstract = True abstract = True

View file

@ -1,217 +1,62 @@
.drop-zone { form ul.errorlist {
position: absolute; color: var(--red);
top: 0; font-weight: 550;
left: 0; list-style-type: "! ";
right: 0; margin: 0;
bottom: 0;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
align-items: center;
text-align: center;
color: transparent;
display: grid;
transition-property: backdrop-filter;
transition-duration: 750ms;
z-index: -1;
> span {
font-weight: 650;
font-size: 2rem;
transition-property: color;
transition-duration: inherit;
}
main.highlight > & {
z-index: 100;
backdrop-filter: blur(0.1rem);
> span {
color: var(--green);
}
}
} }
form { form > table > tbody > tr > th {
display: grid; background: var(--bg-01);
gap: 0.5rem; background-clip: padding-box;
grid-template-columns: repeat(auto-fill, 32rem); }
@media (width < 1024px) { form tbody input,
grid-template-columns: 1fr; form tbody select,
} form tbody textarea {
font: inherit;
&.hidden { border: none;
display: none; background: var(--bg);
} width: 100%;
height: 100%;
.column { line-height: 1.5;
display: grid; }
gap: 0.5rem; form input[type="checkbox"] {
border: 1px solid var(--gray); width: initial;
padding: var(--gap); }
block-size: min-content; table.file-input tr {
} border: none;
.fieldset { }
display: grid; table.file-input th {
grid-auto-columns: 1fr; text-align: left;
grid-auto-flow: column; }
gap: inherit; table.file-input tr :first-child {
padding: 0; padding-left: 0;
margin: 0; }
border: none; table.file-input tr :last-child {
} padding-right: 0;
}
ul.errorlist {
color: var(--red); form tfoot {
font-weight: 550; text-align: right;
list-style: none; }
padding: 0; .buttons input {
margin: 0; font: inherit;
} line-height: 1.5;
margin-left: var(--gap);
.field { border-radius: var(--radius);
display: grid; padding: 0 var(--gap);
grid-auto-rows: min-content; cursor: pointer;
align-items: center; }
column-gap: 0.5rem; .buttons input:hover {
text-decoration: underline;
ul.errorlist { }
font-size: 0.8rem; .buttons input[type="submit"] {
} border: 0.1rem solid var(--green);
background: var(--green-1);
&:has(> textarea) { }
grid-template-rows: min-content 1fr; .buttons input[type="reset"] {
textarea { border: 0.1rem solid var(--red);
resize: block; background: var(--red-1);
} }
} .buttons a.del {
&:has(> input[type="checkbox"]) { color: var(--red);
grid-template-columns: min-content 1fr;
> label {
font-size: inherit;
grid-row: 1;
grid-column: 2;
padding: 0.5rem;
line-height: initial;
}
> input {
grid-row: 1;
grid-column: 1;
margin: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
> label {
font-size: 0.8rem;
line-height: 0.8rem;
z-index: 10;
}
> a {
padding: 0.5rem;
}
input,
select,
textarea {
font: inherit;
line-height: initial;
border: none;
border: 1px solid transparent;
border-bottom: 1px solid var(--gray);
background: none;
z-index: 1;
padding: 0.5rem;
&:has(~ ul.errorlist) {
border-color: var(--red);
}
&.autocompleted:not(.mod) {
border-bottom-color: var(--green);
}
&:not([type="checkbox"]) {
width: 100%;
margin: 0;
}
&[name*="value"] {
text-align: right;
font-feature-settings: var(--num);
}
&[name*="date"] {
font-feature-settings: var(--num);
}
&:focus {
outline: none;
background: var(--bg-1);
}
}
> .file-input {
display: grid;
> .current {
display: grid;
grid-template-columns: 1fr;
grid-auto-columns: max-content;
grid-auto-flow: column;
a {
padding: 0.5rem;
}
}
input[type="file"] {
&::file-selector-button {
display: none;
}
}
}
> .ico-input {
display: grid;
grid-template-columns: min-content 1fr;
column-gap: 0.5rem;
align-items: center;
span[class|="ri"] {
padding: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
}
.buttons {
grid-column: 1 / -1;
line-height: 2rem;
input,
a {
font: inherit;
cursor: pointer;
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
color: inherit;
&:hover {
text-decoration: underline;
}
}
input[type="submit"] {
background: var(--green-1);
border-color: var(--green);
}
input[type="reset"] {
background: var(--bg-1);
}
a.del {
color: var(--red);
border-color: var(--red);
border-style: dashed;
}
}
} }

View file

@ -1,5 +1,4 @@
@import "https://rsms.me/inter/inter.css"; @import "https://rsms.me/inter/inter.css";
@import "https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css";
*, *,
*::before, *::before,
@ -24,7 +23,7 @@
--bg-inv: var(--theme-1); --bg-inv: var(--theme-1);
--text-inv: #ffffffde; --text-inv: #ffffffde;
--bg-1: #f0f0f0; --bg-01: #f0f0f0;
--text-green: #296629; --text-green: #296629;
--text-link: var(--text-green); --text-link: var(--text-green);
@ -40,8 +39,7 @@
--border: 0.5em; --border: 0.5em;
--radius: 0.25em; --radius: 0.25em;
--default-ffs: "dlig", "ss01", "ss04"; --num: "tnum", "ss01", "ss02", "case";
--num: var(--default-ffs), "tnum", "case";
} }
body { body {
@ -53,7 +51,6 @@ body {
display: grid; display: grid;
grid-template-columns: max-content 1fr; grid-template-columns: max-content 1fr;
font-feature-settings: var(--default-ffs);
} }
p { p {
@ -63,10 +60,9 @@ a {
color: var(--text-link); color: var(--text-link);
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
}
&:is(:hover, :focus) { a:hover {
text-decoration: underline; text-decoration: underline;
}
} }
.red { .red {
@ -81,33 +77,12 @@ a {
main, main,
nav, nav,
footer { footer {
padding: 2rem 1rem; padding: 2rem;
@media (width > 720px) {
padding: 2rem;
}
background: var(--bg);
} }
main { main {
position: relative;
grid-column: 2; grid-column: 2;
grid-row: 1; grid-row: 1;
overflow-x: hidden; overflow-x: hidden;
h2.new {
opacity: 0.8;
}
.split {
display: grid;
gap: var(--gap);
grid-template-columns: 100%;
@media (width > 720px) {
grid-template-columns: minmax(20rem, max-content) 1fr;
}
& > section > :first-child {
margin-top: 0;
}
}
} }
nav { nav {
grid-column: 1; grid-column: 1;
@ -116,50 +91,41 @@ nav {
height: 100vh; height: 100vh;
position: sticky; position: sticky;
top: 0; top: 0;
overflow-y: auto;
background: var(--bg-1); background: var(--bg-01);
line-height: 2rem; line-height: 2rem;
h1 img {
height: 1cap;
}
ul {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
a {
&.skip-link {
font-weight: 300;
&:is(:active, :focus) {
font-weight: 500;
}
}
display: grid;
grid-template-columns: 1fr max-content;
align-items: baseline;
[class^="ri-"] {
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
border-radius: var(--radius);
}
&.cur {
font-weight: 550;
[class^="ri-"] {
background: var(--text-link);
color: var(--bg);
}
}
}
} }
:is(nav, main) > :first-child, nav h1 img {
main > section:first-child > :first-child { height: 1cap;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
nav .skip-link {
opacity: 0.8;
font-weight: 300;
}
nav .skip-link:active,
nav .skip-link:focus {
opacity: initial;
font-weight: 500;
}
nav a {
display: block;
}
nav a.cur {
font-weight: 550;
}
nav a.cur::after {
content: "◎";
position: absolute;
right: 0;
}
nav > :first-child,
main > :first-child {
margin-top: 0; margin-top: 0;
} }
footer { footer {
@ -168,46 +134,16 @@ footer {
font-weight: 250; font-weight: 250;
} }
.pagination { #pagination {
text-align: center; text-align: center;
font-feature-settings: var(--num); font-feature-settings: var(--num);
}
a { #pagination a {
min-width: 1rem; width: 2rem;
padding: 0 0.5rem; }
#pagination a.cur {
&.cur { font-weight: 650;
font-weight: 650; text-decoration: underline dotted;
text-decoration: underline dotted;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
}
@media (width > 720px) {
&.n3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
width: max-content;
margin: 0.5rem auto;
.prev {
grid-column: 1;
}
.cur {
grid-column: 2;
}
.next {
grid-column: 3;
}
}
}
& + section :first-child {
margin-top: 0;
}
} }
@media (width < 1024px) { @media (width < 1024px) {
@ -223,38 +159,13 @@ footer {
height: initial; height: initial;
} }
} }
a.big-link {
margin-right: 1em;
}
[class^="ri-"] { [class^="ri-"] {
display: inline-block; margin-right: 0.5em;
text-align: center;
font-weight: normal; font-weight: normal;
&.green,
&.red,
&.white {
&.green {
background: var(--green);
color: var(--bg);
}
&.red {
background: var(--red);
color: var(--bg);
}
&.white {
background: var(--bg-1);
}
border-radius: var(--radius);
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
}
a:not(.i) &,
.wi &,
h2 & {
&:first-child::after {
content: "\2002";
}
}
} }
h1, h1,
@ -274,299 +185,9 @@ h2 {
h3 { h3 {
font-size: 1.5rem; font-size: 1.5rem;
} }
main h2.new {
opacity: 0.8;
}
p { p {
margin: 0.5em 0; margin: 0.5em 0;
} }
ul.messages {
font-weight: 550;
list-style-type: none;
margin: 0;
margin-bottom: var(--gap);
background: var(--bg-1);
padding: 0;
li {
--message-color: var(--text);
padding: calc(var(--gap) / 2) var(--gap);
border-left: var(--message-color) solid var(--border);
[class^="ri-"] {
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
border-radius: var(--radius);
background: var(--message-color);
color: var(--bg);
margin-right: 0.5rem;
}
&.msg-level-20 {
--message-color: var(--green);
}
&.msg-level-25 {
--message-color: var(--green-1);
}
&.msg-level-30 {
--message-color: var(--red-1);
}
&.msg-level-40 {
--message-color: var(--red);
}
}
}
.backlinks {
display: grid;
grid-template-columns: repeat(2, 1fr);
p {
grid-column: 1;
&.back {
grid-column: 2;
text-align: right;
a {
margin-right: 0;
margin-left: 1em;
[class^="ri-"] {
margin-right: 0em;
margin-left: 0.5em;
}
}
}
}
}
dl.accounts {
margin: 0;
dt,
dd {
margin: 0;
}
.account {
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid var(--gray);
display: grid;
grid-template-columns: 1fr min-content;
&.new,
&.more {
border-style: dashed;
}
&.more label {
display: block;
}
}
&:not(.show-archive) .account.archived {
display: none;
}
}
ul.statements,
ul.invoices {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-auto-rows: 1fr;
gap: 0.5rem;
list-style: none;
padding: 0;
margin: 0;
li {
display: grid;
gap: 0.5rem;
padding: var(--gap);
border: var(--gray) 1px solid;
text-align: right;
align-items: center;
> * {
&.title {
font-weight: 650;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&.new,
&.more {
border-style: dashed;
}
}
}
ul.statements {
li {
> * {
display: grid;
align-items: center;
&.date :first-child {
font-size: 2rem;
}
&.value :first-child {
font-weight: 650;
}
&.icon {
span {
margin: auto;
}
}
&.account a {
overflow: hidden;
text-overflow: ".";
white-space: nowrap;
}
}
}
}
ul.invoices {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.statement-details {
display: grid;
@media (width > 720px) {
grid-template-columns: repeat(3, min-content);
}
gap: var(--gap);
align-items: center;
> span.evolution {
display: grid;
grid-auto-rows: min-content;
> span[class^="ri-"] {
font-size: 2rem;
}
> span.value {
text-align: right;
}
}
> span.start,
> span.end,
> span.file,
> span.incons {
display: grid;
border: var(--gray) 1px solid;
padding: var(--gap);
&.file,
&.incons {
grid-column: 1 / -1;
}
&.incons {
border-color: var(--red);
grid-template-columns: min-content 1fr;
grid-auto-flow: column;
align-items: center;
gap: 0.5rem;
> .ds {
display: grid;
grid-auto-rows: min-content;
}
.value {
text-align: right;
}
.diff {
font-weight: 650;
}
.sum {
text-decoration: line-through;
}
.links {
grid-column: 1 / -1;
grid-row: 2;
display: grid;
gap: inherit;
a {
color: var(--red);
}
}
}
> .date {
text-align: right;
}
> .value {
font-size: 2rem;
}
}
}
.multilink {
display: grid;
grid-auto-columns: max-content;
}
.transaction-details {
display: grid;
grid-auto-columns: minmax(1fr, max-content);
grid-auto-rows: min-content;
max-width: 32rem;
ul {
list-style: none;
display: grid;
li.value {
font-size: 1.5rem;
text-align: right;
}
}
p.description,
ul {
border: var(--gray) 1px solid;
padding: var(--gap);
}
}
.category,
.big-link {
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
&.add {
border-style: dashed;
}
}
.date,
.value {
font-feature-settings: var(--num);
}
details {
border: var(--gray) 1px solid;
margin-bottom: var(--gap);
summary {
font-weight: 650;
cursor: pointer;
padding: var(--gap);
&::marker {
content: "\ed27\2002";
font-family: remixicon;
font-weight: initial;
}
}
&[open] summary {
background: var(--bg-1);
}
form {
padding: var(--gap);
}
}

View file

@ -0,0 +1,3 @@
.pagination .current {
font-feature-settings: "tnum", "ss01";
}

View file

@ -1,123 +1,72 @@
table.full-width col.bar { table.full-width col.bar {
width: auto; width: auto;
} }
.plot { .plot {
overflow-x: auto; overflow-x: auto;
table {
min-width: 40rem;
}
td.bar {
position: relative;
padding: 0;
overflow: hidden;
div {
position: absolute;
height: 0.5rem;
top: 0;
&:not(.tot) {
width: 0;
box-sizing: border-box;
z-index: 1;
display: inline-block;
}
&.tot {
z-index: 10;
height: 0.5rem;
span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: 0.5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num);
}
}
}
&.p div {
left: 0;
border-radius: 0 var(--radius) var(--radius) 0;
background: var(--green-1);
&.tot {
background: var(--green);
span {
left: 0;
}
}
}
&.m div {
right: 0;
border-radius: var(--radius) 0 0 var(--radius);
background: var(--red-1);
&.tot {
background: var(--red);
span {
right: 0;
}
}
}
}
&.history tbody tr {
background: initial;
}
tbody tr {
&.empty {
height: 0.5rem;
}
&.even {
background: #eeeeff;
}
}
} }
.calendar { .plot td.bar {
overflow-x: auto; position: relative;
padding: 0;
}
.plot td.bar div {
position: absolute;
height: 0.5rem;
top: 0;
}
margin-bottom: var(--gap); .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 {
background: var(--red-1);
}
.plot td.bar.p div {
background: var(--green-1);
}
.plot td.bar div.tot {
z-index: 10;
height: 0.5rem;
}
.plot td.bar.m div.tot {
background: var(--red);
}
.plot td.bar.p div.tot {
background: var(--green);
}
.plot td.bar div.tot span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: 0.5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num); font-feature-settings: var(--num);
}
.plot td.bar.p div.tot span {
left: 0;
}
.plot td.bar.m div.tot span {
right: 0;
}
table { @media (width < 720px) {
tbody tr { .plot .bar {
background: initial; width: 0;
&:not(:last-child) { overflow: hidden;
border-bottom: none;
}
&:not(:first-child) {
border-top: none;
}
td.month {
text-align: center;
background-color: color-mix(
in hsl,
var(--td-bg, var(--bg)) var(--opacity),
var(--bg)
);
padding: 0;
width: 4rem;
height: 4rem;
&.p {
--td-bg: var(--green);
}
&.m {
--td-bg: var(--red);
}
}
td.total {
text-align: right;
font-weight: 650;
font-feature-settings: var(--num);
}
}
} }
} }

View file

@ -1,59 +1,49 @@
.table { .table,
form {
overflow-x: auto; overflow-x: auto;
width: 100%;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
}
&.full-width { table.more tbody:last-child tr:last-child {
width: 100%; border-bottom: 0.1rem dashed var(--gray);
}
col { table.full-width {
width: 8rem; width: 100%;
} }
} thead {
col.icon { background: var(--bg-01);
width: 1ch; }
} table.full-width col {
thead tr:not(.new) { width: 8rem;
background: var(--bg-1); }
} table col.icon {
tr { width: 1ch;
border: 1px solid var(--gray); }
height: 2rem; tr {
line-height: 2rem; border: 0.1rem solid var(--gray);
height: 2rem;
tbody &:where(:nth-of-type(even)) { line-height: 2rem;
background: #eeeeff; }
} td,
&.more, th {
&.new { padding: 0 var(--gap);
text-align: center; position: relative;
border-style: dashed; white-space: nowrap;
} text-overflow: ellipsis;
} }
td, .date,
th { .value {
padding: 0 var(--gap); font-feature-settings: var(--num);
position: relative; }
white-space: nowrap; .l {
text-overflow: ellipsis; text-align: left;
&.empty { }
text-align: center; .r,
opacity: 0.8; .value {
font-weight: 300; text-align: right;
} }
} .c,
.date {
.l { text-align: center;
text-align: left;
}
.r,
.value {
text-align: right;
}
.c,
.date {
text-align: center;
}
} }

File diff suppressed because one or more lines are too long

View file

@ -1,181 +0,0 @@
const beforeUnloadHandler = (event) => {
event.preventDefault();
};
const forms = document.querySelectorAll("form");
for (let form of forms) {
let inputs = form.querySelectorAll("input");
for (input of inputs) {
input.addEventListener("input", (event) => {
window.addEventListener("beforeunload", beforeUnloadHandler);
});
}
form.addEventListener("submit", (event) => {
window.removeEventListener("beforeunload", beforeUnloadHandler);
});
form.addEventListener("reset", (event) => {
window.removeEventListener("beforeunload", beforeUnloadHandler);
});
let categorySelect = form.querySelector(".category-select");
if (categorySelect) {
let input = categorySelect.querySelector("select");
let icon = categorySelect.querySelector("span");
let icons = JSON.parse(input.dataset.icons);
function setIcon(event) {
icon.className = `ri-${icons[input.value] || "folder"}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let accountSelect = form.querySelector(".account-select");
if (accountSelect) {
let input = accountSelect.querySelector("select");
let icon = accountSelect.querySelector("span");
let icons = JSON.parse(input.dataset.icons);
function setIcon(event) {
icon.className = `ri-${icons[input.value] || "bank"}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let iconSelect = form.querySelector(".icon-select");
if (iconSelect) {
let input = iconSelect.querySelector("input");
let icon = iconSelect.querySelector("span");
let icons = Array.from(iconSelect.querySelector("datalist").options).map(
(opt) => opt.value,
);
function setIcon(event) {
icon.className = `ri-${
icons.includes(input.value) ? input.value : "square"
}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let ac_json = form.querySelector("#autocomplete");
if (ac_json) {
let autocomplete_data = JSON.parse(ac_json.textContent);
let fields = form.querySelectorAll("[name]");
function clearMod(event) {
event.target.classList.add("mod");
event.target.removeEventListener("input", clearMod);
}
function setMod() {
for (let f of fields) {
f.addEventListener("input", clearMod);
}
}
setMod();
form.addEventListener("reset", (event) => {
for (let f of fields) {
f.classList.remove("mod", "autocompleted");
}
setMod();
});
let old_values = [];
let field = form.querySelector(`[name="${autocomplete_data.field}"]`);
field.addEventListener("change", (event) => {
if (field.value in autocomplete_data.data) {
old_values = [];
for (let comp of autocomplete_data.data[field.value]) {
let f = form.querySelector(`[name="${comp.field}"]`);
if (!f.classList.contains("mod")) {
old_values.push({ field: f, value: f.value });
f.value = comp.value;
f.dispatchEvent(new Event("input"));
f.animate([{ borderColor: "var(--green)" }, {}], 750);
f.classList.remove("mod");
f.classList.add("autocompleted");
f.addEventListener("input", clearMod);
function ccAutocomplete(event) {
document.removeEventListener("keyup", cancelAutocomplete);
for (comp of old_values) {
comp.field.removeEventListener("input", ccAutocomplete);
}
}
f.addEventListener("input", ccAutocomplete);
}
}
function cancelAutocomplete(event) {
if (event.code == "Escape") {
for (comp of old_values) {
let f = comp.field;
f.value = comp.value;
f.dispatchEvent(new Event("input"));
f.animate([{ borderColor: "var(--red)" }, {}], 750);
f.classList.remove("mod");
f.classList.remove("autocompleted");
f.addEventListener("input", clearMod);
}
document.removeEventListener("keyup", cancelAutocomplete);
}
}
document.addEventListener("keyup", cancelAutocomplete);
form.addEventListener("reset", (event) => {
document.removeEventListener("keyup", cancelAutocomplete);
});
}
});
}
}
let accounts = document.querySelector("dl.accounts");
if (accounts) {
let toggle = accounts.querySelector("input#show-archived-accounts");
if (toggle) {
accounts.classList.toggle("show-archive", toggle.checked);
toggle.addEventListener("change", (event) => {
accounts.classList.toggle("show-archive", toggle.checked);
});
}
}
const filterForm = document.querySelector("form.filter");
if (filterForm) {
const accountSelect = filterForm.querySelector("[name='account']");
const statementSelect = filterForm.querySelector("[name='statement']");
if (!statementSelect.disabled) {
accountSelect.addEventListener("input", (event) => {
statementSelect.value = "";
statementSelect.disabled = true;
});
filterForm.addEventListener("reset", (event) => {
statementSelect.disabled = false;
});
}
let disableStatement = false;
filterForm.addEventListener("submit", (event) => {
for (element of filterForm.elements) {
if (
element.value == "" ||
(disableStatement && element.name == "statement")
) {
if (element.name == "account") {
disableStatement = true;
}
element.disabled = true;
}
}
});
}

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,22 @@
<svg version="1.1"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg">
<defs>
<rect id="basemask" x="5" y="-5" width="90" height="110" />
<rect id="line" x="45" y="-10" width="10" height="120" />
</defs>
<mask id="mask">
<use href="#basemask" fill="white" />
<use href="#line" fill="black" />
<use href="#line" transform="translate(20)" fill="black" />
</mask>
<circle cx="50" cy="50" r="50"
fill="#ffffff"
mask="url(#mask)"
transform-origin="center center" transform="rotate(10) scale(.8)" />
</svg>

After

Width:  |  Height:  |  Size: 539 B

View file

@ -6,10 +6,6 @@
<defs> <defs>
<rect id="basemask" x="5" y="-5" width="90" height="110" /> <rect id="basemask" x="5" y="-5" width="90" height="110" />
<rect id="line" x="45" y="-10" width="10" height="120" /> <rect id="line" x="45" y="-10" width="10" height="120" />
<linearGradient id="grad" gradientTransform="rotate(90)">
<stop offset="5%" stop-color="#66cc66" />
<stop offset="95%" stop-color="#cc6699" />
</linearGradient>
</defs> </defs>
<mask id="mask"> <mask id="mask">
@ -19,8 +15,8 @@
</mask> </mask>
<circle cx="50" cy="50" r="50" <circle cx="50" cy="50" r="50"
fill="url('#grad')" fill="#000000"
mask="url('#mask')" mask="url(#mask)"
transform-origin="center center" transform="rotate(10)" /> transform-origin="center center" transform="rotate(10) scale(.8)" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 539 B

Before After
Before After

View file

@ -1,96 +1,94 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load main_extras %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title> <title>
{% block title %}Nummi{% endblock %} {% block title %}Nummi{% endblock %}
</title> </title>
{% block link %} {% block link %}
<link rel="icon" href="{% static "main/svg/logo.svg" %}" type="image/svg+xml"> <link rel="icon" href="{% static "main/svg/logo.svg" %}" type="image/svg+xml" />
{% css "main/css/main.css" %} <link rel="stylesheet" href="{% static "main/css/main.css" %}" type="text/css" />
{% js "main/js/base.js" %} <link rel="stylesheet" href="{% static "main/remixicon/remixicon.css" %}" type="text/css" />
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% block nav %} {% block nav %}
{% spaceless %} {% spaceless %}
<nav> <nav>
<h1> <h1>
<img src="{% static "main/svg/logo.svg" %}" alt=""> <img src="{% static "main/svg/logo.svg" %}" alt="" />
Nummi Nummi
</h1> </h1>
<ul> <ul>
<li> <li>
<a class="skip-link" href="#main">{% translate "Skip to main content" %}</a> <a class="skip-link" href="#main">{% translate "Skip to main content" %}</a>
</li> </li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li> <li>
<a href="{% url "index" %}" <a href="{% url "index" %}"
class="home{% if request.resolver_match.url_name == "index" %} cur{% endif %}" class="home{% if request.resolver_match.url_name == "index" %} cur{% endif %}"
accesskey="h"> accesskey="h">{% translate "Home" %}</a>
<span>{% translate "Home" %}</span> </li>
{{ "home"|remix }} <li>
</a> <a href="{% url "statements" %}"
</li> class="{% if request.resolver_match.url_name == "statements" %}cur{% endif %}">
<li> {% translate "Statements" %}
<a href="{% url "statements" %}" </a>
class="{% if request.resolver_match.url_name == "statements" %}cur{% endif %}"> </li>
<span>{% translate "Statements" %}</span> <li>
{{ "file"|remix }} <a href="{% url "transactions" %}"
</a> class="{% if request.resolver_match.url_name == "transactions" %}cur{% endif %}">
</li> {% translate "Transactions" %}
<li> </a>
<a href="{% url "transactions" %}" </li>
class="{% if request.resolver_match.url_name == "transactions" %}cur{% endif %}"> <li>
<span>{% translate "Transactions" %}</span> <a href="{% url "new_account" %}"
{{ "receipt"|remix }} class="{% if request.resolver_match.url_name == "new_account" %}cur{% endif %}"
</a> accesskey="a">{% translate "Create account" %}</a>
</li> </li>
<li> <li>
<a href="{% url "search" %}" <a href="{% url "new_statement" %}"
class="{% if request.resolver_match.url_name == "search" %}cur{% endif %}" class="{% if request.resolver_match.url_name == "new_statement" %}cur{% endif %}"
accesskey="r"> accesskey="s">{% translate "Create statement" %}</a>
<span>{% translate "Search" %}</span> </li>
{{ "search"|remix }} <li>
</a> <a href="{% url "new_category" %}"
</li> class="{% if request.resolver_match.url_name == "new_category" %}cur{% endif %}"
<li> accesskey="c">{% translate "Create category" %}</a>
{% blocktranslate %}Logged in as <strong>{{ user }}</strong>{% endblocktranslate %} </li>
</li> <li>
<li> <a href="{% url "new_transaction" %}"
<a href="{% url "logout" %}" accesskey="l"> class="{% if request.resolver_match.url_name == "new_transaction" %}cur{% endif %}"
<span>{% translate "Log out" %}</span> accesskey="t">{% translate "Create transaction" %}</a>
{{ "close-circle"|remix }} </li>
</a> <li>
</li> <a href="{% url "search" %}"
{% else %} class="{% if request.resolver_match.url_name == "search" %}cur{% endif %}"
<li> accesskey="r">{% translate "Search" %}</a>
<a {% if request.resolver_match.url_name == "login" %}class="cur"{% endif %} </li>
href="{% url "login" %}"> <li>
<span>{% translate "Log in" %}</span> <a href="{% url "logout" %}" accesskey="l">{% translate "Log out" %}</a>
{{ "user"|remix }} </li>
</a> {% else %}
</li> <li>
{% endif %} <a {% if request.resolver_match.url_name == "login" %}class="cur"{% endif %}
</ul> href="{% url "login" %}">{% translate "Log in" %}</a>
</nav> </li>
{% endspaceless %} {% endif %}
{% endblock %} </ul>
<main id="main"> {% if user %}
{% if messages %} <p>
<ul class="messages"> {% blocktranslate %}Logged in as <strong>{{ user }}</strong>{% endblocktranslate %}
{% for message in messages %} </p>
<li class="msg-level-{{ message.level }}"> {% endif %}
{{ message.level|messageicon }}{{ message }} </nav>
</li> {% endspaceless %}
{% endfor %} {% endblock %}
</ul> <main id="main">
{% endif %} {% block body %}{% endblock %}
{% block body %}{% endblock %} </main>
</main> </body>
</body>
</html> </html>

View file

@ -3,23 +3,26 @@
{% load main_extras %} {% load main_extras %}
{% load i18n %} {% load i18n %}
{% block link %} {% block link %}
{{ block.super }} {{ block.super }}
{% css "main/css/form.css" %} <link rel="stylesheet"
{% css "main/css/table.css" %} href="{% static 'main/css/form.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% spaceless %} {% spaceless %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<p> <p>
{% blocktranslate %}Are you sure you want do delete <strong>{{ object }}</strong> ?{% endblocktranslate %} {% blocktranslate %}Are you sure you want do delete <strong>{{ object }}</strong> ?{% endblocktranslate %}
</p> </p>
{% block additionalinfo %}{% endblock %} {{ form }}
{{ form }} <div class="buttons">
<div class="buttons"> <a href="{{ object.get_absolute_url }}">{% translate "Cancel" %}</a>
<a href="{{ object.get_absolute_url }}">{% translate "Cancel" %}</a> <input class="del" type="submit" value="{% translate "Confirm" %}" />
<input class="del" type="submit" value="{% translate "Confirm" %}" /> </div>
</div> </form>
</form> {% endspaceless %}
{% endspaceless %}
{% endblock %} {% endblock %}

View file

@ -3,38 +3,42 @@
{% load main_extras %} {% load main_extras %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% if form.instance|adding %} {% if form.instance|adding %}
{% block title_new %}{% endblock %} {% block title_new %}{% endblock %}
{% else %} {% else %}
{{ form.instance }} {{ form.instance }}
{% endif %} {% endif %}
Nummi Nummi
{% endblock %} {% endblock %}
{% block link %} {% block link %}
{{ block.super }} {{ block.super }}
{% css "main/css/form.css" %} <link rel="stylesheet"
{% css "main/css/table.css" %} href="{% static 'main/css/form.css' %}"
{% css "main/css/plot.css" %} type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/plot.css' %}"
type="text/css" />
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% with instance=form.instance %} {% with instance=form.instance %}
{% if instance|adding %} {% if instance|adding %}
<h2 class="new"> <h2 class="new">
{% block h2_new %}{% endblock %} {% block h2_new %}{% endblock %}
</h2> </h2>
{% else %} {% else %}
<h2> <h2>
{% block h2 %}{{ instance }}{% endblock %} {% block h2 %}{{ instance }}{% endblock %}
</h2> </h2>
{% endif %} {% endif %}
{% block pre %}{% endblock %} {% block pre %}{% endblock %}
<form method="post" enctype="multipart/form-data"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% if instance|adding %} {% if instance|adding %}<input hidden name="next" value="{{ request.path }}" />{% endif %}
<input hidden name="next" value="{{ request.path }}"> {{ form }}
{% endif %} </form>
{{ form }} {% block tables %}{% endblock %}
</form> {% endwith %}
{% block tables %}{% endblock %}
{% endwith %}
{% endblock %} {% endblock %}

View file

@ -1,21 +1,36 @@
{% load i18n %} {% load i18n %}
{% load main_extras %} {% load main_extras %}
<div class="file-input"> <table class="file-input">
{% if widget.is_initial %} {% if widget.is_initial %}
<div class="current"> <tr>
<a href="{{ widget.value.url }}" target="_blank">{{ "file"|remix }}{% translate "File" %} [{{ widget.value|extension }}]</a> <th>{{ widget.initial_text }}</th>
{% if not widget.required %} <td>
<span class="field checkbox"> <a href="{{ widget.value.url }}">{% translate "File" %} [{{ widget.value|extension }}]</a>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label> </td>
<input type="checkbox" </tr>
name="{{ widget.checkbox_name }}" {% if not widget.required %}
id="{{ widget.checkbox_id }}" <tr>
{% if widget.attrs.disabled %}disabled{% endif %}> <th>
</span> <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>
{% endif %} </th>
</div> <td>
{% endif %} <input type="checkbox"
<input type="{{ widget.type }}" name="{{ widget.checkbox_name }}"
name="{{ widget.name }}" id="{{ widget.checkbox_id }}"
{% include "django/forms/widgets/attrs.html" %}> {% if widget.attrs.disabled %}disabled{% endif %}>
</div> </td>
</tr>
{% endif %}
<tr>
<th>{{ widget.input_text }}</th>
<td>
{% else %}
<tr>
<td colspan="2">
{% endif %}
<input type="{{ widget.type }}"
name="{{ widget.name }}"
{% include "django/forms/widgets/attrs.html" %}>
</td>
</tr>
</table>

View file

@ -1,56 +1,43 @@
{% load i18n %} {% load i18n %}
{% load main_extras %} {% load main_extras %}
{% block fields %} {% block fields %}
{% if form.autocomplete %}{{ form.autocomplete|json_script:"autocomplete" }}{% endif %} {% if form.non_field_errors %}
{% if form.non_field_errors %} <ul class='errorlist'>
<ul class="errorlist"> {% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %} </ul>
</ul> {% endif %}
{% endif %} <table>
{% if form.fieldsets %} <tbody>
{% for group in form.fieldsets %} {% for field in form %}
<div class="column">
{% for fieldset in group %}
<div class="fieldset">
{% for field in fieldset %}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %} {% if field.errors %}
<ul class="errorlist"> <tr>
{% for error in field.errors %} <td colspan="2">{{ field.errors }}</td>
<li class="wi">{{ "error-warning"|remix }}{{ error }}</li> </tr>
{% endfor %}
</ul>
{% endif %} {% endif %}
</div> <tr>
<th class="l">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
</th>
<td>{{ field }}</td>
</tr>
{% endfor %} {% endfor %}
</div> </tbody>
{% endfor %} <tfoot>
</div> <tr class="buttons">
{% endfor %} <td colspan="2">
{% else %} {% block buttons %}
<div class="column"> {% if not form.instance|adding %}
{% for field in form %} <a class="del" href="{{ form.instance.get_delete_url }}">{% translate "Delete" %}</a>
{% if field.errors %}<p class="error">{{ field.errors }}</p>{% endif %} {% endif %}
<div class="field"> <input type="reset" value="{% translate "Reset" %}" />
<label for="{{ field.id_for_label }}">{{ field.label }}</label> {% if form.instance|adding %}
{{ field }} <input type="submit" value="{% translate "Create" %}" />
</div> {% else %}
{% endfor %} <input type="submit" value="{% translate "Save" %}" />
</div> {% endif %}
{% endif %} {% endblock %}
<div class="buttons"> </td>
{% block buttons %} </tr>
{% if form.instance|adding %} </tfoot>
<input type="submit" value="{% translate "Create" %}"> </table>
{% else %} {% endblock %}
<input type="submit" value="{% translate "Save" %}">
{% endif %}
<input type="reset" value="{% translate "Reset" %}">
{% if not form.instance|adding %}
<a class="del" href="{{ form.instance.get_delete_url }}">{% translate "Delete" %}</a>
{% endif %}
{% endblock buttons %}
</div>
{% endblock fields %}

View file

@ -1,7 +1,7 @@
{% extends "main/form/form_base.html" %} {% extends "main/form/form_base.html" %}
{% load i18n %} {% load i18n %}
{% block buttons %} {% block buttons %}
<input hidden value="{{ next }}" name="next"> <input hidden value="{{ next }}" name="next" />
<input type="submit" value="{% translate "Log in" %}"> <input type="reset" />
<input type="reset"> <input type="submit" value="{% translate "Log in" %}" />
{% endblock buttons %} {% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "main/form/form_base.html" %}
{% load i18n %}
{% block buttons %}
<input type="reset" />
<input type="submit" value="{% translate "Search" %}" />
{% endblock %}

View file

@ -1,4 +0,0 @@
{% include "django/forms/widgets/text.html" %}
<datalist id="{{ widget.attrs.list }}">
{% for option in widget.options %}<option value="{{ option }}"></option>{% endfor %}
</datalist>

View file

@ -1,5 +0,0 @@
{% load main_extras %}
<span class="ico-input icon-select">
{{ "square"|remix }}
{% include "main/forms/widgets/datalist.html" %}
</span>

View file

@ -1,37 +1,47 @@
{% extends "main/base.html" %} {% extends "main/base.html" %}
{% load static %} {% load static %}
{% load main_extras account_extras history_extras statement_extras %} {% load main_extras %}
{% load i18n %} {% load i18n %}
{% block link %} {% block link %}
{{ block.super }} {{ block.super }}
{% css "main/css/table.css" %} <link rel="stylesheet"
{% css "main/css/plot.css" %} href="{% static 'main/css/table.css' %}"
{% endblock link %} type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/plot.css' %}"
type="text/css" />
{% endblock %}
{% block body %} {% block body %}
<div class="split"> {% if accounts %}
<section> <h2>{% translate "Accounts" %}</h2>
<h2>{% translate "Accounts" %}</h2> {% spaceless %}
{% account_table user.account_set.all index=True %} <p>
</section> {% for acc in accounts %}
<section> <a class="big-link" href="{% url 'account' acc.id %}">{{ acc.icon|remix }}{{ acc }}</a>
<h2>{% translate "Statements" %}</h2> {% endfor %}
{% url "statements" as s_url %} </p>
{% statement_table statements statements_url=s_url %} {% endspaceless %}
</section> {% endif %}
</div> {% if transactions %}
<section> <h2>{% translate "Transactions" %}</h2>
<h2>{% translate "Categories" %}</h2> {% include "main/table/transaction.html" %}
{% spaceless %} {% endif %}
<p> {% if categories %}
{% for cat in user.category_set.all %} <h2>{% translate "Categories" %}</h2>
<a class="category" href="{{ cat.get_absolute_url }}">{{ cat.icon|remix }}{{ cat }}</a> {% spaceless %}
{% endfor %} <p>
<a class="category add" href="{% url "new_category" %}">{{ "add"|remix }}{% translate "Create category" %}</a> {% for cat in categories %}
</p> <a class="big-link" href="{% url 'category' cat.id %}">{{ cat.icon|remix }}{{ cat }}</a>
{% endspaceless %} {% endfor %}
</section> </p>
<section> {% endspaceless %}
<h2>{% translate "History" %}</h2> {% endif %}
{% history_plot user.transaction_set.all %} {% if statements %}
</section> <h2>{% translate "Statements" %}</h2>
{% endblock body %} {% include "main/table/statement.html" %}
{% endif %}
{% if history.data %}
<h2>{% translate "History" %}</h2>
{% include "main/plot/history.html" %}
{% endif %}
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load i18n %}
{% block title %}
{% block name %}{% endblock %}
{% if account %} {{ account }}{% endif %}
{% if category %} {{ category }}{% endif %}
{% if search %}
{% translate "Search" %}
{% endif %}
Nummi
{% endblock %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% endblock %}
{% block body %}
<h2>
{% block h2 %}{% endblock %}
</h2>
{% if account or category or search %}
<div class="backlinks">
{% block backlinks %}
{% if account %}
<p>
<a class="big-link" href="{{ account.get_absolute_url }}">{{ account.icon|remix }}{{ account }}</a>
</p>
{% endif %}
{% if category %}
<p>
<a class="big-link" href="{{ category.get_absolute_url }}">{{ category.icon|remix }}{{ category }}</a>
</p>
{% endif %}
{% if search %}
<p>
<a href="{% url "search" %}">{% translate "Search" %}</a>
</p>
{% endif %}
{% endblock %}
</div>
{% endif %}
{% include "main/pagination.html" %}
{% block table %}{% endblock %}
{% include "main/pagination.html" %}
{% block body_more %}{% endblock %}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% load i18n %}
{% if page_obj %}
<p id="pagination">
{% for page in paginator.page_range %}
<a href="?page={{ page }}"
{% if page == page_obj.number %}class="cur"{% endif %}>{{ page }}</a>
{% endfor %}
</p>
{% endif %}

View file

@ -1,16 +1,19 @@
{% extends "main/base.html" %} {% extends "main/base.html" %}
{% load main_extras %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block link %} {% block link %}
{{ block.super }} {{ block.super }}
{% css "main/css/table.css" %} <link rel="stylesheet"
{% css "main/css/form.css" %} href="{% static 'main/css/table.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/form.css' %}"
type="text/css" />
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<h2>{% translate "Log in" %}</h2> <h2>{% translate "Log In" %}</h2>
<form action="{% url 'login' %}" method="post"> <form action="{% url 'login' %}" method="post">
{% csrf_token %} {% csrf_token %}
{% include "main/form/login.html" %} {% include "main/form/login.html" %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,4 +0,0 @@
{% load i18n main_extras transaction_extras %}
{% if page_obj %}
<p class="pagination">{% pagination_links page_obj %}</p>
{% endif %}

View file

@ -1,13 +0,0 @@
{% load i18n main_extras %}
{% if first.show %}
<a href="?{% page_url 1 %}" class="first">1</a>
{% if first.dots %}<span></span>{% endif %}
{% endif %}
{% for page in pages %}
<a href="?{% page_url page.number %}"
{% if page.current %}class="cur"{% endif %}>{{ page.number }}</a>
{% endfor %}
{% if last.show %}
{% if last.dots %}<span></span>{% endif %}
<a href="?{% page_url last.number %}" class="last">{{ last.number }}</a>
{% endif %}

View file

@ -0,0 +1,54 @@
{% load main_extras %}
{% load i18n %}
<div class="plot">
<table class="full-width">
<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 scope="col" colspan="2">{% translate "Expenses" %}</th>
<th 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 %}{{ cat.category__icon|remix }}{% 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

@ -0,0 +1,63 @@
{% load main_extras %}
{% load i18n %}
<div class="plot">
<table class="full-width">
<colgroup>
<col class="icon">
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{{ "expand-up-down"|remix }}</th>
<th scope="col">{% translate "Month" %}</th>
<th scope="col" colspan="2">{% translate "Expenses" %}</th>
<th scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% spaceless %}
{% for date in history.data %}
<tr>
<td class="icon">
<span class="ri-{% if date.sum > 0 %}arrow-up-s-line green{% elif date.sum < 0 %}arrow-down-s-line red{% endif %}"></span>
</td>
<th class="date" scope="row">
{% if date.has_transactions %}
{% if account %}
<a href="{% url "transaction_month" account=account.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% elif category %}
<a href="{% url "transaction_month" category=category.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% else %}
<a href="{% url "transaction_month" year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% endif %}
{% else %}
{{ date.month|date:"Y-m" }}
{% endif %}
</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

@ -0,0 +1,42 @@
{% load main_extras %}
{% load i18n %}
<div id="invoices">
<table>
<colgroup>
<col class="desc" span="3">
</colgroup>
<thead>
<th>{% translate "Name" %}</th>
<th>{% translate "File" %}</th>
<th>{% translate "Delete" %}</th>
</thead>
<tbody>
{% if transaction.invoices %}
{% for invoice in transaction.invoices %}
<tr>
<th scope="row" class="l">
<a href="{{ invoice.get_absolute_url }}">{{ invoice.name }}</a>
</th>
<td>
<a href="{{ invoice.file.url }}">{% translate "File" %} [{{ invoice.file|extension }}]</a>
</td>
<td>
<a href="{{ invoice.get_delete_url }}">{% translate "Delete" %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">{% translate "No invoice" %}</td>
</tr>
{% endif %}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<a href="{% url "new_invoice" transaction.pk %}">{% translate "Create invoice" %}</a>
</td>
</tr>
</tfoot>
</table>
</div>

View file

@ -0,0 +1,62 @@
{% load main_extras %}
{% load i18n %}
{% if new_statement_url %}
<p>
<a href="{{ new_statement_url }}">{% translate "Create statement" %}</a>
</p>
{% endif %}
<div id="statements" class="table">
<table class="full-width {% if statements_url %}more{% endif %}">
<colgroup>
<col class="icon" span="2">
<col class="date">
{% if not account %}
<col class="icon">
<col class="desc">
{% endif %}
<col class="value" span="3">
</colgroup>
<thead>
<th>{{ "check"|remix }}</th>
<th>{{ "attachment"|remix }}</th>
<th>{% translate "Date" %}</th>
{% if not account %}
<th colspan="2">{% translate "Account" %}</th>
{% endif %}
<th>{% translate "Value" %}</th>
<th>{% translate "Difference" %}</th>
<th>{% translate "Transactions" %}</th>
</thead>
<tbody>
{% for snap in statements %}
<tr>
{% if snap.sum == snap.diff %}
<td class="c green">{{ "check"|remix }}</td>
{% else %}
<td class="c red">{{ "close"|remix }}</td>
{% endif %}
<td class="c">
{% if snap.file %}<a href="{{ snap.file.url }}">{{ "attachment"|remix }}</a>{% endif %}
</td>
<th class="date" scope="row">
<a href="{% url "statement" snap.id %}">{{ snap.date|date:"Y-m-d" }}</a>
</th>
{% if not account %}
<td class="r">{{ snap.account.icon|remix }}</td>
<td>
<a href="{% url "account" snap.account.id %}">{{ snap.account }}</a>
</td>
{% endif %}
<td class="value">{{ snap.value|value }}</td>
<td class="value">{{ snap.diff|pmvalue }}</td>
<td class="value">{{ snap.sum|pmvalue }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if statements_url %}
<p>
<a href="{{ statements_url }}">{% translate "View all statements" %}</a>
</p>
{% endif %}

View file

@ -0,0 +1,75 @@
{% load main_extras %}
{% load i18n %}
{% if new_transaction_url %}
<p>
<a href="{{ new_transaction_url }}">{% translate "Create transaction" %}</a>
</p>
{% endif %}
<div id="transactions" class="table">
<table class="full-width {% if transactions_url %}more{% endif %}">
<colgroup>
<col class="icon">
<col class="date">
<col class="desc">
<col class="value">
<col class="desc">
{% if not category %}
<col class="icon">
<col class="desc">
{% endif %}
{% if not account %}
<col class="icon">
<col class="desc">
{% endif %}
</colgroup>
<thead>
<th>{{ "attachment"|remix }}</th>
<th>{% translate "Date" %}</th>
<th>{% translate "Name" %}</th>
<th>{% translate "Value" %}</th>
<th>{% translate "Trader" %}</th>
{% if not category %}
<th colspan="2">{% translate "Category" %}</th>
{% endif %}
{% if not account %}
<th colspan="2">{% translate "Account" %}</th>
{% endif %}
</thead>
<tbody>
{% for trans in transactions %}
<tr>
<td class="c">
{% for invoice in trans.invoices %}<a href="{{ invoice.file.url }}">{{ "attachment"|remix }}</a>{% endfor %}
</td>
<td class="date">{{ trans.date|date:"Y-m-d" }}</td>
<th scope="row" class="l">
<a href="{% url "transaction" trans.id %}">{{ trans.name }}</a>
</th>
<td class="value">{{ trans.value|pmvalue }}</td>
<td>{{ trans.trader|default_if_none:"" }}</td>
{% if not category %}
{% if trans.category %}
<td class="r">{{ trans.category.icon|remix }}</td>
<td>
<a href="{% url "category" trans.category.id %}">{{ trans.category }}</a>
</td>
{% else %}
<td colspan="2"></td>
{% endif %}
{% endif %}
{% if not account %}
<td class="r">{{ trans.account.icon|remix }}</td>
<td>
<a href="{% url "account" trans.account.id %}">{{ trans.account }}</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if transactions_url %}
<p>
<a href="{{ transactions_url }}">{% translate "View all transactions" %}</a>
</p>
{% endif %}

View file

@ -0,0 +1,9 @@
{% extends "main/tag/value.html" %}
{% block "value" %}
{% if value %}
{% if value > 0 %}+{% endif %}
{{ block.super }}
{% else %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,5 @@
{% spaceless %}
<span>
{% block "value" %}{{ value }} €{% endblock %}
</span>
{% endspaceless %}

View file

@ -1,9 +1,4 @@
from urllib import parse
from dateutil.relativedelta import relativedelta
from django import template from django import template
from django.templatetags.static import static
from django.urls import reverse
from django.utils import formats from django.utils import formats
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -12,16 +7,16 @@ register = template.Library()
@register.filter @register.filter
def value(val, pm=False, r=2): def value(val, pm=False, r=2):
if val is None: if not val:
return "" return ""
_prefix = "" _prefix = ""
_suffix = "&nbsp;€" _suffix = "&nbsp;€"
_val = formats.number_format(val, decimal_pos=r, use_l10n=True, force_grouping=True) _val = formats.number_format(round(val, r), r, use_l10n=True, force_grouping=True)
if val > 0: if val > 0:
if pm: if pm:
_prefix += "&plus;&nbsp;" _prefix += "&plus;&nbsp;"
elif val < 0: else:
_val = _val[1:] _val = _val[1:]
_prefix += "&minus;&nbsp;" _prefix += "&minus;&nbsp;"
@ -39,31 +34,21 @@ def pmrvalue(val):
@register.filter @register.filter
def remix(icon, *args): def remix(icon, cls=""):
return remixnl(f"{icon}-line", *args) return mark_safe(f"""<span class="ri-{icon}-line {cls}"></span>""")
@register.filter @register.filter
def remixnl(icon, cls=""): def check(sum, diff):
return mark_safe(f"""<span class="ri-{icon} {cls}"></span>""") if sum == diff:
return remix("check", "green")
else:
@register.filter return remix("close", "red")
def messageicon(level):
ico = {
10: "bug",
20: "information",
25: "check",
30: "alert",
40: "error-warning",
}
return remix(ico.get(level, "question"))
@register.filter @register.filter
def extension(file): def extension(file):
return file.name.split(".", 1)[1].upper() return file.name.split(".")[-1].upper()
@register.filter @register.filter
@ -74,67 +59,3 @@ def verbose_name(obj):
@register.filter @register.filter
def adding(obj): def adding(obj):
return obj._state.adding return obj._state.adding
@register.simple_tag
def css(href):
return mark_safe(
f"""<link rel="stylesheet" href="{static(href)}" type="text/css">"""
)
@register.simple_tag
def js(href):
return mark_safe(f"""<script src="{static(href)}" defer></script>""")
@register.filter
def balance(accounts):
return sum(
statement.value for acc in accounts if (statement := acc.statement_set.first())
)
@register.inclusion_tag("main/pagination_links.html", takes_context=True)
def pagination_links(context, page_obj):
_n = 3
return {
"request": context["request"],
"pages": [
{"number": p, "current": p == page_obj.number}
for p in page_obj.paginator.page_range
if abs(p - page_obj.number) < _n
],
"first": {
"show": page_obj.number > _n,
"dots": page_obj.number > _n + 1,
},
"last": {
"show": page_obj.number <= page_obj.paginator.num_pages - _n,
"dots": page_obj.number < page_obj.paginator.num_pages - _n,
"number": page_obj.paginator.num_pages,
},
}
@register.simple_tag(takes_context=True)
def page_url(context, page):
query = context["request"].GET.copy()
query["page"] = page
return query.urlencode()
@register.simple_tag
def url_get(name, **kwargs):
kwargs = {k: v for k, v in kwargs.items() if v}
return f"{reverse(name)}?{parse.urlencode(kwargs)}"
@register.filter
def end_of_month(month):
return month + relativedelta(months=1, days=-1)
@register.filter
def end_of_year(year):
return year + relativedelta(years=1, days=-1)

View file

@ -10,6 +10,5 @@ urlpatterns = [
path("category/", include("category.urls")), path("category/", include("category.urls")),
path("statement/", include("statement.urls")), path("statement/", include("statement.urls")),
path("transaction/", include("transaction.urls")), path("transaction/", include("transaction.urls")),
path("history/", include("history.urls")),
path("search/", include("search.urls")), path("search/", include("search.urls")),
] ]

View file

@ -1,18 +0,0 @@
import json
import pathlib
from django.contrib.staticfiles import finders
def get_icons():
with open(finders.find("main/icons/remixicon.glyph.json"), "r") as f:
data = json.load(f)
return [i.removesuffix("-line") for i in data.keys() if i.endswith("-line")]
def pdf_outline_to_str(outline):
return " ".join(
(
dest.title if not isinstance(dest, list) else pdf_outline_to_str(dest)
for dest in outline
)
)

View file

@ -1,30 +1,41 @@
from django.contrib import messages from account.models import Account
from category.models import Category
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext as _
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView,
ListView, ListView,
TemplateView, TemplateView,
UpdateView, UpdateView,
) )
from statement.models import Statement
from transaction.models import Transaction
from transaction.utils import history
class IndexView(LoginRequiredMixin, TemplateView): class IndexView(LoginRequiredMixin, TemplateView):
template_name = "main/index.html" template_name = "main/index.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { _max = 8
"statements": ( _transactions = Transaction.objects.filter(user=self.request.user)
self.request.user.statement_set.exclude(account__archived=True) _statements = Statement.objects.filter(user=self.request.user)
.order_by("account__id", "-date")
.distinct("account__id") res = {
) "accounts": Account.objects.filter(user=self.request.user),
"transactions": _transactions[:_max],
"categories": Category.objects.filter(user=self.request.user),
"statements": _statements[:_max],
"history": history(_transactions.exclude(category__budget=False)),
} }
if _transactions.count() > _max:
res["transactions_url"] = reverse_lazy("transactions")
if _statements.count() > _max:
res["statements_url"] = reverse_lazy("statements")
return super().get_context_data(**kwargs) | res
class UserMixin(LoginRequiredMixin): class UserMixin(LoginRequiredMixin):
@ -44,28 +55,13 @@ class NummiCreateView(UserMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
surl = super().get_success_url() return self.next or super().get_success_url()
messages.success(
self.request,
format_html(
"<a href='{surl}'>{name}</a> {msg}",
surl=surl,
name=self.object,
msg=_("was created successfully"),
),
)
return self.next or surl
class NummiUpdateView(UserMixin, UpdateView): class NummiUpdateView(UserMixin, UpdateView):
pass pass
class NummiDetailView(UserMixin, DetailView):
pass
class NummiDeleteView(UserMixin, DeleteView): class NummiDeleteView(UserMixin, DeleteView):
template_name = "main/confirm_delete.html" template_name = "main/confirm_delete.html"
success_url = reverse_lazy("index") success_url = reverse_lazy("index")
@ -86,4 +82,4 @@ class LogoutView(auth_views.LogoutView):
class NummiListView(UserMixin, ListView): class NummiListView(UserMixin, ListView):
pass paginate_by = 96

View file

@ -11,18 +11,18 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
""" """
import os import os
import tomllib
from pathlib import Path from pathlib import Path
import toml
CONFIG_PATH = os.environ.get("NUMMI_CONFIG", None) CONFIG_PATH = os.environ.get("NUMMI_CONFIG", None)
CONFIG = dict() if CONFIG_PATH is None:
if CONFIG_PATH is not None: CONFIG = dict()
with Path(CONFIG_PATH).open("rb") as CONFIG_FILE: else:
CONFIG = tomllib.load(CONFIG_FILE) CONFIG = toml.load(CONFIG_PATH)
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
MEDIA_CONF = CONFIG.get("media", {}) MEDIA_CONF = CONFIG.get("media", {})
STATIC_CONF = CONFIG.get("static", {})
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = Path(MEDIA_CONF.get("root", "/var/lib/nummi")) MEDIA_ROOT = Path(MEDIA_CONF.get("root", "/var/lib/nummi"))
MEDIA_URL = "media/" MEDIA_URL = "media/"
@ -51,7 +51,6 @@ INSTALLED_APPS = [
"statement", "statement",
"transaction", "transaction",
"search", "search",
"history",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@ -118,14 +117,16 @@ AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = "fr-fr" LANGUAGE_CODE = "fr-fr"
TIME_ZONE = CONFIG.get("time_zone", "CET") TIME_ZONE = CONFIG.get("time_zone", "CET")
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
LOCALE_PATHS = [
"locale",
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = "static/" STATIC_URL = "static/"
STATIC_ROOT = STATIC_CONF.get("root", "/srv/nummi") STATIC_ROOT = "/srv/nummi"
LOGIN_URL = "login" LOGIN_URL = "login"
# Default primary key field type # Default primary key field type

Some files were not shown because too many files have changed in this diff Show more