Newer
Older
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
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 ..tables import PurchasedItemsTable, TotalsTable
class InvoiceGroup(ExtensibleModel):
name = models.CharField(verbose_name=_("Invoice group name"), max_length=255)
Client,
verbose_name=_("Linked client"),
related_name="invoice_groups",
on_delete=models.SET_NULL,
null=True,
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",
}
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")
# For manual invoicing
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()
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
models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
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 = []
values.append(
{
"name": _("Included VAT {} %").format(tax_rate),
"value": total,
"currency": self.currency,
}
)
values.append(
{
"name": _("Gross total"),
"value": self.total,
return reverse("invoice_by_token", kwargs={"slug": self.token})
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"))
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"))
tax_rate = models.DecimalField(
verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0"
)
def as_purchased_item(self):