import pathlib import uuid from datetime import date from django.conf import settings from django.core.validators import FileExtensionValidator from django.db import models from django.forms import ModelForm from django.urls import reverse from django.utils.translation import gettext as _ class UserModel(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_("User"), editable=False, ) class Meta: abstract = True class CustomModel(UserModel): @property def adding(self): return self._state.adding class Meta: abstract = True class Account(CustomModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name")) icon = models.CharField(max_length=64, default="folder", verbose_name=_("Icon")) def __str__(self): return 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 = ["name"] verbose_name = _("Account") verbose_name_plural = _("Accounts") class AccountModel(CustomModel): account = models.ForeignKey( Account, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("Account"), ) class Meta: abstract = True class Category(CustomModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField( max_length=64, default=_("Category"), verbose_name=_("Name") ) icon = models.CharField(max_length=64, default="folder", verbose_name=_("Icon")) budget = models.BooleanField(default=True, verbose_name=_("Budget")) def __str__(self): return 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 Transaction(AccountModel): 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=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"), ) def __str__(self): return f"{self.date} – {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"] verbose_name = _("Transaction") verbose_name_plural = _("Transactions") def invoice_path(instance, filename): return pathlib.Path("invoices", str(instance.id)).with_suffix(".pdf") class Invoice(CustomModel): 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=invoice_path, validators=[FileExtensionValidator(["pdf"])], verbose_name=_("File"), max_length=128, ) transaction = models.ForeignKey( Transaction, on_delete=models.CASCADE, editable=False ) def __str__(self): if hasattr(self, "transaction"): return f"{self.name} – {self.transaction.name}" return 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") def snapshot_path(instance, filename): return pathlib.Path("snapshots", str(instance.id)).with_suffix(".pdf") class Snapshot(AccountModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) date = models.DateField(default=date.today, verbose_name=_("Date")) value = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name=_("Value") ) previous = models.OneToOneField( "self", on_delete=models.SET_NULL, blank=True, null=True, editable=False ) diff = models.DecimalField( max_digits=12, decimal_places=2, editable=False, blank=True, null=True ) file = models.FileField( upload_to=snapshot_path, validators=[FileExtensionValidator(["pdf"])], verbose_name=_("File"), max_length=256, blank=True, default="", ) def __str__(self): return _("%(date)s snapshot") % {"date": self.date} def save(self, *args, only_super=False, **kwargs): if not only_super: 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) _prev = ( self.__class__.objects.order_by("-date") .exclude(id=self.id) .filter(date__lt=self.date) .first() ) try: _next = self.__class__.objects.exclude(id=self.id).get(previous=_prev) except self.__class__.DoesNotExist: pass else: try: _prevnext = self.__class__.objects.exclude(id=self.id).get( previous=self ) except self.__class__.DoesNotExist: pass else: _prevnext.previous = ( self.__class__.objects.order_by("-date") .exclude(id=self.id) .filter(date__lt=_prevnext.date) .first() ) _prevnext.save(only_super=True) _next.previous = None super().save(*args, **kwargs) _next.previous = self _next.save(only_super=True) self.previous = _prev if self.previous is None: self.diff = None else: self.diff = self.value - self.previous.value super().save(*args, **kwargs) try: _next = self.__class__.objects.get(previous=self) except self.__class__.DoesNotExist: pass else: _next.save(only_super=True) def delete(self, *args, only_super=False, **kwargs): self.file.delete() if not only_super: try: _next = self.__class__.objects.get(previous=self) except self.__class__.DoesNotExist: super().delete(*args, **kwargs) else: _next.previous = self.previous super().delete(*args, **kwargs) _next.save(only_super=True) else: 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}) @property def sum(self): if self.previous is None: return 0 trans = self.transactions.aggregate(sum=models.Sum("value")) return trans["sum"] or 0 @property def transactions(self): if self.previous is None: return Transaction.objects.none() return Transaction.objects.filter( date__lte=self.date, date__gt=self.previous.date ) @property def pos(self): return ( self.transactions.filter(value__gt=0).aggregate(sum=models.Sum("value"))[ "sum" ] or 0 ) @property def neg(self): return ( self.transactions.filter(value__lt=0).aggregate(sum=models.Sum("value"))[ "sum" ] or 0 ) class Meta: ordering = ["-date"] verbose_name = _("Snapshot") verbose_name_plural = _("Snapshots") class NummiForm(ModelForm): template_name = "main/form/base.html" def __init__(self, *args, **kwargs): kwargs.pop("user", None) super().__init__(*args, **kwargs) class AccountForm(NummiForm): class Meta: model = Account fields = "__all__" class CategoryForm(NummiForm): class Meta: model = Category fields = "__all__" class TransactionForm(NummiForm): class Meta: model = Transaction fields = "__all__" def __init__(self, *args, **kwargs): _user = kwargs.pop("user") super().__init__(*args, **kwargs) self.fields["category"].queryset = Category.objects.filter(user=_user) self.fields["account"].queryset = Account.objects.filter(user=_user) class InvoiceForm(NummiForm): prefix = "invoice" class Meta: model = Invoice fields = "__all__" class SnapshotForm(NummiForm): class Meta: model = Snapshot fields = "__all__" def __init__(self, *args, **kwargs): _user = kwargs.pop("user") super().__init__(*args, **kwargs) self.fields["account"].queryset = Account.objects.filter(user=_user)