diff --git a/src/pretix/base/migrations/0006_create_invoice_voucher.py b/src/pretix/base/migrations/0006_create_invoice_voucher.py new file mode 100644 index 000000000..9fb93c033 --- /dev/null +++ b/src/pretix/base/migrations/0006_create_invoice_voucher.py @@ -0,0 +1,73 @@ +# Generated by Django 5.1.3 on 2024-11-12 08:04 + +from django.db import migrations, models + +import pretix.base.models.base +import pretix.base.models.vouchers + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0005_page_alter_cachedcombinedticket_id_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="InvoiceVoucher", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ( + "code", + models.CharField( + db_index=True, + default=pretix.base.models.vouchers.generate_code, + max_length=255, + unique=True, + ), + ), + ("max_usages", models.PositiveIntegerField(default=1)), + ("redeemed", models.PositiveIntegerField(default=0)), + ( + "budget", + models.DecimalField(decimal_places=2, max_digits=10, null=True), + ), + ( + "valid_until", + models.DateTimeField(blank=True, db_index=True, null=True), + ), + ("price_mode", models.CharField(default="none", max_length=100)), + ( + "value", + models.DecimalField(decimal_places=2, max_digits=10, null=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("created_by", models.CharField(default="system", max_length=50)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("updated_by", models.CharField(default="system", max_length=50)), + ( + "limit_events", + models.ManyToManyField( + related_name="invoice_vouchers", to="pretixbase.event" + ), + ), + ( + "limit_organizer", + models.ManyToManyField( + related_name="invoice_vouchers", to="pretixbase.organizer" + ), + ), + ], + options={ + "verbose_name": "Invoice Voucher", + "verbose_name_plural": "Invoice Vouchers", + "ordering": ("code",), + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + ] diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 0e458054b..1be8f7f06 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -503,3 +503,83 @@ def budget_used(self): ] ).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00') return ops + + +class InvoiceVoucher(LoggedModel): + PRICE_MODES = ( + ('none', _('No effect')), + ('set', _('Set product price to')), + ('subtract', _('Subtract from product price')), + ('percent', _('Reduce product price by (%)')), + ) + code = models.CharField( + verbose_name=_("Voucher code"), + max_length=255, default=generate_code, + db_index=True, + validators=[MinLengthValidator(5)], + unique=True + ) + max_usages = models.PositiveIntegerField( + verbose_name=_("Maximum usages"), + help_text=_("Number of times this voucher can be redeemed."), + default=1 + ) + redeemed = models.PositiveIntegerField( + verbose_name=_("Redeemed"), + default=0 + ) + budget = models.DecimalField( + verbose_name=_("Maximum discount budget"), + help_text=_("This is the maximum monetary amount that will be " + "discounted using this voucher across all usages."), + decimal_places=2, max_digits=10, + null=True, blank=True + ) + valid_until = models.DateTimeField( + blank=True, null=True, db_index=True, + verbose_name=_("Valid until") + ) + price_mode = models.CharField( + verbose_name=_("Price mode"), + max_length=100, + choices=PRICE_MODES, + default='none' + ) + value = models.DecimalField( + verbose_name=_("Voucher value"), + decimal_places=2, max_digits=10, null=True, blank=True, + ) + + limit_events = models.ManyToManyField( + 'Event', + verbose_name=_("Limit to events"), + blank=True, + related_name='invoice_vouchers' + ) + + limit_organizer = models.ManyToManyField( + 'Organizer', + verbose_name=_("Limit to Organizer"), + blank=True, + related_name='invoice_vouchers' + ) + + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.CharField(max_length=50, default="system") + updated_at = models.DateTimeField(auto_now=True) + updated_by = models.CharField(max_length=50, default="system") + + class Meta: + verbose_name = _("Invoice Voucher") + verbose_name_plural = _("Invoice Vouchers") + ordering = ('code', ) + + def __str__(self): + return self.code + + def is_active(self): + if self.redeemed >= self.max_usages: + return False + if self.valid_until and self.valid_until < now(): + return False + return True diff --git a/src/pretix/control/forms/admin/__init__.py b/src/pretix/control/forms/admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/control/forms/admin/vouchers.py b/src/pretix/control/forms/admin/vouchers.py new file mode 100644 index 000000000..3b02d49f7 --- /dev/null +++ b/src/pretix/control/forms/admin/vouchers.py @@ -0,0 +1,67 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ +from django_scopes import scopes_disabled + +from pretix.base.forms import I18nModelForm +from pretix.base.forms.widgets import SplitDateTimePickerWidget +from pretix.base.models import Event, Organizer +from pretix.base.models.vouchers import InvoiceVoucher +from pretix.control.forms import SplitDateTimeField + + +class InvoiceVoucherForm(I18nModelForm): + event_effect = forms.ModelMultipleChoiceField( + queryset=Event.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Event effect"), + help_text=_("The voucher will only be valid for the selected events.") + ) + organizer_effect = forms.ModelMultipleChoiceField( + queryset=Organizer.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Organizer effect"), + help_text=_("The voucher will be valid for all events under the selected organizers.") + ) + + class Meta: + model = InvoiceVoucher + localized_fields = '__all__' + fields = [ + 'code', 'valid_until', 'value', 'max_usages', 'price_mode', 'budget', 'event_effect', 'organizer_effect' + ] + field_classes = { + 'valid_until': SplitDateTimeField, + } + widgets = { + 'valid_until': SplitDateTimePickerWidget(), + } + + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + super().__init__(*args, **kwargs) + if instance: + self.fields['event_effect'].initial = instance.limit_events.all() + self.fields['organizer_effect'].initial = instance.limit_organizer.all() + with scopes_disabled(): + self.fields['event_effect'].queryset = Event.objects.all() + self.fields['organizer_effect'].queryset = Organizer.objects.all() + + def clean(self): + data = super().clean() + return data + + def save(self, commit=True): + instance = super().save(commit=False) + + if commit: + instance.save() + + instance.limit_events.set(self.cleaned_data.get('event_effect', [])) + instance.limit_organizer.set(self.cleaned_data.get('organizer_effect', [])) + + if commit: + self.save_m2m() + + return instance diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index e403913d2..3a01e57a7 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -549,6 +549,12 @@ def get_admin_navigation(request): }, ] }, + { + 'label': _('Vouchers'), + 'url': reverse('control:admin.vouchers'), + 'active': 'vouchers' in url.url_name, + 'icon': 'tags', + }, { 'label': _('Global settings'), 'url': reverse('control:admin.global.settings'), diff --git a/src/pretix/control/templates/pretixcontrol/admin/vouchers/delete.html b/src/pretix/control/templates/pretixcontrol/admin/vouchers/delete.html new file mode 100644 index 000000000..bc0e0d8d4 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/admin/vouchers/delete.html @@ -0,0 +1,19 @@ +{% extends "pretixcontrol/admin/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Delete voucher" %}{% endblock %} +{% block content %} +
+ {% blocktrans trimmed %} + You haven't created any vouchers yet. + {% endblocktrans %} +
+ + {% trans "Create a new voucher" %} ++ {% trans "Create a new voucher" %} +
+ + {% include "pretixcontrol/pagination.html" %} + {% endif %} +{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 2ed3799a5..1f79c135c 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -347,6 +347,12 @@ url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='admin.global.settings'), url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='admin.global.update'), url(r'^global/message/$', global_settings.MessageView.as_view(), name='admin.global.message'), + url(r'^vouchers/$', admin.VoucherList.as_view(), name='admin.vouchers'), + url(r'^vouchers/add$', admin.VoucherCreate.as_view(), name='admin.vouchers.add'), + url(r'^vouchers/(?P