Split backend in applications

This commit is contained in:
Edgar P. Burkhart 2023-04-22 11:16:42 +02:00
parent a0d0b5d594
commit b05c3e6760
Signed by: edpibu
GPG Key ID: 9833D3C5A25BD227
47 changed files with 1463 additions and 866 deletions

View File

6
nummi/account/apps.py Normal file
View 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
View 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",
]

View 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)
]

View File

49
nummi/account/models.py Normal file
View 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", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_account", kwargs={"pk": 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

33
nummi/account/urls.py Normal file
View File

@ -0,0 +1,33 @@
from django.urls import path
from . import views
urlpatterns = [
path("account", views.AccountCreateView.as_view(), name="new_account"),
path("account/<pk>", views.AccountUpdateView.as_view(), name="account"),
path(
"account/<pk>/transactions",
views.AccountTListView.as_view(),
name="account_transactions",
),
path(
"account/<pk>/statements",
views.AccountSListView.as_view(),
name="account_statements",
),
path(
"account/<account>/statement",
views.StatementCreateView.as_view(),
name="new_statement",
),
path(
"account/<pk>/delete",
views.AccountDeleteView.as_view(),
name="del_account",
),
path(
"account/<account>/history/<int:year>/<int:month>",
views.TransactionMonthView.as_view(),
name="transaction_month",
),
]

68
nummi/account/views.py Normal file
View File

@ -0,0 +1,68 @@
from django.urls import reverse_lazy
from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
from snapshot.views import SnapshotListView
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
template_name = "main/form/account.html"
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 AccountDeleteView(NummiDeleteView):
model = Account
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 AccountTListView(AccountMixin, TransactionListView):
pass
class AccountSListView(AccountMixin, SnapshotListView):
pass

View File

6
nummi/category/apps.py Normal file
View 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
View 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",
]

View 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)
]

View File

33
nummi/category/models.py Normal file
View 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", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_category", kwargs={"pk": self.pk})
class Meta:
ordering = ["name"]
verbose_name = _("Category")
verbose_name_plural = _("Categories")

21
nummi/category/urls.py Normal file
View File

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

50
nummi/category/views.py Normal file
View File

@ -0,0 +1,50 @@
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
template_name = "main/form/category.html"
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 CategoryDeleteView(NummiDeleteView):
model = Category
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 CategoryTListView(CategoryMixin, TransactionListView):
pass

View File

@ -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)

View File

@ -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"])
], ],

View File

@ -0,0 +1,32 @@
# 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.SeparateDatabaseAndState(
database_operations=database_operations,
state_operations=state_operations,
),
]

View File

@ -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"]

View File

@ -6,92 +6,4 @@ urlpatterns = [
path("", views.IndexView.as_view(), name="index"), path("", views.IndexView.as_view(), name="index"),
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("snapshots", views.SnapshotListView.as_view(), name="snapshots"),
path("account", views.AccountCreateView.as_view(), name="new_account"),
path("transaction", views.TransactionCreateView.as_view(), name="new_transaction"),
path(
"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",
),
] ]

View File

@ -1,34 +1,18 @@
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.db import models
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
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 transaction.models import Transaction
from .forms import ( from transaction.utils import history
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):
@ -37,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
@ -92,355 +76,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 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)

10
nummi/media/utils.py Normal file
View 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")

View File

@ -44,8 +44,12 @@ CSRF_TRUSTED_ORIGINS = CONFIG.get("trusted_origins", ["http://localhost"])
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"main.apps.MainConfig", "main",
"media.apps.MediaConfig", "media",
"account",
"category",
"statement",
"transaction",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",

0
nummi/search/__init__.py Normal file
View File

6
nummi/search/apps.py Normal file
View 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
View 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)

View File

8
nummi/search/urls.py Normal file
View 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
View 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 = "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"]}

View File

6
nummi/statement/apps.py Normal file
View 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
View 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

View 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)
]

View File

90
nummi/statement/models.py Normal file
View 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", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_statement", kwargs={"pk": self.pk})
class Meta:
ordering = ["-date", "account"]
verbose_name = _("Statement")
verbose_name_plural = _("Statements")

24
nummi/statement/urls.py Normal file
View File

@ -0,0 +1,24 @@
from django.urls import path
from . import views
urlpatterns = [
path("statements", views.SnapshotListView.as_view(), name="statements"),
path("statement", views.StatementCreateView.as_view(), name="new_statement"),
path("statement/<pk>", views.StatementUpdateView.as_view(), name="statement"),
path(
"statement/<pk>/transactions",
views.StatementTListView.as_view(),
name="statement_transactions",
),
path(
"statement/<statement>/transaction",
views.TransactionCreateView.as_view(),
name="new_transaction",
),
path(
"statement/<pk>/delete",
views.StatementDeleteView.as_view(),
name="del_statement",
),
]

97
nummi/statement/views.py Normal file
View 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
template_name = "main/form/statement.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 StatementUpdateView(NummiUpdateView):
model = Statement
form_class = StatementForm
template_name = "main/form/statement.html"
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
class StatementListView(NummiListView):
model = Statement
template_name = "main/list/statement.html"
context_object_name = "statements"
class StatementMixin:
def get_queryset(self):
return super().get_queryset().filter(statement=self.kwargs.get("pk"))
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"object": Statement.objects.get(pk=self.kwargs.get("pk")),
"statement": True,
}
class StatementTListView(StatementMixin, TransactionListView):
pass

View File

View File

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

View 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,
}

View 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)
]

View File

128
nummi/transaction/models.py Normal file
View File

@ -0,0 +1,128 @@
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", 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", "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", 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"]

34
nummi/transaction/urls.py Normal file
View File

@ -0,0 +1,34 @@
from django.urls import path
from . import views
urlpatterns = [
path("transactions", views.TransactionListView.as_view(), name="transactions"),
path("transaction", views.TransactionCreateView.as_view(), name="new_transaction"),
path(
"transaction/<transaction_pk>/invoice",
views.InvoiceCreateView.as_view(),
name="new_invoice",
),
path("transaction/<pk>", views.TransactionUpdateView.as_view(), name="transaction"),
path(
"transaction/<transaction_pk>/invoice/<pk>",
views.InvoiceUpdateView.as_view(),
name="invoice",
),
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(
"history/<int:year>/<int:month>",
views.TransactionMonthView.as_view(),
name="transaction_month",
),
]

View File

@ -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"

144
nummi/transaction/views.py Normal file
View File

@ -0,0 +1,144 @@
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
template_name = "main/form/transaction.html"
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
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 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 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 TransactionListView(NummiListView):
model = Transaction
template_name = "main/list/transaction.html"
context_object_name = "transactions"
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)