Compare commits
8 commits
ad18226974
...
ee25223e73
Author | SHA1 | Date | |
---|---|---|---|
ee25223e73 | |||
a98b073eea | |||
c9d1496e00 | |||
f6e8b583cc | |||
719436f9ad | |||
bb4a8c5db1 | |||
b05c3e6760 | |||
a0d0b5d594 |
71 changed files with 1643 additions and 948 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,4 +4,4 @@ __pycache__
|
||||||
/nummi-git/
|
/nummi-git/
|
||||||
/pkg/
|
/pkg/
|
||||||
/src/
|
/src/
|
||||||
/nummi/media
|
/media
|
||||||
|
|
0
nummi/account/__init__.py
Normal file
0
nummi/account/__init__.py
Normal file
6
nummi/account/apps.py
Normal file
6
nummi/account/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "account"
|
13
nummi/account/forms.py
Normal file
13
nummi/account/forms.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from main.forms import NummiForm
|
||||||
|
|
||||||
|
from .models import Account
|
||||||
|
|
||||||
|
|
||||||
|
class AccountForm(NummiForm):
|
||||||
|
class Meta:
|
||||||
|
model = Account
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"icon",
|
||||||
|
"default",
|
||||||
|
]
|
65
nummi/account/migrations/0001_initial.py
Normal file
65
nummi/account/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:01
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("main", "0002_segmentation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
state_operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Account",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default="Account", max_length=64, verbose_name="Name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"icon",
|
||||||
|
models.SlugField(
|
||||||
|
default="bank", max_length=24, verbose_name="Icon"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("default", models.BooleanField(default=False, verbose_name="Default")),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="User",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Account",
|
||||||
|
"verbose_name_plural": "Accounts",
|
||||||
|
"ordering": ["-default", "name"],
|
||||||
|
"db_table": "account_account",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.SeparateDatabaseAndState(state_operations=state_operations)
|
||||||
|
]
|
16
nummi/account/migrations/0002_alter_account_table.py
Normal file
16
nummi/account/migrations/0002_alter_account_table.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("account", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="account",
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
0
nummi/account/migrations/__init__.py
Normal file
0
nummi/account/migrations/__init__.py
Normal file
49
nummi/account/models.py
Normal file
49
nummi/account/models.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from main.models import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class Account(UserModel):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
|
||||||
|
icon = models.SlugField(
|
||||||
|
max_length=24,
|
||||||
|
default="bank",
|
||||||
|
verbose_name=_("Icon"),
|
||||||
|
)
|
||||||
|
default = models.BooleanField(default=False, verbose_name=_("Default"))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.default:
|
||||||
|
for ac in Account.objects.filter(user=self.user, default=True):
|
||||||
|
ac.default = False
|
||||||
|
ac.save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("account", args=(self.pk,))
|
||||||
|
|
||||||
|
def get_delete_url(self):
|
||||||
|
return reverse("del_account", args=(self.pk,))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-default", "name"]
|
||||||
|
verbose_name = _("Account")
|
||||||
|
verbose_name_plural = _("Accounts")
|
||||||
|
|
||||||
|
|
||||||
|
class AccountModel(UserModel):
|
||||||
|
account = models.ForeignKey(
|
||||||
|
Account,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Account"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
|
@ -11,7 +11,7 @@
|
||||||
{% block tables %}
|
{% block tables %}
|
||||||
{% if not form.instance|adding %}
|
{% if not form.instance|adding %}
|
||||||
<h3>{% translate "Statements" %}</h3>
|
<h3>{% translate "Statements" %}</h3>
|
||||||
{% include "main/table/snapshot.html" %}
|
{% include "main/table/statement.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if transactions %}
|
{% if transactions %}
|
||||||
<h3>{% translate "Transactions" %}</h3>
|
<h3>{% translate "Transactions" %}</h3>
|
35
nummi/account/urls.py
Normal file
35
nummi/account/urls.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from django.urls import path
|
||||||
|
from statement.views import StatementCreateView
|
||||||
|
from transaction.views import TransactionMonthView
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.AccountCreateView.as_view(), name="new_account"),
|
||||||
|
path("<account>", views.AccountUpdateView.as_view(), name="account"),
|
||||||
|
path(
|
||||||
|
"<account>/transactions",
|
||||||
|
views.AccountTListView.as_view(),
|
||||||
|
name="account_transactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<account>/statements",
|
||||||
|
views.AccountSListView.as_view(),
|
||||||
|
name="account_statements",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<account>/statement",
|
||||||
|
StatementCreateView.as_view(),
|
||||||
|
name="new_statement",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<account>/delete",
|
||||||
|
views.AccountDeleteView.as_view(),
|
||||||
|
name="del_account",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<account>/history/<int:year>/<int:month>",
|
||||||
|
TransactionMonthView.as_view(),
|
||||||
|
name="transaction_month",
|
||||||
|
),
|
||||||
|
]
|
70
nummi/account/views.py
Normal file
70
nummi/account/views.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
|
||||||
|
from statement.views import StatementListView
|
||||||
|
from transaction.utils import history
|
||||||
|
from transaction.views import TransactionListView
|
||||||
|
|
||||||
|
from .forms import AccountForm
|
||||||
|
from .models import Account
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCreateView(NummiCreateView):
|
||||||
|
model = Account
|
||||||
|
form_class = AccountForm
|
||||||
|
|
||||||
|
|
||||||
|
class AccountUpdateView(NummiUpdateView):
|
||||||
|
model = Account
|
||||||
|
form_class = AccountForm
|
||||||
|
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):
|
||||||
|
model = Account
|
||||||
|
pk_url_kwarg = "account"
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMixin:
|
||||||
|
def get_queryset(self):
|
||||||
|
self.account = get_object_or_404(
|
||||||
|
Account.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs.get("account"),
|
||||||
|
)
|
||||||
|
return super().get_queryset().filter(account=self.account)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {"account": self.account}
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTListView(AccountMixin, TransactionListView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSListView(AccountMixin, StatementListView):
|
||||||
|
pass
|
0
nummi/category/__init__.py
Normal file
0
nummi/category/__init__.py
Normal file
6
nummi/category/apps.py
Normal file
6
nummi/category/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "category"
|
13
nummi/category/forms.py
Normal file
13
nummi/category/forms.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from main.forms import NummiForm
|
||||||
|
|
||||||
|
from .models import Category
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryForm(NummiForm):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"icon",
|
||||||
|
"budget",
|
||||||
|
]
|
64
nummi/category/migrations/0001_initial.py
Normal file
64
nummi/category/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:01
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
state_operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Category",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default="Category", max_length=64, verbose_name="Name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"icon",
|
||||||
|
models.SlugField(
|
||||||
|
default="folder", max_length=24, verbose_name="Icon"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("budget", models.BooleanField(default=True, verbose_name="Budget")),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="User",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Category",
|
||||||
|
"verbose_name_plural": "Categories",
|
||||||
|
"ordering": ["name"],
|
||||||
|
"db_table": "category_category",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.SeparateDatabaseAndState(state_operations=state_operations)
|
||||||
|
]
|
16
nummi/category/migrations/0002_alter_category_table.py
Normal file
16
nummi/category/migrations/0002_alter_category_table.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("category", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="category",
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
0
nummi/category/migrations/__init__.py
Normal file
0
nummi/category/migrations/__init__.py
Normal file
33
nummi/category/models.py
Normal file
33
nummi/category/models.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from main.models import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class Category(UserModel):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=64, default=_("Category"), verbose_name=_("Name")
|
||||||
|
)
|
||||||
|
icon = models.SlugField(
|
||||||
|
max_length=24,
|
||||||
|
default="folder",
|
||||||
|
verbose_name=_("Icon"),
|
||||||
|
)
|
||||||
|
budget = models.BooleanField(default=True, verbose_name=_("Budget"))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("category", args=(self.pk,))
|
||||||
|
|
||||||
|
def get_delete_url(self):
|
||||||
|
return reverse("del_category", args=(self.pk,))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
verbose_name = _("Category")
|
||||||
|
verbose_name_plural = _("Categories")
|
|
@ -9,9 +9,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
|
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
|
||||||
{% block tables %}
|
{% block tables %}
|
||||||
{% if form.instance.transactions %}
|
{% if transactions %}
|
||||||
<h3>{% translate "Transactions" %}</h3>
|
<h3>{% translate "Transactions" %}</h3>
|
||||||
{% include "main/table/transaction.html" %}
|
{% include "main/table/transaction.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if history.data %}
|
||||||
<h3>{% translate "History" %}</h3>
|
<h3>{% translate "History" %}</h3>
|
||||||
{% include "main/plot/history.html" %}
|
{% include "main/plot/history.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
20
nummi/category/urls.py
Normal file
20
nummi/category/urls.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django.urls import path
|
||||||
|
from transaction.views import TransactionMonthView
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.CategoryCreateView.as_view(), name="new_category"),
|
||||||
|
path("<category>", views.CategoryUpdateView.as_view(), name="category"),
|
||||||
|
path(
|
||||||
|
"<category>/transactions",
|
||||||
|
views.CategoryTListView.as_view(),
|
||||||
|
name="category_transactions",
|
||||||
|
),
|
||||||
|
path("<category>/delete", views.CategoryDeleteView.as_view(), name="del_category"),
|
||||||
|
path(
|
||||||
|
"<category>/history/<int:year>/<int:month>",
|
||||||
|
TransactionMonthView.as_view(),
|
||||||
|
name="transaction_month",
|
||||||
|
),
|
||||||
|
]
|
52
nummi/category/views.py
Normal file
52
nummi/category/views.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
|
||||||
|
from transaction.utils import history
|
||||||
|
from transaction.views import TransactionListView
|
||||||
|
|
||||||
|
from .forms import CategoryForm
|
||||||
|
from .models import Category
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryCreateView(NummiCreateView):
|
||||||
|
model = Category
|
||||||
|
form_class = CategoryForm
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryUpdateView(NummiUpdateView):
|
||||||
|
model = Category
|
||||||
|
form_class = CategoryForm
|
||||||
|
pk_url_kwarg = "category"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
category = data["form"].instance
|
||||||
|
|
||||||
|
return data | {
|
||||||
|
"transactions": category.transaction_set.all()[:8],
|
||||||
|
"transactions_url": reverse_lazy(
|
||||||
|
"category_transactions", args=(category.pk,)
|
||||||
|
),
|
||||||
|
"history": history(category.transaction_set),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryDeleteView(NummiDeleteView):
|
||||||
|
model = Category
|
||||||
|
pk_url_kwarg = "category"
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryMixin:
|
||||||
|
def get_queryset(self):
|
||||||
|
self.category = get_object_or_404(
|
||||||
|
Category.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs.get("category"),
|
||||||
|
)
|
||||||
|
return super().get_queryset().filter(category=self.category)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {"category": self.category}
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryTListView(CategoryMixin, TransactionListView):
|
||||||
|
pass
|
|
@ -1,7 +1,4 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .models import Account, Category, Invoice, Snapshot, Transaction
|
|
||||||
|
|
||||||
|
|
||||||
class NummiFileInput(forms.ClearableFileInput):
|
class NummiFileInput(forms.ClearableFileInput):
|
||||||
|
@ -13,101 +10,3 @@ class NummiForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, user, **kwargs):
|
def __init__(self, *args, user, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AccountForm(NummiForm):
|
|
||||||
class Meta:
|
|
||||||
model = Account
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"icon",
|
|
||||||
"default",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryForm(NummiForm):
|
|
||||||
class Meta:
|
|
||||||
model = Category
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"icon",
|
|
||||||
"budget",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(NummiForm):
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = [
|
|
||||||
"snapshot",
|
|
||||||
"name",
|
|
||||||
"value",
|
|
||||||
"date",
|
|
||||||
"real_date",
|
|
||||||
"category",
|
|
||||||
"trader",
|
|
||||||
"payment",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
_user = kwargs.get("user")
|
|
||||||
_disable_snapshot = kwargs.pop("disable_snapshot", False)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["category"].queryset = Category.objects.filter(user=_user)
|
|
||||||
self.fields["snapshot"].queryset = Snapshot.objects.filter(user=_user)
|
|
||||||
if _disable_snapshot:
|
|
||||||
self.fields["snapshot"].disabled = True
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceForm(NummiForm):
|
|
||||||
prefix = "invoice"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Invoice
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"file",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"file": NummiFileInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotForm(NummiForm):
|
|
||||||
class Meta:
|
|
||||||
model = Snapshot
|
|
||||||
fields = ["account", "start_date", "date", "start_value", "value", "file"]
|
|
||||||
widgets = {
|
|
||||||
"file": NummiFileInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
_user = kwargs.get("user")
|
|
||||||
_disable_account = kwargs.pop("disable_account", False)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["account"].queryset = Account.objects.filter(user=_user)
|
|
||||||
self.fields["transactions"] = forms.MultipleChoiceField(
|
|
||||||
choices=(
|
|
||||||
((_transaction.id), _transaction)
|
|
||||||
for _transaction in Transaction.objects.filter(user=_user)
|
|
||||||
),
|
|
||||||
label=_("Add transactions"),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
if _disable_account:
|
|
||||||
self.fields["account"].disabled = True
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
instance = super().save(*args, **kwargs)
|
|
||||||
new_transactions = Transaction.objects.filter(
|
|
||||||
id__in=self.cleaned_data["transactions"]
|
|
||||||
)
|
|
||||||
|
|
||||||
instance.transaction_set.add(*new_transactions, bulk=False)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class SearchForm(forms.Form):
|
|
||||||
template_name = "main/form/search.html"
|
|
||||||
search = forms.CharField(label=_("Search"), max_length=128)
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import uuid
|
||||||
import django.contrib.postgres.operations
|
import django.contrib.postgres.operations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import main.utils
|
import media.utils
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
max_length=256,
|
max_length=256,
|
||||||
upload_to=main.utils.get_path,
|
upload_to=media.utils.get_path,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.FileExtensionValidator(["pdf"])
|
django.core.validators.FileExtensionValidator(["pdf"])
|
||||||
],
|
],
|
||||||
|
@ -315,7 +315,7 @@ class Migration(migrations.Migration):
|
||||||
"file",
|
"file",
|
||||||
models.FileField(
|
models.FileField(
|
||||||
max_length=128,
|
max_length=128,
|
||||||
upload_to=main.utils.get_path,
|
upload_to=media.utils.get_path,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.FileExtensionValidator(["pdf"])
|
django.core.validators.FileExtensionValidator(["pdf"])
|
||||||
],
|
],
|
||||||
|
|
33
nummi/main/migrations/0002_segmentation.py
Normal file
33
nummi/main/migrations/0002_segmentation.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:01
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("main", "0001_v1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
database_operations = [
|
||||||
|
migrations.AlterModelTable("Account", "account_account"),
|
||||||
|
migrations.AlterModelTable("Category", "category_category"),
|
||||||
|
migrations.AlterModelTable("Snapshot", "statement_statement"),
|
||||||
|
migrations.AlterModelTable("Transaction", "transaction_transaction"),
|
||||||
|
migrations.AlterModelTable("Invoice", "transaction_invoice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
state_operations = [
|
||||||
|
migrations.DeleteModel("Account"),
|
||||||
|
migrations.DeleteModel("Category"),
|
||||||
|
migrations.DeleteModel("Snapshot"),
|
||||||
|
migrations.DeleteModel("Transaction"),
|
||||||
|
migrations.DeleteModel("Invoice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField("Transaction", "snapshot", "statement"),
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
database_operations=database_operations,
|
||||||
|
state_operations=state_operations,
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,15 +1,7 @@
|
||||||
import datetime
|
|
||||||
import pathlib
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.db import models
|
||||||
from django.db import models, transaction
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .utils import get_path
|
|
||||||
|
|
||||||
|
|
||||||
class UserModel(models.Model):
|
class UserModel(models.Model):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
|
@ -21,280 +13,3 @@ class UserModel(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Account(UserModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
|
|
||||||
icon = models.SlugField(
|
|
||||||
max_length=24,
|
|
||||||
default="bank",
|
|
||||||
verbose_name=_("Icon"),
|
|
||||||
)
|
|
||||||
default = models.BooleanField(default=False, verbose_name=_("Default"))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if self.default:
|
|
||||||
for ac in Account.objects.filter(user=self.user, default=True):
|
|
||||||
ac.default = False
|
|
||||||
ac.save()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("account", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
def get_delete_url(self):
|
|
||||||
return reverse("del_account", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transactions(self):
|
|
||||||
return Transaction.objects.filter(account=self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def snapshots(self):
|
|
||||||
return Snapshot.objects.filter(account=self)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["-default", "name"]
|
|
||||||
verbose_name = _("Account")
|
|
||||||
verbose_name_plural = _("Accounts")
|
|
||||||
|
|
||||||
|
|
||||||
class AccountModel(UserModel):
|
|
||||||
account = models.ForeignKey(
|
|
||||||
Account,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("Account"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Category(UserModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=64, default=_("Category"), verbose_name=_("Name")
|
|
||||||
)
|
|
||||||
icon = models.SlugField(
|
|
||||||
max_length=24,
|
|
||||||
default="folder",
|
|
||||||
verbose_name=_("Icon"),
|
|
||||||
)
|
|
||||||
budget = models.BooleanField(default=True, verbose_name=_("Budget"))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("category", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
def get_delete_url(self):
|
|
||||||
return reverse("del_category", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transactions(self):
|
|
||||||
return Transaction.objects.filter(category=self)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["name"]
|
|
||||||
verbose_name = _("Category")
|
|
||||||
verbose_name_plural = _("Categories")
|
|
||||||
|
|
||||||
|
|
||||||
class Snapshot(AccountModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
date = models.DateField(default=datetime.date.today, verbose_name=_("End date"))
|
|
||||||
start_date = models.DateField(
|
|
||||||
default=datetime.date.today, verbose_name=_("Start date")
|
|
||||||
)
|
|
||||||
value = models.DecimalField(
|
|
||||||
max_digits=12, decimal_places=2, default=0, verbose_name=_("End value")
|
|
||||||
)
|
|
||||||
start_value = models.DecimalField(
|
|
||||||
max_digits=12, decimal_places=2, default=0, verbose_name=_("Start value")
|
|
||||||
)
|
|
||||||
diff = models.DecimalField(
|
|
||||||
max_digits=12,
|
|
||||||
decimal_places=2,
|
|
||||||
default=0,
|
|
||||||
verbose_name=_("Difference"),
|
|
||||||
editable=False,
|
|
||||||
)
|
|
||||||
sum = models.DecimalField(
|
|
||||||
max_digits=12,
|
|
||||||
decimal_places=2,
|
|
||||||
default=0,
|
|
||||||
verbose_name=_("Transaction difference"),
|
|
||||||
editable=False,
|
|
||||||
)
|
|
||||||
file = models.FileField(
|
|
||||||
upload_to=get_path,
|
|
||||||
validators=[FileExtensionValidator(["pdf"])],
|
|
||||||
verbose_name=_("File"),
|
|
||||||
max_length=256,
|
|
||||||
blank=True,
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
desc = _("%(date)s statement") % {"date": self.date}
|
|
||||||
if hasattr(self, "account"):
|
|
||||||
return f"{desc} – {self.account}"
|
|
||||||
return desc
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if Snapshot.objects.filter(id=self.id).exists():
|
|
||||||
_prever = Snapshot.objects.get(id=self.id)
|
|
||||||
if _prever.file and _prever.file != self.file:
|
|
||||||
pathlib.Path(_prever.file.path).unlink(missing_ok=True)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
for trans in self.transaction_set.all():
|
|
||||||
trans.save()
|
|
||||||
|
|
||||||
self.diff = self.value - self.start_value
|
|
||||||
self.sum = (
|
|
||||||
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
|
|
||||||
)
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def update_sum(self):
|
|
||||||
self.sum = (
|
|
||||||
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
|
|
||||||
)
|
|
||||||
super().save()
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
if self.file:
|
|
||||||
self.file.delete()
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("snapshot", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
def get_delete_url(self):
|
|
||||||
return reverse("del_snapshot", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["-date", "account"]
|
|
||||||
verbose_name = _("Statement")
|
|
||||||
verbose_name_plural = _("Statements")
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(UserModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=256, default=_("Transaction"), verbose_name=_("Name")
|
|
||||||
)
|
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
|
||||||
value = models.DecimalField(
|
|
||||||
max_digits=12, decimal_places=2, default=0, verbose_name=_("Value")
|
|
||||||
)
|
|
||||||
date = models.DateField(default=datetime.date.today, verbose_name=_("Date"))
|
|
||||||
real_date = models.DateField(blank=True, null=True, verbose_name=_("Real date"))
|
|
||||||
trader = models.CharField(
|
|
||||||
max_length=128, blank=True, null=True, verbose_name=_("Trader")
|
|
||||||
)
|
|
||||||
payment = models.CharField(
|
|
||||||
max_length=128, blank=True, null=True, verbose_name=_("Payment")
|
|
||||||
)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
Category,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_("Category"),
|
|
||||||
)
|
|
||||||
snapshot = models.ForeignKey(
|
|
||||||
Snapshot,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("Statement"),
|
|
||||||
)
|
|
||||||
account = models.ForeignKey(
|
|
||||||
Account,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("Account"),
|
|
||||||
editable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if Transaction.objects.filter(pk=self.pk):
|
|
||||||
prev_self = Transaction.objects.get(pk=self.pk)
|
|
||||||
else:
|
|
||||||
prev_self = None
|
|
||||||
self.account = self.snapshot.account
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
if prev_self is not None:
|
|
||||||
prev_self.snapshot.update_sum()
|
|
||||||
self.snapshot.update_sum()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("transaction", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
def get_delete_url(self):
|
|
||||||
return reverse("del_transaction", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def invoices(self):
|
|
||||||
return Invoice.objects.filter(transaction=self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_invoice(self):
|
|
||||||
return self.invoices.count() > 0
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["-date", "snapshot"]
|
|
||||||
verbose_name = _("Transaction")
|
|
||||||
verbose_name_plural = _("Transactions")
|
|
||||||
|
|
||||||
|
|
||||||
class Invoice(UserModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=256, default=_("Invoice"), verbose_name=_("Name")
|
|
||||||
)
|
|
||||||
file = models.FileField(
|
|
||||||
upload_to=get_path,
|
|
||||||
validators=[FileExtensionValidator(["pdf"])],
|
|
||||||
verbose_name=_("File"),
|
|
||||||
max_length=128,
|
|
||||||
)
|
|
||||||
transaction = models.ForeignKey(
|
|
||||||
Transaction, on_delete=models.CASCADE, editable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if Invoice.objects.filter(id=self.id).exists():
|
|
||||||
_prever = Invoice.objects.get(id=self.id)
|
|
||||||
if _prever.file and _prever.file != self.file:
|
|
||||||
pathlib.Path(_prever.file.path).unlink(missing_ok=True)
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
self.file.delete()
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
"invoice", kwargs={"transaction_pk": self.transaction.pk, "pk": self.pk}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_delete_url(self):
|
|
||||||
return reverse(
|
|
||||||
"del_invoice", kwargs={"transaction_pk": self.transaction.pk, "pk": self.pk}
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Invoice")
|
|
||||||
verbose_name_plural = _("Invoices")
|
|
||||||
ordering = ["transaction", "name"]
|
|
||||||
|
|
|
@ -33,9 +33,9 @@
|
||||||
accesskey="h">{% translate "Home" %}</a>
|
accesskey="h">{% translate "Home" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "snapshots" %}"
|
<a href="{% url "statements" %}"
|
||||||
class="{% if request.resolver_match.url_name == "snapshots" %}cur{% endif %}">
|
class="{% if request.resolver_match.url_name == "statements" %}cur{% endif %}">
|
||||||
{% translate "Snapshots" %}
|
{% translate "Statements" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -50,9 +50,9 @@
|
||||||
accesskey="a">{% translate "Create account" %}</a>
|
accesskey="a">{% translate "Create account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "new_snapshot" %}"
|
<a href="{% url "new_statement" %}"
|
||||||
class="{% if request.resolver_match.url_name == "new_snapshot" %}cur{% endif %}"
|
class="{% if request.resolver_match.url_name == "new_statement" %}cur{% endif %}"
|
||||||
accesskey="s">{% translate "Create snapshot" %}</a>
|
accesskey="s">{% translate "Create statement" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "new_category" %}"
|
<a href="{% url "new_category" %}"
|
||||||
|
|
|
@ -36,9 +36,9 @@
|
||||||
</p>
|
</p>
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if snapshots %}
|
{% if statements %}
|
||||||
<h2>{% translate "Snapshots" %}</h2>
|
<h2>{% translate "Statements" %}</h2>
|
||||||
{% include "main/table/snapshot.html" %}
|
{% include "main/table/statement.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if history.data %}
|
{% if history.data %}
|
||||||
<h2>{% translate "History" %}</h2>
|
<h2>{% translate "History" %}</h2>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% extends "main/base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
{% load main_extras %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block link %}
|
|
||||||
{{ block.super }}
|
|
||||||
<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 %}
|
|
||||||
{% block body %}
|
|
||||||
<h2>{% translate "Transactions" %} – {{ month|date:"F Y"|capfirst }}</h2>
|
|
||||||
{% if account %}
|
|
||||||
<p>
|
|
||||||
<a href="{% url "account" account.pk %}">{{ account.icon|remix }}{{ account }}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if category %}
|
|
||||||
<p>
|
|
||||||
<a href="{% url "category" category.pk %}">{{ category.icon|remix }}{{ category }}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% include "main/table/transaction.html" %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,12 +1,12 @@
|
||||||
{% load main_extras %}
|
{% load main_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if new_snapshot_url %}
|
{% if new_statement_url %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ new_snapshot_url }}">{% translate "Create statement" %}</a>
|
<a href="{{ new_statement_url }}">{% translate "Create statement" %}</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="snapshots" class="table">
|
<div id="statements" class="table">
|
||||||
<table class="full-width {% if snapshots_url %}more{% endif %}">
|
<table class="full-width {% if statements_url %}more{% endif %}">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="icon" span="2">
|
<col class="icon" span="2">
|
||||||
<col class="date">
|
<col class="date">
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<th>{% translate "Transactions" %}</th>
|
<th>{% translate "Transactions" %}</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for snap in snapshots %}
|
{% for snap in statements %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if snap.sum == snap.diff %}
|
{% if snap.sum == snap.diff %}
|
||||||
<td class="c green">{{ "check"|remix }}</td>
|
<td class="c green">{{ "check"|remix }}</td>
|
||||||
|
@ -39,12 +39,12 @@
|
||||||
{% if snap.file %}<a href="{{ snap.file.url }}">{{ "attachment"|remix }}</a>{% endif %}
|
{% if snap.file %}<a href="{{ snap.file.url }}">{{ "attachment"|remix }}</a>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<th class="date" scope="row">
|
<th class="date" scope="row">
|
||||||
<a href="{% url "snapshot" pk=snap.id %}">{{ snap.date|date:"Y-m-d" }}</a>
|
<a href="{% url "statement" snap.id %}">{{ snap.date|date:"Y-m-d" }}</a>
|
||||||
</th>
|
</th>
|
||||||
{% if not account %}
|
{% if not account %}
|
||||||
<td class="r">{{ snap.account.icon|remix }}</td>
|
<td class="r">{{ snap.account.icon|remix }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "account" pk=snap.account.id %}">{{ snap.account }}</a>
|
<a href="{% url "account" snap.account.id %}">{{ snap.account }}</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="value">{{ snap.value|value }}</td>
|
<td class="value">{{ snap.value|value }}</td>
|
||||||
|
@ -55,8 +55,8 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if snapshots_url %}
|
{% if statements_url %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ snapshots_url }}">{% translate "View all statements" %}</a>
|
<a href="{{ statements_url }}">{% translate "View all statements" %}</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -43,7 +43,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="date">{{ trans.date|date:"Y-m-d" }}</td>
|
<td class="date">{{ trans.date|date:"Y-m-d" }}</td>
|
||||||
<th scope="row" class="l">
|
<th scope="row" class="l">
|
||||||
<a href="{% url "transaction" pk=trans.id %}">{{ trans.name }}</a>
|
<a href="{% url "transaction" trans.id %}">{{ trans.name }}</a>
|
||||||
</th>
|
</th>
|
||||||
<td class="value">{{ trans.value|pmvalue }}</td>
|
<td class="value">{{ trans.value|pmvalue }}</td>
|
||||||
<td>{{ trans.trader|default_if_none:"" }}</td>
|
<td>{{ trans.trader|default_if_none:"" }}</td>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
{% if trans.category %}
|
{% if trans.category %}
|
||||||
<td class="r">{{ trans.category.icon|remix }}</td>
|
<td class="r">{{ trans.category.icon|remix }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "category" pk=trans.category.id %}">{{ trans.category }}</a>
|
<a href="{% url "category" trans.category.id %}">{{ trans.category }}</a>
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
{% if not account %}
|
{% if not account %}
|
||||||
<td class="r">{{ trans.account.icon|remix }}</td>
|
<td class="r">{{ trans.account.icon|remix }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "account" pk=trans.account.id %}">{{ trans.account }}</a>
|
<a href="{% url "account" trans.account.id %}">{{ trans.account }}</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,98 +1,14 @@
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.IndexView.as_view(), name="index"),
|
path("", views.IndexView.as_view(), name="index"),
|
||||||
path("media/user/<username>/<path:path>", views.MediaView.as_view(), name="media"),
|
|
||||||
path("login", views.LoginView.as_view(), name="login"),
|
path("login", views.LoginView.as_view(), name="login"),
|
||||||
path("logout", views.LogoutView.as_view(), name="logout"),
|
path("logout", views.LogoutView.as_view(), name="logout"),
|
||||||
path("transactions", views.TransactionListView.as_view(), name="transactions"),
|
path("account/", include("account.urls")),
|
||||||
path("snapshots", views.SnapshotListView.as_view(), name="snapshots"),
|
path("category/", include("category.urls")),
|
||||||
path("account", views.AccountCreateView.as_view(), name="new_account"),
|
path("statement/", include("statement.urls")),
|
||||||
path("transaction", views.TransactionCreateView.as_view(), name="new_transaction"),
|
path("transaction/", include("transaction.urls")),
|
||||||
path(
|
path("search/", include("search.urls")),
|
||||||
"transaction/<transaction_pk>/invoice",
|
|
||||||
views.InvoiceCreateView.as_view(),
|
|
||||||
name="new_invoice",
|
|
||||||
),
|
|
||||||
path("category", views.CategoryCreateView.as_view(), name="new_category"),
|
|
||||||
path("snapshot", views.SnapshotCreateView.as_view(), name="new_snapshot"),
|
|
||||||
path("account/<pk>", views.AccountUpdateView.as_view(), name="account"),
|
|
||||||
path(
|
|
||||||
"account/<pk>/transactions",
|
|
||||||
views.AccountTListView.as_view(),
|
|
||||||
name="account_transactions",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"account/<pk>/snapshots",
|
|
||||||
views.AccountSListView.as_view(),
|
|
||||||
name="account_snapshots",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"account/<account>/snapshot",
|
|
||||||
views.SnapshotCreateView.as_view(),
|
|
||||||
name="new_snapshot",
|
|
||||||
),
|
|
||||||
path("transaction/<pk>", views.TransactionUpdateView.as_view(), name="transaction"),
|
|
||||||
path(
|
|
||||||
"transaction/<transaction_pk>/invoice/<pk>",
|
|
||||||
views.InvoiceUpdateView.as_view(),
|
|
||||||
name="invoice",
|
|
||||||
),
|
|
||||||
path("category/<pk>", views.CategoryUpdateView.as_view(), name="category"),
|
|
||||||
path(
|
|
||||||
"category/<pk>/transactions",
|
|
||||||
views.CategoryTListView.as_view(),
|
|
||||||
name="category_transactions",
|
|
||||||
),
|
|
||||||
path("snapshot/<pk>", views.SnapshotUpdateView.as_view(), name="snapshot"),
|
|
||||||
path(
|
|
||||||
"snapshot/<pk>/transactions",
|
|
||||||
views.SnapshotTListView.as_view(),
|
|
||||||
name="snapshot_transactions",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"snapshot/<snapshot>/transaction",
|
|
||||||
views.TransactionCreateView.as_view(),
|
|
||||||
name="new_transaction",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"account/<pk>/delete",
|
|
||||||
views.AccountDeleteView.as_view(),
|
|
||||||
name="del_account",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"transaction/<pk>/delete",
|
|
||||||
views.TransactionDeleteView.as_view(),
|
|
||||||
name="del_transaction",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"transaction/<transaction_pk>/invoice/<pk>/delete",
|
|
||||||
views.InvoiceDeleteView.as_view(),
|
|
||||||
name="del_invoice",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"category/<pk>/delete", views.CategoryDeleteView.as_view(), name="del_category"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"snapshot/<pk>/delete", views.SnapshotDeleteView.as_view(), name="del_snapshot"
|
|
||||||
),
|
|
||||||
path("search", views.SearchFormView.as_view(), name="search"),
|
|
||||||
path("search/<search>", views.SearchView.as_view(), name="search"),
|
|
||||||
path(
|
|
||||||
"history/<int:year>/<int:month>",
|
|
||||||
views.TransactionMonthView.as_view(),
|
|
||||||
name="transaction_month",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"account/<account>/history/<int:year>/<int:month>",
|
|
||||||
views.TransactionMonthView.as_view(),
|
|
||||||
name="transaction_month",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"category/<category>/history/<int:year>/<int:month>",
|
|
||||||
views.TransactionMonthView.as_view(),
|
|
||||||
name="transaction_month",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,39 +1,18 @@
|
||||||
from django.conf import settings
|
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.contrib.postgres.search import (
|
|
||||||
SearchQuery,
|
|
||||||
SearchRank,
|
|
||||||
SearchVector,
|
|
||||||
TrigramSimilarity,
|
|
||||||
)
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.db import models
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views import View
|
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
CreateView,
|
CreateView,
|
||||||
DeleteView,
|
DeleteView,
|
||||||
FormView,
|
|
||||||
ListView,
|
ListView,
|
||||||
TemplateView,
|
TemplateView,
|
||||||
UpdateView,
|
UpdateView,
|
||||||
)
|
)
|
||||||
from django.views.generic.dates import MonthArchiveView
|
from statement.models import Statement
|
||||||
from django.views.static import serve
|
from transaction.models import Transaction
|
||||||
|
from transaction.utils import history
|
||||||
from .forms import (
|
|
||||||
AccountForm,
|
|
||||||
CategoryForm,
|
|
||||||
InvoiceForm,
|
|
||||||
SearchForm,
|
|
||||||
SnapshotForm,
|
|
||||||
TransactionForm,
|
|
||||||
)
|
|
||||||
from .models import Account, Category, Invoice, Snapshot, Transaction
|
|
||||||
from .utils import history
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(LoginRequiredMixin, TemplateView):
|
class IndexView(LoginRequiredMixin, TemplateView):
|
||||||
|
@ -42,19 +21,19 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
_max = 8
|
_max = 8
|
||||||
_transactions = Transaction.objects.filter(user=self.request.user)
|
_transactions = Transaction.objects.filter(user=self.request.user)
|
||||||
_snapshots = Snapshot.objects.filter(user=self.request.user)
|
_statements = Statement.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
"accounts": Account.objects.filter(user=self.request.user),
|
"accounts": Account.objects.filter(user=self.request.user),
|
||||||
"transactions": _transactions[:_max],
|
"transactions": _transactions[:_max],
|
||||||
"categories": Category.objects.filter(user=self.request.user),
|
"categories": Category.objects.filter(user=self.request.user),
|
||||||
"snapshots": _snapshots[:_max],
|
"statements": _statements[:_max],
|
||||||
"history": history(_transactions.exclude(category__budget=False)),
|
"history": history(_transactions.exclude(category__budget=False)),
|
||||||
}
|
}
|
||||||
if _transactions.count() > _max:
|
if _transactions.count() > _max:
|
||||||
res["transactions_url"] = reverse_lazy("transactions")
|
res["transactions_url"] = reverse_lazy("transactions")
|
||||||
if _snapshots.count() > _max:
|
if _statements.count() > _max:
|
||||||
res["snapshots_url"] = reverse_lazy("snapshots")
|
res["statements_url"] = reverse_lazy("statements")
|
||||||
|
|
||||||
return super().get_context_data(**kwargs) | res
|
return super().get_context_data(**kwargs) | res
|
||||||
|
|
||||||
|
@ -87,6 +66,11 @@ 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")
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
_res = super().get_form_kwargs()
|
||||||
|
_res.pop("user")
|
||||||
|
return _res
|
||||||
|
|
||||||
|
|
||||||
class LoginView(auth_views.LoginView):
|
class LoginView(auth_views.LoginView):
|
||||||
template_name = "main/login.html"
|
template_name = "main/login.html"
|
||||||
|
@ -97,371 +81,5 @@ class LogoutView(auth_views.LogoutView):
|
||||||
next_page = "login"
|
next_page = "login"
|
||||||
|
|
||||||
|
|
||||||
class AccountCreateView(NummiCreateView):
|
|
||||||
model = Account
|
|
||||||
form_class = AccountForm
|
|
||||||
template_name = "main/form/account.html"
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionCreateView(NummiCreateView):
|
|
||||||
model = Transaction
|
|
||||||
form_class = TransactionForm
|
|
||||||
template_name = "main/form/transaction.html"
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
_queryset = Snapshot.objects.filter(user=self.request.user)
|
|
||||||
if "snapshot" in self.kwargs:
|
|
||||||
self.snapshot = get_object_or_404(_queryset, pk=self.kwargs["snapshot"])
|
|
||||||
else:
|
|
||||||
self.snapshot = _queryset.first()
|
|
||||||
return {"snapshot": self.snapshot}
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
if "snapshot" in self.kwargs:
|
|
||||||
return super().get_form_kwargs() | {"disable_snapshot": True}
|
|
||||||
return super().get_form_kwargs()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
if "snapshot" in self.kwargs:
|
|
||||||
return super().get_context_data(**kwargs) | {"snapshot": self.snapshot}
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceCreateView(NummiCreateView):
|
|
||||||
model = Invoice
|
|
||||||
form_class = InvoiceForm
|
|
||||||
template_name = "main/form/invoice.html"
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.transaction = get_object_or_404(
|
|
||||||
Transaction.objects.filter(user=self.request.user),
|
|
||||||
pk=self.kwargs["transaction_pk"],
|
|
||||||
)
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy("transaction", kwargs={"pk": self.object.transaction.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryCreateView(NummiCreateView):
|
|
||||||
model = Category
|
|
||||||
form_class = CategoryForm
|
|
||||||
template_name = "main/form/category.html"
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotCreateView(NummiCreateView):
|
|
||||||
model = Snapshot
|
|
||||||
form_class = SnapshotForm
|
|
||||||
template_name = "main/form/snapshot.html"
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
_queryset = Account.objects.filter(user=self.request.user)
|
|
||||||
if "account" in self.kwargs:
|
|
||||||
self.account = get_object_or_404(_queryset, pk=self.kwargs["account"])
|
|
||||||
else:
|
|
||||||
self.account = _queryset.first()
|
|
||||||
return {"account": self.account}
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
if "account" in self.kwargs:
|
|
||||||
return super().get_form_kwargs() | {"disable_account": True}
|
|
||||||
return super().get_form_kwargs()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
if "account" in self.kwargs:
|
|
||||||
return super().get_context_data(**kwargs) | {"account": self.account}
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountUpdateView(NummiUpdateView):
|
|
||||||
model = Account
|
|
||||||
form_class = AccountForm
|
|
||||||
template_name = "main/form/account.html"
|
|
||||||
|
|
||||||
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,)
|
|
||||||
)
|
|
||||||
_snapshots = account.snapshot_set.all()
|
|
||||||
if _snapshots.count() > _max:
|
|
||||||
data["snapshots_url"] = reverse_lazy(
|
|
||||||
"account_snapshots", args=(account.pk,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return data | {
|
|
||||||
"transactions": _transactions[:8],
|
|
||||||
"new_snapshot_url": reverse_lazy(
|
|
||||||
"new_snapshot", kwargs={"account": account.pk}
|
|
||||||
),
|
|
||||||
"snapshots": _snapshots[:8],
|
|
||||||
"history": history(account.transaction_set),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionUpdateView(NummiUpdateView):
|
|
||||||
model = Transaction
|
|
||||||
form_class = TransactionForm
|
|
||||||
template_name = "main/form/transaction.html"
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceUpdateView(NummiUpdateView):
|
|
||||||
model = Invoice
|
|
||||||
form_class = InvoiceForm
|
|
||||||
template_name = "main/form/invoice.html"
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy("transaction", kwargs={"pk": self.object.transaction.pk})
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(
|
|
||||||
transaction=get_object_or_404(
|
|
||||||
Transaction, pk=self.kwargs["transaction_pk"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryUpdateView(NummiUpdateView):
|
|
||||||
model = Category
|
|
||||||
form_class = CategoryForm
|
|
||||||
template_name = "main/form/category.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
data = super().get_context_data(**kwargs)
|
|
||||||
category = data["form"].instance
|
|
||||||
|
|
||||||
return data | {
|
|
||||||
"transactions": category.transaction_set.all()[:8],
|
|
||||||
"transactions_url": reverse_lazy(
|
|
||||||
"category_transactions", args=(category.pk,)
|
|
||||||
),
|
|
||||||
"history": history(category.transaction_set),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotUpdateView(NummiUpdateView):
|
|
||||||
model = Snapshot
|
|
||||||
form_class = SnapshotForm
|
|
||||||
template_name = "main/form/snapshot.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
data = super().get_context_data(**kwargs)
|
|
||||||
snapshot = data["form"].instance
|
|
||||||
|
|
||||||
_transactions = snapshot.transaction_set.all()
|
|
||||||
if _transactions:
|
|
||||||
_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")
|
|
||||||
)
|
|
||||||
data["categories"] = {
|
|
||||||
"data": _categories,
|
|
||||||
"max": max(
|
|
||||||
_categories.aggregate(
|
|
||||||
max=models.Max("sum_p", default=0),
|
|
||||||
min=models.Min("sum_m", default=0),
|
|
||||||
).values(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
return data | {
|
|
||||||
"account": snapshot.account,
|
|
||||||
"new_transaction_url": reverse_lazy(
|
|
||||||
"new_transaction", kwargs={"snapshot": snapshot.pk}
|
|
||||||
),
|
|
||||||
"transactions": _transactions,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AccountDeleteView(NummiDeleteView):
|
|
||||||
model = Account
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionDeleteView(NummiDeleteView):
|
|
||||||
model = Transaction
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceDeleteView(NummiDeleteView):
|
|
||||||
model = Invoice
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy("transaction", kwargs={"pk": self.object.transaction.pk})
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(
|
|
||||||
transaction=get_object_or_404(
|
|
||||||
Transaction, pk=self.kwargs["transaction_pk"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryDeleteView(NummiDeleteView):
|
|
||||||
model = Category
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotDeleteView(NummiDeleteView):
|
|
||||||
model = Snapshot
|
|
||||||
|
|
||||||
|
|
||||||
class NummiListView(UserMixin, ListView):
|
class NummiListView(UserMixin, ListView):
|
||||||
paginate_by = 96
|
paginate_by = 96
|
||||||
|
|
||||||
|
|
||||||
class TransactionListView(NummiListView):
|
|
||||||
model = Transaction
|
|
||||||
template_name = "main/list/transaction.html"
|
|
||||||
context_object_name = "transactions"
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotListView(NummiListView):
|
|
||||||
model = Snapshot
|
|
||||||
template_name = "main/list/snapshot.html"
|
|
||||||
context_object_name = "snapshots"
|
|
||||||
|
|
||||||
|
|
||||||
class AccountMixin:
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().filter(account=self.kwargs.get("pk"))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(**kwargs) | {
|
|
||||||
"object": Account.objects.get(pk=self.kwargs.get("pk")),
|
|
||||||
"account": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotMixin:
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().filter(snapshot=self.kwargs.get("pk"))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(**kwargs) | {
|
|
||||||
"object": Snapshot.objects.get(pk=self.kwargs.get("pk")),
|
|
||||||
"snapshot": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryMixin:
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().filter(category=self.kwargs.get("pk"))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(**kwargs) | {
|
|
||||||
"object": Category.objects.get(pk=self.kwargs.get("pk")),
|
|
||||||
"category": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AccountTListView(AccountMixin, TransactionListView):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AccountSListView(AccountMixin, SnapshotListView):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SnapshotTListView(SnapshotMixin, TransactionListView):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryTListView(CategoryMixin, TransactionListView):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SearchFormView(LoginRequiredMixin, FormView):
|
|
||||||
template_name = "main/search.html"
|
|
||||||
form_class = SearchForm
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
return redirect("search", search=form.cleaned_data.get("search"))
|
|
||||||
|
|
||||||
|
|
||||||
class SearchView(TransactionListView):
|
|
||||||
def get_queryset(self):
|
|
||||||
self.search = self.kwargs["search"]
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.annotate(
|
|
||||||
rank=SearchRank(
|
|
||||||
SearchVector("name", weight="A")
|
|
||||||
+ SearchVector("description", weight="B")
|
|
||||||
+ SearchVector("trader", weight="B"),
|
|
||||||
SearchQuery(self.search, search_type="websearch"),
|
|
||||||
),
|
|
||||||
similarity=TrigramSimilarity("name", self.search),
|
|
||||||
)
|
|
||||||
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
|
|
||||||
.order_by("-rank", "-date")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(**kwargs) | {"search": self.kwargs["search"]}
|
|
||||||
|
|
||||||
|
|
||||||
class MediaView(LoginRequiredMixin, View):
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
_username = kwargs.get("username")
|
|
||||||
_path = kwargs.get("path")
|
|
||||||
if request.user.get_username() != _username:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
return serve(request, f"user/{_username}/{_path}", settings.MEDIA_ROOT)
|
|
||||||
|
|
||||||
_res = HttpResponse()
|
|
||||||
_res["Content-Type"] = ""
|
|
||||||
_res["X-Accel-Redirect"] = f"/internal/media/user/{_username}/{_path}"
|
|
||||||
return _res
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionMonthView(UserMixin, MonthArchiveView):
|
|
||||||
template_name = "main/month/transaction.html"
|
|
||||||
model = Transaction
|
|
||||||
date_field = "date"
|
|
||||||
context_object_name = "transactions"
|
|
||||||
month_format = "%m"
|
|
||||||
|
|
||||||
account = None
|
|
||||||
category = None
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
if "account" in self.kwargs:
|
|
||||||
self.account = get_object_or_404(
|
|
||||||
Account.objects.filter(user=self.request.user),
|
|
||||||
pk=self.kwargs["account"],
|
|
||||||
)
|
|
||||||
return super().get_queryset().filter(account=self.account)
|
|
||||||
if "category" in self.kwargs:
|
|
||||||
self.category = get_object_or_404(
|
|
||||||
Category.objects.filter(user=self.request.user),
|
|
||||||
pk=self.kwargs["category"],
|
|
||||||
)
|
|
||||||
return super().get_queryset().filter(category=self.category)
|
|
||||||
|
|
||||||
return super().get_queryset()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
if "account" in self.kwargs:
|
|
||||||
return super().get_context_data(**kwargs) | {"account": self.account}
|
|
||||||
if "category" in self.kwargs:
|
|
||||||
return super().get_context_data(**kwargs) | {"category": self.category}
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
0
nummi/media/__init__.py
Normal file
0
nummi/media/__init__.py
Normal file
6
nummi/media/apps.py
Normal file
6
nummi/media/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MediaConfig(AppConfig):
|
||||||
|
name = "media"
|
||||||
|
verbose_name = "Media"
|
7
nummi/media/urls.py
Normal file
7
nummi/media/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("user/<username>/<path:path>", views.MediaView.as_view(), name="media"),
|
||||||
|
]
|
10
nummi/media/utils.py
Normal file
10
nummi/media/utils.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
def get_path(instance, filename):
|
||||||
|
return pathlib.Path(
|
||||||
|
"user",
|
||||||
|
str(instance.user.username),
|
||||||
|
instance._meta.model_name,
|
||||||
|
str(instance.pk),
|
||||||
|
).with_suffix(".pdf")
|
22
nummi/media/views.py
Normal file
22
nummi/media/views.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.views.static import serve
|
||||||
|
|
||||||
|
|
||||||
|
class MediaView(LoginRequiredMixin, View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
_username = kwargs.get("username")
|
||||||
|
_path = kwargs.get("path")
|
||||||
|
if request.user.get_username() != _username:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
return serve(request, f"user/{_username}/{_path}", settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
_res = HttpResponse()
|
||||||
|
_res["Content-Type"] = ""
|
||||||
|
_res["X-Accel-Redirect"] = f"/internal/media/user/{_username}/{_path}"
|
||||||
|
return _res
|
|
@ -44,7 +44,13 @@ CSRF_TRUSTED_ORIGINS = CONFIG.get("trusted_origins", ["http://localhost"])
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"main.apps.MainConfig",
|
"main",
|
||||||
|
"media",
|
||||||
|
"account",
|
||||||
|
"category",
|
||||||
|
"statement",
|
||||||
|
"transaction",
|
||||||
|
"search",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
|
|
|
@ -19,6 +19,7 @@ from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = i18n_patterns(
|
urlpatterns = i18n_patterns(
|
||||||
path("", include("main.urls")),
|
path("", include("main.urls")),
|
||||||
|
path("media/", include("media.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
prefix_default_language=False,
|
prefix_default_language=False,
|
||||||
)
|
)
|
||||||
|
|
0
nummi/search/__init__.py
Normal file
0
nummi/search/__init__.py
Normal file
6
nummi/search/apps.py
Normal file
6
nummi/search/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SearchConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "search"
|
7
nummi/search/forms.py
Normal file
7
nummi/search/forms.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(forms.Form):
|
||||||
|
template_name = "main/form/search.html"
|
||||||
|
search = forms.CharField(label=_("Search"), max_length=128)
|
0
nummi/search/migrations/__init__.py
Normal file
0
nummi/search/migrations/__init__.py
Normal file
8
nummi/search/urls.py
Normal file
8
nummi/search/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("search", views.SearchFormView.as_view(), name="search"),
|
||||||
|
path("search/<search>", views.SearchView.as_view(), name="search"),
|
||||||
|
]
|
44
nummi/search/views.py
Normal file
44
nummi/search/views.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib.postgres.search import (
|
||||||
|
SearchQuery,
|
||||||
|
SearchRank,
|
||||||
|
SearchVector,
|
||||||
|
TrigramSimilarity,
|
||||||
|
)
|
||||||
|
from django.db import models
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
from transaction.views import TransactionListView
|
||||||
|
|
||||||
|
from .forms import SearchForm
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFormView(LoginRequiredMixin, FormView):
|
||||||
|
template_name = "search/search.html"
|
||||||
|
form_class = SearchForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
return redirect("search", search=form.cleaned_data.get("search"))
|
||||||
|
|
||||||
|
|
||||||
|
class SearchView(TransactionListView):
|
||||||
|
def get_queryset(self):
|
||||||
|
self.search = self.kwargs["search"]
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.annotate(
|
||||||
|
rank=SearchRank(
|
||||||
|
SearchVector("name", weight="A")
|
||||||
|
+ SearchVector("description", weight="B")
|
||||||
|
+ SearchVector("trader", weight="B"),
|
||||||
|
SearchQuery(self.search, search_type="websearch"),
|
||||||
|
),
|
||||||
|
similarity=TrigramSimilarity("name", self.search),
|
||||||
|
)
|
||||||
|
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
|
||||||
|
.order_by("-rank", "-date")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {"search": self.kwargs["search"]}
|
0
nummi/statement/__init__.py
Normal file
0
nummi/statement/__init__.py
Normal file
6
nummi/statement/apps.py
Normal file
6
nummi/statement/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class StatementConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "statement"
|
41
nummi/statement/forms.py
Normal file
41
nummi/statement/forms.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from account.models import Account
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from main.forms import NummiFileInput, NummiForm
|
||||||
|
from transaction.models import Transaction
|
||||||
|
|
||||||
|
from .models import Statement
|
||||||
|
|
||||||
|
|
||||||
|
class StatementForm(NummiForm):
|
||||||
|
class Meta:
|
||||||
|
model = Statement
|
||||||
|
fields = ["account", "start_date", "date", "start_value", "value", "file"]
|
||||||
|
widgets = {
|
||||||
|
"file": NummiFileInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
_user = kwargs.get("user")
|
||||||
|
_disable_account = kwargs.pop("disable_account", False)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["account"].queryset = Account.objects.filter(user=_user)
|
||||||
|
self.fields["transactions"] = forms.MultipleChoiceField(
|
||||||
|
choices=(
|
||||||
|
((_transaction.id), _transaction)
|
||||||
|
for _transaction in Transaction.objects.filter(user=_user)
|
||||||
|
),
|
||||||
|
label=_("Add transactions"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
if _disable_account:
|
||||||
|
self.fields["account"].disabled = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
instance = super().save(*args, **kwargs)
|
||||||
|
new_transactions = Transaction.objects.filter(
|
||||||
|
id__in=self.cleaned_data["transactions"]
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.transaction_set.add(*new_transactions, bulk=False)
|
||||||
|
return instance
|
127
nummi/statement/migrations/0001_initial.py
Normal file
127
nummi/statement/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:01
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import media.utils
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("main", "0002_segmentation"),
|
||||||
|
("account", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
state_operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Statement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date",
|
||||||
|
models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name="End date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"start_date",
|
||||||
|
models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name="Start date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"value",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
max_digits=12,
|
||||||
|
verbose_name="End value",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"start_value",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
max_digits=12,
|
||||||
|
verbose_name="Start value",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"diff",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
editable=False,
|
||||||
|
max_digits=12,
|
||||||
|
verbose_name="Difference",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sum",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
editable=False,
|
||||||
|
max_digits=12,
|
||||||
|
verbose_name="Transaction difference",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
max_length=256,
|
||||||
|
upload_to=media.utils.get_path,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(["pdf"])
|
||||||
|
],
|
||||||
|
verbose_name="File",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"account",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="account.account",
|
||||||
|
verbose_name="Account",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="User",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Statement",
|
||||||
|
"verbose_name_plural": "Statements",
|
||||||
|
"ordering": ["-date", "account"],
|
||||||
|
"db_table": "statement_statement",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.SeparateDatabaseAndState(state_operations=state_operations)
|
||||||
|
]
|
16
nummi/statement/migrations/0002_alter_statement_table.py
Normal file
16
nummi/statement/migrations/0002_alter_statement_table.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("statement", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="statement",
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
0
nummi/statement/migrations/__init__.py
Normal file
0
nummi/statement/migrations/__init__.py
Normal file
90
nummi/statement/models.py
Normal file
90
nummi/statement/models.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from account.models import AccountModel
|
||||||
|
from django.core.validators import FileExtensionValidator
|
||||||
|
from django.db import models, transaction
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from media.utils import get_path
|
||||||
|
|
||||||
|
|
||||||
|
class Statement(AccountModel):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
date = models.DateField(default=datetime.date.today, verbose_name=_("End date"))
|
||||||
|
start_date = models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name=_("Start date")
|
||||||
|
)
|
||||||
|
value = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, default=0, verbose_name=_("End value")
|
||||||
|
)
|
||||||
|
start_value = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, default=0, verbose_name=_("Start value")
|
||||||
|
)
|
||||||
|
diff = models.DecimalField(
|
||||||
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
verbose_name=_("Difference"),
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
sum = models.DecimalField(
|
||||||
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
verbose_name=_("Transaction difference"),
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to=get_path,
|
||||||
|
validators=[FileExtensionValidator(["pdf"])],
|
||||||
|
verbose_name=_("File"),
|
||||||
|
max_length=256,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
desc = _("%(date)s statement") % {"date": self.date}
|
||||||
|
if hasattr(self, "account"):
|
||||||
|
return f"{desc} – {self.account}"
|
||||||
|
return desc
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if Statement.objects.filter(id=self.id).exists():
|
||||||
|
_prever = Statement.objects.get(id=self.id)
|
||||||
|
if _prever.file and _prever.file != self.file:
|
||||||
|
Path(_prever.file.path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for trans in self.transaction_set.all():
|
||||||
|
trans.save()
|
||||||
|
|
||||||
|
self.diff = self.value - self.start_value
|
||||||
|
self.sum = (
|
||||||
|
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
|
||||||
|
)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def update_sum(self):
|
||||||
|
self.sum = (
|
||||||
|
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
|
||||||
|
)
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
if self.file:
|
||||||
|
self.file.delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("statement", args=(self.pk,))
|
||||||
|
|
||||||
|
def get_delete_url(self):
|
||||||
|
return reverse("del_statement", args=(self.pk,))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-date", "account"]
|
||||||
|
verbose_name = _("Statement")
|
||||||
|
verbose_name_plural = _("Statements")
|
|
@ -4,7 +4,8 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% translate "Statements" %}
|
{% translate "Statements" %}
|
||||||
{% if object %}– {{ object }}{% endif %}
|
{% if account %}– {{ account }}{% endif %}
|
||||||
|
{% if category %}– {{ category }}{% endif %}
|
||||||
– Nummi
|
– Nummi
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block link %}
|
{% block link %}
|
||||||
|
@ -18,12 +19,21 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>{% translate "Statements" %}</h2>
|
<h2>{% translate "Statements" %}</h2>
|
||||||
{% if object %}<a href="{{ object.get_absolute_url }}">{{ object }}</a>{% endif %}
|
{% if account %}
|
||||||
{% if snapshots %}
|
<p>
|
||||||
|
<a href="{{ account.get_absolute_url }}">{{ account.icon|remix }}{{ account }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if category %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ category.get_absolute_url }}">{{ category.icon|remix }}{{ category }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if statements %}
|
||||||
{% include "main/list/pagination.html" %}
|
{% include "main/list/pagination.html" %}
|
||||||
{% include "main/table/snapshot.html" %}
|
{% include "main/table/statement.html" %}
|
||||||
{% include "main/list/pagination.html" %}
|
{% include "main/list/pagination.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate "No snapshots to show" %}</p>
|
<p>{% translate "No statements to show" %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
25
nummi/statement/urls.py
Normal file
25
nummi/statement/urls.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from django.urls import path
|
||||||
|
from transaction.views import TransactionCreateView
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("list", views.StatementListView.as_view(), name="statements"),
|
||||||
|
path("new", views.StatementCreateView.as_view(), name="new_statement"),
|
||||||
|
path("<statement>", views.StatementUpdateView.as_view(), name="statement"),
|
||||||
|
path(
|
||||||
|
"<statement>/transaction/list",
|
||||||
|
views.StatementTListView.as_view(),
|
||||||
|
name="statement_transactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<statement>/transaction/new",
|
||||||
|
TransactionCreateView.as_view(),
|
||||||
|
name="new_transaction",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<statement>/delete",
|
||||||
|
views.StatementDeleteView.as_view(),
|
||||||
|
name="del_statement",
|
||||||
|
),
|
||||||
|
]
|
97
nummi/statement/views.py
Normal file
97
nummi/statement/views.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from account.models import Account
|
||||||
|
from django.db import models
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from main.views import NummiCreateView, NummiDeleteView, NummiListView, NummiUpdateView
|
||||||
|
from transaction.views import TransactionListView
|
||||||
|
|
||||||
|
from .forms import StatementForm
|
||||||
|
from .models import Statement
|
||||||
|
|
||||||
|
|
||||||
|
class StatementCreateView(NummiCreateView):
|
||||||
|
model = Statement
|
||||||
|
form_class = StatementForm
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
_queryset = Account.objects.filter(user=self.request.user)
|
||||||
|
if "account" in self.kwargs:
|
||||||
|
self.account = get_object_or_404(_queryset, pk=self.kwargs["account"])
|
||||||
|
else:
|
||||||
|
self.account = _queryset.first()
|
||||||
|
return {"account": self.account}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
if "account" in self.kwargs:
|
||||||
|
return super().get_form_kwargs() | {"disable_account": True}
|
||||||
|
return super().get_form_kwargs()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
if "account" in self.kwargs:
|
||||||
|
return super().get_context_data(**kwargs) | {"account": self.account}
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class StatementUpdateView(NummiUpdateView):
|
||||||
|
model = Statement
|
||||||
|
form_class = StatementForm
|
||||||
|
pk_url_kwarg = "statement"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
statement = data["form"].instance
|
||||||
|
|
||||||
|
_transactions = statement.transaction_set.all()
|
||||||
|
if _transactions:
|
||||||
|
_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")
|
||||||
|
)
|
||||||
|
data["categories"] = {
|
||||||
|
"data": _categories,
|
||||||
|
"max": max(
|
||||||
|
_categories.aggregate(
|
||||||
|
max=models.Max("sum_p", default=0),
|
||||||
|
min=models.Min("sum_m", default=0),
|
||||||
|
).values(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return data | {
|
||||||
|
"account": statement.account,
|
||||||
|
"new_transaction_url": reverse_lazy(
|
||||||
|
"new_transaction", kwargs={"statement": statement.pk}
|
||||||
|
),
|
||||||
|
"transactions": _transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StatementDeleteView(NummiDeleteView):
|
||||||
|
model = Statement
|
||||||
|
pk_url_kwarg = "statement"
|
||||||
|
|
||||||
|
|
||||||
|
class StatementListView(NummiListView):
|
||||||
|
model = Statement
|
||||||
|
context_object_name = "statements"
|
||||||
|
|
||||||
|
|
||||||
|
class StatementMixin:
|
||||||
|
def get_queryset(self):
|
||||||
|
self.statement = get_object_or_404(
|
||||||
|
Statement.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs.get("statement"),
|
||||||
|
)
|
||||||
|
return super().get_queryset().filter(statement=self.statement)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {"statement": self.statement}
|
||||||
|
|
||||||
|
|
||||||
|
class StatementTListView(StatementMixin, TransactionListView):
|
||||||
|
pass
|
0
nummi/transaction/__init__.py
Normal file
0
nummi/transaction/__init__.py
Normal file
6
nummi/transaction/apps.py
Normal file
6
nummi/transaction/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "transaction"
|
44
nummi/transaction/forms.py
Normal file
44
nummi/transaction/forms.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from category.models import Category
|
||||||
|
from main.forms import NummiFileInput, NummiForm
|
||||||
|
from statement.models import Statement
|
||||||
|
|
||||||
|
from .models import Invoice, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionForm(NummiForm):
|
||||||
|
class Meta:
|
||||||
|
model = Transaction
|
||||||
|
fields = [
|
||||||
|
"statement",
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"date",
|
||||||
|
"real_date",
|
||||||
|
"category",
|
||||||
|
"trader",
|
||||||
|
"payment",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
_user = kwargs.get("user")
|
||||||
|
_disable_statement = kwargs.pop("disable_statement", False)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["category"].queryset = Category.objects.filter(user=_user)
|
||||||
|
self.fields["statement"].queryset = Statement.objects.filter(user=_user)
|
||||||
|
if _disable_statement:
|
||||||
|
self.fields["statement"].disabled = True
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceForm(NummiForm):
|
||||||
|
prefix = "invoice"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Invoice
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"file",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"file": NummiFileInput,
|
||||||
|
}
|
175
nummi/transaction/migrations/0001_initial.py
Normal file
175
nummi/transaction/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:01
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import media.utils
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("main", "0002_segmentation"),
|
||||||
|
("category", "0001_initial"),
|
||||||
|
("account", "0001_initial"),
|
||||||
|
("statement", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
state_operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Transaction",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default="Transaction", max_length=256, verbose_name="Name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(blank=True, null=True, verbose_name="Description"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"value",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2, default=0, max_digits=12, verbose_name="Value"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date",
|
||||||
|
models.DateField(default=datetime.date.today, verbose_name="Date"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"real_date",
|
||||||
|
models.DateField(blank=True, null=True, verbose_name="Real date"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"trader",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=128, null=True, verbose_name="Trader"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"payment",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=128, null=True, verbose_name="Payment"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"account",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="account.account",
|
||||||
|
verbose_name="Account",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="category.category",
|
||||||
|
verbose_name="Category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"statement",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="statement.statement",
|
||||||
|
verbose_name="Statement",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="User",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Transaction",
|
||||||
|
"verbose_name_plural": "Transactions",
|
||||||
|
"ordering": ["-date", "statement"],
|
||||||
|
"db_table": "transaction_transaction",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Invoice",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default="Invoice", max_length=256, verbose_name="Name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
max_length=128,
|
||||||
|
upload_to=media.utils.get_path,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(["pdf"])
|
||||||
|
],
|
||||||
|
verbose_name="File",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"transaction",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="transaction.transaction",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="User",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Invoice",
|
||||||
|
"verbose_name_plural": "Invoices",
|
||||||
|
"ordering": ["transaction", "name"],
|
||||||
|
"db_table": "transaction_invoice",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.SeparateDatabaseAndState(state_operations=state_operations)
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-04-22 09:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("transaction", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="invoice",
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="transaction",
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
0
nummi/transaction/migrations/__init__.py
Normal file
0
nummi/transaction/migrations/__init__.py
Normal file
124
nummi/transaction/models.py
Normal file
124
nummi/transaction/models.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from account.models import Account
|
||||||
|
from category.models import Category
|
||||||
|
from django.core.validators import FileExtensionValidator
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from main.models import UserModel
|
||||||
|
from media.utils import get_path
|
||||||
|
from statement.models import Statement
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(UserModel):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=256, default=_("Transaction"), verbose_name=_("Name")
|
||||||
|
)
|
||||||
|
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
||||||
|
value = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, default=0, verbose_name=_("Value")
|
||||||
|
)
|
||||||
|
date = models.DateField(default=datetime.date.today, verbose_name=_("Date"))
|
||||||
|
real_date = models.DateField(blank=True, null=True, verbose_name=_("Real date"))
|
||||||
|
trader = models.CharField(
|
||||||
|
max_length=128, blank=True, null=True, verbose_name=_("Trader")
|
||||||
|
)
|
||||||
|
payment = models.CharField(
|
||||||
|
max_length=128, blank=True, null=True, verbose_name=_("Payment")
|
||||||
|
)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
Category,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("Category"),
|
||||||
|
)
|
||||||
|
statement = models.ForeignKey(
|
||||||
|
Statement,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Statement"),
|
||||||
|
)
|
||||||
|
account = models.ForeignKey(
|
||||||
|
Account,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Account"),
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if Transaction.objects.filter(pk=self.pk):
|
||||||
|
prev_self = Transaction.objects.get(pk=self.pk)
|
||||||
|
else:
|
||||||
|
prev_self = None
|
||||||
|
self.account = self.statement.account
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if prev_self is not None:
|
||||||
|
prev_self.statement.update_sum()
|
||||||
|
self.statement.update_sum()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("transaction", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
def get_delete_url(self):
|
||||||
|
return reverse("del_transaction", args=(self.pk,))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invoices(self):
|
||||||
|
return Invoice.objects.filter(transaction=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_invoice(self):
|
||||||
|
return self.invoices.count() > 0
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-date", "statement"]
|
||||||
|
verbose_name = _("Transaction")
|
||||||
|
verbose_name_plural = _("Transactions")
|
||||||
|
|
||||||
|
|
||||||
|
class Invoice(UserModel):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=256, default=_("Invoice"), verbose_name=_("Name")
|
||||||
|
)
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to=get_path,
|
||||||
|
validators=[FileExtensionValidator(["pdf"])],
|
||||||
|
verbose_name=_("File"),
|
||||||
|
max_length=128,
|
||||||
|
)
|
||||||
|
transaction = models.ForeignKey(
|
||||||
|
Transaction, on_delete=models.CASCADE, editable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if Invoice.objects.filter(id=self.id).exists():
|
||||||
|
_prever = Invoice.objects.get(id=self.id)
|
||||||
|
if _prever.file and _prever.file != self.file:
|
||||||
|
Path(_prever.file.path).unlink(missing_ok=True)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.name)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
self.file.delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("invoice", args=(self.transaction.pk, self.pk))
|
||||||
|
|
||||||
|
def get_delete_url(self):
|
||||||
|
return reverse("del_invoice", args=(self.transaction.pk, self.pk))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Invoice")
|
||||||
|
verbose_name_plural = _("Invoices")
|
||||||
|
ordering = ["transaction", "name"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "transaction/transaction_list.html" %}
|
||||||
|
{% block h2 %}{{ month|date:"F Y"|capfirst }}{% endblock %}
|
|
@ -4,7 +4,8 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% translate "Transactions" %}
|
{% translate "Transactions" %}
|
||||||
{% if object %}– {{ object }}{% endif %}
|
{% if account %}– {{ account }}{% endif %}
|
||||||
|
{% if category %}– {{ category }}{% endif %}
|
||||||
{% if search %}
|
{% if search %}
|
||||||
– {% translate "Search" %}
|
– {% translate "Search" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -20,10 +21,25 @@
|
||||||
type="text/css" />
|
type="text/css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>{% translate "Transactions" %}</h2>
|
<h2>
|
||||||
{% if object %}<a href="{{ object.get_absolute_url }}">{{ object }}</a>{% endif %}
|
{% block h2 %}
|
||||||
|
{% translate "Transactions" %}
|
||||||
|
{% endblock %}
|
||||||
|
</h2>
|
||||||
|
{% if account %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ account.get_absolute_url }}">{{ account.icon|remix }}{{ account }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if category %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ category.get_absolute_url }}">{{ category.icon|remix }}{{ category }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
{% if search %}
|
{% if search %}
|
||||||
|
<p>
|
||||||
<a href="{% url "search" %}">{% translate "Search" %}</a>
|
<a href="{% url "search" %}">{% translate "Search" %}</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if transactions %}
|
{% if transactions %}
|
||||||
{% include "main/list/pagination.html" %}
|
{% include "main/list/pagination.html" %}
|
34
nummi/transaction/urls.py
Normal file
34
nummi/transaction/urls.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("list", views.TransactionListView.as_view(), name="transactions"),
|
||||||
|
path(
|
||||||
|
"history/<int:year>/<int:month>",
|
||||||
|
views.TransactionMonthView.as_view(),
|
||||||
|
name="transaction_month",
|
||||||
|
),
|
||||||
|
path("new", views.TransactionCreateView.as_view(), name="new_transaction"),
|
||||||
|
path("<transaction>", views.TransactionUpdateView.as_view(), name="transaction"),
|
||||||
|
path(
|
||||||
|
"<transaction>/delete",
|
||||||
|
views.TransactionDeleteView.as_view(),
|
||||||
|
name="del_transaction",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<transaction>/invoice/new",
|
||||||
|
views.InvoiceCreateView.as_view(),
|
||||||
|
name="new_invoice",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<transaction>/invoice/<invoice>",
|
||||||
|
views.InvoiceUpdateView.as_view(),
|
||||||
|
name="invoice",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<transaction>/invoice/<invoice>/delete",
|
||||||
|
views.InvoiceDeleteView.as_view(),
|
||||||
|
name="del_invoice",
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,19 +1,8 @@
|
||||||
import pathlib
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Func, Max, Min, Q, Sum, Value
|
from django.db.models import Func, Max, Min, Q, Sum, Value
|
||||||
from django.db.models.functions import Now, TruncMonth
|
from django.db.models.functions import Now, TruncMonth
|
||||||
|
|
||||||
|
|
||||||
def get_path(instance, filename):
|
|
||||||
return pathlib.Path(
|
|
||||||
"user",
|
|
||||||
str(instance.user.username),
|
|
||||||
instance._meta.model_name,
|
|
||||||
str(instance.pk),
|
|
||||||
).with_suffix(".pdf")
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateMonth(Func):
|
class GenerateMonth(Func):
|
||||||
function = "generate_series"
|
function = "generate_series"
|
||||||
template = "%(function)s(%(expressions)s, '1 month')::date"
|
template = "%(function)s(%(expressions)s, '1 month')::date"
|
142
nummi/transaction/views.py
Normal file
142
nummi/transaction/views.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
from account.models import Account
|
||||||
|
from category.models import Category
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic.dates import MonthArchiveView
|
||||||
|
from main.views import (
|
||||||
|
NummiCreateView,
|
||||||
|
NummiDeleteView,
|
||||||
|
NummiListView,
|
||||||
|
NummiUpdateView,
|
||||||
|
UserMixin,
|
||||||
|
)
|
||||||
|
from statement.models import Statement
|
||||||
|
|
||||||
|
from .forms import InvoiceForm, TransactionForm
|
||||||
|
from .models import Invoice, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionCreateView(NummiCreateView):
|
||||||
|
model = Transaction
|
||||||
|
form_class = TransactionForm
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
_queryset = Statement.objects.filter(user=self.request.user)
|
||||||
|
if "statement" in self.kwargs:
|
||||||
|
self.statement = get_object_or_404(_queryset, pk=self.kwargs["statement"])
|
||||||
|
else:
|
||||||
|
self.statement = _queryset.first()
|
||||||
|
return {"statement": self.statement}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
if "statement" in self.kwargs:
|
||||||
|
return super().get_form_kwargs() | {"disable_statement": True}
|
||||||
|
return super().get_form_kwargs()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
if "statement" in self.kwargs:
|
||||||
|
return super().get_context_data(**kwargs) | {"statement": self.statement}
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceCreateView(NummiCreateView):
|
||||||
|
model = Invoice
|
||||||
|
form_class = InvoiceForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.transaction = get_object_or_404(
|
||||||
|
Transaction.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs["transaction"],
|
||||||
|
)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionUpdateView(NummiUpdateView):
|
||||||
|
model = Transaction
|
||||||
|
form_class = TransactionForm
|
||||||
|
pk_url_kwarg = "transaction"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceUpdateView(NummiUpdateView):
|
||||||
|
model = Invoice
|
||||||
|
form_class = InvoiceForm
|
||||||
|
pk_url_kwarg = "invoice"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
transaction=get_object_or_404(
|
||||||
|
Transaction, pk=self.kwargs["transaction"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionDeleteView(NummiDeleteView):
|
||||||
|
model = Transaction
|
||||||
|
pk_url_kwarg = "transaction"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceDeleteView(NummiDeleteView):
|
||||||
|
model = Invoice
|
||||||
|
pk_url_kwarg = "invoice"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
transaction=get_object_or_404(
|
||||||
|
Transaction, pk=self.kwargs["transaction"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionListView(NummiListView):
|
||||||
|
model = Transaction
|
||||||
|
context_object_name = "transactions"
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionMonthView(UserMixin, MonthArchiveView):
|
||||||
|
model = Transaction
|
||||||
|
date_field = "date"
|
||||||
|
context_object_name = "transactions"
|
||||||
|
month_format = "%m"
|
||||||
|
|
||||||
|
account = None
|
||||||
|
category = None
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if "account" in self.kwargs:
|
||||||
|
self.account = get_object_or_404(
|
||||||
|
Account.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs["account"],
|
||||||
|
)
|
||||||
|
return super().get_queryset().filter(account=self.account)
|
||||||
|
if "category" in self.kwargs:
|
||||||
|
self.category = get_object_or_404(
|
||||||
|
Category.objects.filter(user=self.request.user),
|
||||||
|
pk=self.kwargs["category"],
|
||||||
|
)
|
||||||
|
return super().get_queryset().filter(category=self.category)
|
||||||
|
|
||||||
|
return super().get_queryset()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
if "account" in self.kwargs:
|
||||||
|
return super().get_context_data(**kwargs) | {"account": self.account}
|
||||||
|
if "category" in self.kwargs:
|
||||||
|
return super().get_context_data(**kwargs) | {"category": self.category}
|
||||||
|
return super().get_context_data(**kwargs)
|
Loading…
Reference in a new issue