import datetime import pathlib import uuid from django.conf import settings from django.core.validators import FileExtensionValidator from django.db import models, transaction 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="building-columns", 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.CASCADE, 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") 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=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=snapshot_path, validators=[FileExtensionValidator(["pdf"])], verbose_name=_("File"), max_length=256, blank=True, default="", ) def __str__(self): return _("%(date)s statement") % {"date": self.date} 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 super().save(*args, **kwargs) def update_sum(self): self.sum = self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) super().save() def delete(self, *args, only_super=False, **kwargs): if self.file: self.file.delete() 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"] verbose_name = _("Statement") verbose_name_plural = _("Statements") class Transaction(CustomModel): 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): self.account = self.snapshot.account super().save(*args, **kwargs) self.snapshot.update_sum() 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")