Skip to content
Snippets Groups Projects
invoice.py 6.14 KiB
Newer Older
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
Nik | Klampfradler's avatar
Nik | Klampfradler committed
from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _

from payments import PaymentStatus, PurchasedItem
from payments.models import BasePayment
from aleksis.core.mixins import ExtensibleModel, PureDjangoModel
from aleksis.core.models import Person
from ..tables import PurchasedItemsTable, TotalsTable
from .base import Client


class InvoiceGroup(ExtensibleModel):
    name = models.CharField(verbose_name=_("Invoice group name"), max_length=255)
    client = models.ForeignKey(
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        Client,
        verbose_name=_("Linked client"),
        related_name="invoice_groups",
        on_delete=models.SET_NULL,
        null=True,
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    template_name = models.CharField(
        verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255
    )
    def __str__(self) -> str:
        return self.name

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client")
        ]


class Invoice(BasePayment, PureDjangoModel):
    VARIANT_DISPLAY = {
        "paypal": (_("PayPal"), "logos:paypal"),
        "sofort": (_("Klarna / Sofort"), "simple-icons:klarna"),
        "pledge": (_("Payment pledge / manual payment"), "mdi:hand-coin"),
        "sdd": (_("SEPA Direct Debit"), "mdi:bank-transfer"),
    }
    STATUS_ICONS = {
        PaymentStatus.WAITING: "mdi:cash-lock-open",
        PaymentStatus.INPUT: "mdi:cash-lock-open",
        PaymentStatus.PREAUTH: "mdi:cash-lock",
        PaymentStatus.CONFIRMED: "mdi:cash-check",
        PaymentStatus.REFUNDED: "mdi:cash-refund",
        PaymentStatus.REJECTED: "mdi:cash-remove",
        PaymentStatus.ERROR: "mdi:cash-remove",
    }
    group = models.ForeignKey(
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        InvoiceGroup,
        verbose_name=_("Invoice group"),
        related_name="invoices",
        on_delete=models.SET_NULL,
        null=True,
    number = models.CharField(verbose_name=_("Invoice number"), max_length=255)
    due_date = models.DateField(verbose_name=_("Payment due date"), null=True)

    for_content_type = models.ForeignKey(ContentType, on_delete=models.SET_NULL, null=True)
    for_object_id = models.PositiveIntegerField()
    for_object = GenericForeignKey("for_content_type", "for_object_id")
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    person = models.ForeignKey(
        Person,
        on_delete=models.SET_NULL,
        verbose_name=_("Invoice recipient (person)"),
        blank=True,
        null=True,
    )
    items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items"))

    @classmethod
    def get_variant_choices(cls):
        choices = []
        for variant in settings.PAYMENT_VARIANTS.keys():
            choices.append((variant, cls.VARIANT_DISPLAY[variant][0]))
    def get_variant_name(self):
        return self.__class__.VARIANT_DISPLAY[self.variant][0]

    def get_variant_icon(self):
        return self.__class__.VARIANT_DISPLAY[self.variant][1]

    def get_status_icon(self):
        return self.__class__.STATUS_ICONS[self.status]

    def get_purchased_items(self):
        for item in self.items.all():
            yield item.as_purchased_item()
        else:
            return self.for_object.get_purchased_items()
    def get_person(self):
        if self.person:
            return self.person
        elif hasattr(self.for_object, "person"):
            return self.for_object.person
        elif hasattr(self.for_object, "get_person"):
            return self.for_object.get_person()

        return None

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            models.CheckConstraint(
                check=(Q(for_object_id__isnull=True) | Q(person__isnull=True)),
                name="object_or_person",
            ),
    def get_billing_email_recipients(self):
        if hasattr(self.for_object, "get_billing_email_recipients"):
            return self.for_object.get_billing_email_recipients()
        else:
            return list(self.billing_email)

    @property
    def purchased_items_table(self):
        items = [i._asdict() for i in self.get_purchased_items()]
        return PurchasedItemsTable(items)

    @property
    def totals_table(self):
        tax_amounts = {}
        for item in self.get_purchased_items():
            tax_amounts.setdefault(item.tax_rate, 0)
            tax_amounts[item.tax_rate] += item.price / (item.tax_rate + 100) * item.tax_rate

        values = []
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        for tax_rate, total in tax_amounts.items():
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            values.append(
                {
                    "name": _("Included VAT {} %").format(tax_rate),
                    "value": total,
                    "currency": self.currency,
                }
            )

        values.append(
            {
                "name": _("Gross total"),
                "value": self.total,
                "currency": self.currency,
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            }
        )

        return TotalsTable(values)
Nik | Klampfradler's avatar
Nik | Klampfradler committed

    def get_success_url(self):
Tom Teichler's avatar
Tom Teichler committed
        return reverse("invoice_by_token", kwargs={"slug": self.token})
Nik | Klampfradler's avatar
Nik | Klampfradler committed

    def get_failure_url(self):
Tom Teichler's avatar
Tom Teichler committed
        return reverse("invoice_by_token", kwargs={"slug": self.token})
class InvoiceItem(ExtensibleModel):
    sku = models.CharField(max_length=255, verbose_name=_("Article no."), blank=True)
    description = models.CharField(max_length=255, verbose_name=_("Purchased item"))
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    price = models.DecimalField(
        verbose_name=_("Item gross price"), max_digits=9, decimal_places=2, default="0.0"
    )
    currency = models.CharField(max_length=10, verbose_name=_("Currency"))
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    tax_rate = models.DecimalField(
        verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0"
    )
    def as_purchased_item(self):
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        yield PurchasedItem(
            name=self.description,
            quantity=1,
            price=self.price,
            currency=self.currency,
            sku=self.sku,
            tax_rate=self.tax_rate,
        )