"""Models for the accounting app.""" from hashlib import md5 from typing import Self from django.conf import settings from django.db import models from django.db.models.aggregates import Sum from django.utils.translation import gettext as _ from django.utils.translation import pgettext_lazy from djmoney.models.fields import MoneyField from djmoney.money import Money class CreatedModifiedAbstract(models.Model): """Abstract model to track creation and modification of objects.""" modified = models.DateTimeField(auto_now=True, verbose_name=_("modified")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("created")) class Meta: abstract = True class Account(CreatedModifiedAbstract): """An account for a user. This is the model where we can give access to several users, such that they can decide which account to use to pay for something. """ owner = models.ForeignKey("auth.User", on_delete=models.PROTECT) def __str__(self) -> str: return f"Account of {self.owner.get_full_name()}" @property def balance(self) -> Money: """Return the balance of the account.""" return self.transactions.all().aggregate(Sum("amount")).get("amount", 0) class Transaction(CreatedModifiedAbstract): """A transaction. Tracks in and outgoing events of an account. When an order is received, an amount is subtracted, when a payment is received, an amount is added. """ account = models.ForeignKey( Account, on_delete=models.PROTECT, related_name="transactions", ) amount = MoneyField( verbose_name=_("amount"), max_digits=16, decimal_places=2, help_text=_("This will include VAT"), ) description = models.CharField(max_length=1024, verbose_name=_("description")) def __str__(self) -> str: return f"Transaction of {self.amount} for {self.account}" class Order(CreatedModifiedAbstract): """An order. Scoped out: Contents of invoices will have to be tracked either here or in a separate Invoice model. This is undecided because we are not generating invoices at the moment. """ user = models.ForeignKey("auth.User", on_delete=models.PROTECT) account = models.ForeignKey(Account, on_delete=models.PROTECT) description = models.CharField(max_length=1024, verbose_name=_("description")) price = MoneyField( verbose_name=_("price (excl. VAT)"), max_digits=16, decimal_places=2, ) vat = MoneyField(verbose_name=_("VAT"), max_digits=16, decimal_places=2) is_paid = models.BooleanField(default=False, verbose_name=_("is paid")) class Meta: verbose_name = pgettext_lazy("accounting term", "Order") verbose_name_plural = pgettext_lazy("accounting term", "Orders") def __str__(self) -> str: return f"Order ID {self.display_id}" @property def total(self) -> Money: """Return the total price of the order.""" return self.price + self.vat @property def display_id(self) -> str: """Return an id for the order.""" return str(self.id).zfill(6) @property def payment_token(self) -> str: """Return a token for the payment.""" pk = str(self.pk).encode("utf-8") x = md5() # noqa: S324 x.update(pk) extra_hash = (settings.SECRET_KEY + "blah").encode("utf-8") x.update(extra_hash) return x.hexdigest() class Payment(CreatedModifiedAbstract): """A payment is a transaction that is made to pay for an order.""" amount = MoneyField(max_digits=16, decimal_places=2) order = models.ForeignKey(Order, on_delete=models.PROTECT) description = models.CharField(max_length=1024, verbose_name=_("description")) stripe_charge_id = models.CharField(max_length=255, blank=True) class Meta: verbose_name = _("payment") verbose_name_plural = _("payments") def __str__(self) -> str: return f"Payment ID {self.display_id}" @property def display_id(self) -> str: """Return an id for the payment.""" return str(self.id).zfill(6) @classmethod def from_order(cls, order: Order) -> Self: """Create a payment from an order.""" return cls.objects.create( order=order, user=order.user, amount=order.total, description=order.description, )