"""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("membership.Member", on_delete=models.PROTECT) def __str__(self) -> str: return f"Account of {self.owner}" @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. We assemble the order from a number of products. Once an order is paid, the contents should be considered locked. """ member = models.ForeignKey("membership.Member", on_delete=models.PROTECT) account = models.ForeignKey(Account, on_delete=models.PROTECT) description = models.CharField(max_length=1024, verbose_name=_("description")) 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 (excl VAT).""" return sum(item.price * item.quantity for item in self.items.all()) @property def total_vat(self) -> Money: """Return the total VAT of the order.""" return sum(item.vat * item.quantity for item in self.items.all()) @property def total_with_vat(self) -> Money: """Return the TOTAL amount WITH VAT.""" return self.total + self.total_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 Product(CreatedModifiedAbstract): """A generic product, for instance a membership or a service fee.""" name = models.CharField(max_length=512) price = MoneyField(max_digits=16, decimal_places=2) vat = MoneyField(max_digits=16, decimal_places=2) def __str__(self) -> str: return self.name class OrderProduct(CreatedModifiedAbstract): """When a product is ordered, we store the product on the order. This includes pricing information. """ order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.PROTECT) price = MoneyField(max_digits=16, decimal_places=2) vat = MoneyField(max_digits=16, decimal_places=2) quantity = models.PositiveSmallIntegerField(default=1) class Meta: verbose_name = _("ordered product") verbose_name_plural = _("ordered products") def __str__(self) -> str: return f"{self.product.name}" @property def total_with_vat(self) -> Money: """Total price of this item.""" return (self.price + self.vat) * self.quantity 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")) payment_type = models.ForeignKey("PaymentType", on_delete=models.PROTECT) external_transaction_id = models.CharField(max_length=255, default="", 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, payment_type: "PaymentType") -> Self: """Create a payment from an order.""" return cls.objects.create( order=order, user=order.user, amount=order.total + order.total_vat, description=order.description, payment_type=payment_type, ) class PaymentType(CreatedModifiedAbstract): """Types of payments available in the system. - bank transfer - card payment (specific provider) """ name = models.CharField(max_length=1024, verbose_name=_("description")) description = models.TextField(max_length=2048, blank=True) enabled = models.BooleanField(default=True) def __str__(self) -> str: return f"{self.name}"