diff --git a/aleksis/apps/tezor/migrations/0003_manual_invoicing.py b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py new file mode 100644 index 0000000000000000000000000000000000000000..3751573d6ba5f37a7934371fb750d92808c97437 --- /dev/null +++ b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.12 on 2022-03-12 21:41 + +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0038_notification_send_at.py'), + ('sites', '0002_alter_domain_unique'), + ('tezor', '0002_invoice_due_date'), + ] + + operations = [ + migrations.CreateModel( + name='InvoiceItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('sku', models.CharField(blank=True, max_length=255, verbose_name='Article no.')), + ('description', models.CharField(max_length=255, verbose_name='Purchased item')), + ('price', models.DecimalField(decimal_places=2, default='0.0', max_digits=9, verbose_name='Item gross price')), + ('currency', models.CharField(max_length=10, verbose_name='Currency')), + ('tax_rate', models.DecimalField(decimal_places=1, default='0.0', max_digits=4, verbose_name='Tax rate')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + migrations.AddField( + model_name='invoice', + name='person', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.person', verbose_name='Invoice recipient (person)'), + ), + migrations.AddConstraint( + model_name='invoice', + constraint=models.CheckConstraint(check=models.Q(('for_object_id__isnull', True), ('person__isnull', True), _connector='OR'), name='object_or_person'), + ), + migrations.AddField( + model_name='invoiceitem', + name='site', + field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site'), + ), + migrations.AddField( + model_name='invoice', + name='items', + field=models.ManyToManyField(to='tezor.InvoiceItem', verbose_name='Invoice items'), + ), + ] diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py index c67e29d627d6c74d745cfffb31dbd70e970019df..7c87380376ead88f18aa9e677cf269cc1dfb6dfd 100644 --- a/aleksis/apps/tezor/models/invoice.py +++ b/aleksis/apps/tezor/models/invoice.py @@ -2,17 +2,18 @@ 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 from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ -from djmoney.models.fields import CurrencyField, MoneyField from payments import PaymentStatus, PurchasedItem from payments.models import BasePayment from aleksis.core.mixins import ExtensibleModel, PureDjangoModel +from aleksis.core.models import Person -from .base import Client from ..tables import PurchasedItemsTable, TotalsTable +from .base import Client class InvoiceGroup(ExtensibleModel): @@ -61,6 +62,10 @@ class Invoice(BasePayment, PureDjangoModel): 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 = [] @@ -78,10 +83,15 @@ class Invoice(BasePayment, PureDjangoModel): return self.__class__.STATUS_ICONS[self.status] def get_purchased_items(self): - return self.for_object.get_purchased_items() + for item in self.items.all(): + yield item.as_purchased_item() + else: + return self.for_object.get_purchased_items() def get_person(self): - if hasattr(self.for_object, "person"): + 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() @@ -90,7 +100,8 @@ class Invoice(BasePayment, PureDjangoModel): class Meta: constraints = [ - models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group") + 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"), ] @property @@ -126,3 +137,14 @@ class Invoice(BasePayment, PureDjangoModel): def get_failure_url(self): 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): + yield PurchasedItem(name=self.description, quantity=1, price=self.price, currency=self.currency, sku=self.sku, tax_rate=self.tax_rate)