Compare commits
No commits in common. "b05c3e67607b0b66ae3451710f01ed53fc8f6a3f" and "ad18226974d99116fcc2fc8d4c20670e9a4d7e31" have entirely different histories.
b05c3e6760
...
ad18226974
53 changed files with 888 additions and 1500 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,4 +4,4 @@ __pycache__
|
|||
/nummi-git/
|
||||
/pkg/
|
||||
/src/
|
||||
/media
|
||||
/nummi/media
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "account"
|
|
@ -1,13 +0,0 @@
|
|||
from main.forms import NummiForm
|
||||
|
||||
from .models import Account
|
||||
|
||||
|
||||
class AccountForm(NummiForm):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
"name",
|
||||
"icon",
|
||||
"default",
|
||||
]
|
|
@ -1,65 +0,0 @@
|
|||
# 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)
|
||||
]
|
|
@ -1,49 +0,0 @@
|
|||
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
|
|
@ -1,33 +0,0 @@
|
|||
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",
|
||||
),
|
||||
]
|
|
@ -1,68 +0,0 @@
|
|||
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
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CategoryConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "category"
|
|
@ -1,13 +0,0 @@
|
|||
from main.forms import NummiForm
|
||||
|
||||
from .models import Category
|
||||
|
||||
|
||||
class CategoryForm(NummiForm):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = [
|
||||
"name",
|
||||
"icon",
|
||||
"budget",
|
||||
]
|
|
@ -1,64 +0,0 @@
|
|||
# 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)
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
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")
|
|
@ -1,21 +0,0 @@
|
|||
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",
|
||||
),
|
||||
]
|
|
@ -1,50 +0,0 @@
|
|||
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
|
|
@ -1,4 +1,7 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Account, Category, Invoice, Snapshot, Transaction
|
||||
|
||||
|
||||
class NummiFileInput(forms.ClearableFileInput):
|
||||
|
@ -10,3 +13,101 @@ class NummiForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, user, **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.core.validators
|
||||
import django.db.models.deletion
|
||||
import media.utils
|
||||
import main.utils
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -136,7 +136,7 @@ class Migration(migrations.Migration):
|
|||
blank=True,
|
||||
default="",
|
||||
max_length=256,
|
||||
upload_to=media.utils.get_path,
|
||||
upload_to=main.utils.get_path,
|
||||
validators=[
|
||||
django.core.validators.FileExtensionValidator(["pdf"])
|
||||
],
|
||||
|
@ -315,7 +315,7 @@ class Migration(migrations.Migration):
|
|||
"file",
|
||||
models.FileField(
|
||||
max_length=128,
|
||||
upload_to=media.utils.get_path,
|
||||
upload_to=main.utils.get_path,
|
||||
validators=[
|
||||
django.core.validators.FileExtensionValidator(["pdf"])
|
||||
],
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
# 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,
|
||||
),
|
||||
]
|
|
@ -1,7 +1,15 @@
|
|||
import datetime
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
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 .utils import get_path
|
||||
|
||||
|
||||
class UserModel(models.Model):
|
||||
user = models.ForeignKey(
|
||||
|
@ -13,3 +21,280 @@ class UserModel(models.Model):
|
|||
|
||||
class Meta:
|
||||
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"]
|
||||
|
|
|
@ -4,6 +4,95 @@ from . import views
|
|||
|
||||
urlpatterns = [
|
||||
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("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",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
import pathlib
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Func, Max, Min, Q, Sum, Value
|
||||
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):
|
||||
function = "generate_series"
|
||||
template = "%(function)s(%(expressions)s, '1 month')::date"
|
|
@ -1,18 +1,39 @@
|
|||
from account.models import Account
|
||||
from category.models import Category
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import views as auth_views
|
||||
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.views import View
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
FormView,
|
||||
ListView,
|
||||
TemplateView,
|
||||
UpdateView,
|
||||
)
|
||||
from statement.models import Statement
|
||||
from transaction.models import Transaction
|
||||
from transaction.utils import history
|
||||
from django.views.generic.dates import MonthArchiveView
|
||||
from django.views.static import serve
|
||||
|
||||
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):
|
||||
|
@ -21,19 +42,19 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
|||
def get_context_data(self, **kwargs):
|
||||
_max = 8
|
||||
_transactions = Transaction.objects.filter(user=self.request.user)
|
||||
_statements = Statement.objects.filter(user=self.request.user)
|
||||
_snapshots = Snapshot.objects.filter(user=self.request.user)
|
||||
|
||||
res = {
|
||||
"accounts": Account.objects.filter(user=self.request.user),
|
||||
"transactions": _transactions[:_max],
|
||||
"categories": Category.objects.filter(user=self.request.user),
|
||||
"statements": _statements[:_max],
|
||||
"snapshots": _snapshots[:_max],
|
||||
"history": history(_transactions.exclude(category__budget=False)),
|
||||
}
|
||||
if _transactions.count() > _max:
|
||||
res["transactions_url"] = reverse_lazy("transactions")
|
||||
if _statements.count() > _max:
|
||||
res["statements_url"] = reverse_lazy("statements")
|
||||
if _snapshots.count() > _max:
|
||||
res["snapshots_url"] = reverse_lazy("snapshots")
|
||||
|
||||
return super().get_context_data(**kwargs) | res
|
||||
|
||||
|
@ -76,5 +97,371 @@ class LogoutView(auth_views.LogoutView):
|
|||
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):
|
||||
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)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MediaConfig(AppConfig):
|
||||
name = "media"
|
||||
verbose_name = "Media"
|
|
@ -1,7 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("user/<username>/<path:path>", views.MediaView.as_view(), name="media"),
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
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")
|
|
@ -1,22 +0,0 @@
|
|||
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,12 +44,7 @@ CSRF_TRUSTED_ORIGINS = CONFIG.get("trusted_origins", ["http://localhost"])
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"main",
|
||||
"media",
|
||||
"account",
|
||||
"category",
|
||||
"statement",
|
||||
"transaction",
|
||||
"main.apps.MainConfig",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
|
|
|
@ -19,7 +19,6 @@ from django.urls import include, path
|
|||
|
||||
urlpatterns = i18n_patterns(
|
||||
path("", include("main.urls")),
|
||||
path("media/", include("media.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
prefix_default_language=False,
|
||||
)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SearchConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "search"
|
|
@ -1,7 +0,0 @@
|
|||
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)
|
|
@ -1,8 +0,0 @@
|
|||
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"),
|
||||
]
|
|
@ -1,44 +0,0 @@
|
|||
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"]}
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StatementConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "statement"
|
|
@ -1,41 +0,0 @@
|
|||
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
|
|
@ -1,127 +0,0 @@
|
|||
# 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)
|
||||
]
|
|
@ -1,90 +0,0 @@
|
|||
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")
|
|
@ -1,24 +0,0 @@
|
|||
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",
|
||||
),
|
||||
]
|
|
@ -1,97 +0,0 @@
|
|||
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
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TransactionConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "transaction"
|
|
@ -1,44 +0,0 @@
|
|||
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,
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
# 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)
|
||||
]
|
|
@ -1,128 +0,0 @@
|
|||
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"]
|
|
@ -1,34 +0,0 @@
|
|||
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",
|
||||
),
|
||||
]
|
|
@ -1,144 +0,0 @@
|
|||
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)
|
Loading…
Reference in a new issue