"""Models for the accounting app.""" from hashlib import md5 from typing import Self from django.conf import settings from django.contrib import admin 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 @admin.display( ordering=None, description="Total (incl. VAT)", boolean=False, ) 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}"