diff --git a/doc/api/resources/discounts.rst b/doc/api/resources/discounts.rst
index 1e89d9184b..9900268b60 100644
--- a/doc/api/resources/discounts.rst
+++ b/doc/api/resources/discounts.rst
@@ -30,6 +30,8 @@ available_from datetime The first da
(or ``null``).
available_until datetime The last date time at which this discount can be applied
(or ``null``).
+require_membership boolean If ``true``, booking this variation requires an active membership.
+require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
subevent_mode strings Determines how the discount is handled when used in an
event series. Can be ``"mixed"`` (no special effect),
``"same"`` (discount is only applied for groups within
@@ -108,6 +110,8 @@ Endpoints
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+ "require_membership_hidden": false,
+ "require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
@@ -168,6 +172,8 @@ Endpoints
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+ "require_membership_hidden": false,
+ "require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
@@ -214,6 +220,8 @@ Endpoints
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+ "require_membership_hidden": false,
+ "require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
@@ -249,6 +257,8 @@ Endpoints
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+ "require_membership_hidden": false,
+ "require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
@@ -313,6 +323,8 @@ Endpoints
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+ "require_membership_hidden": false,
+ "require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
diff --git a/src/pretix/api/serializers/discount.py b/src/pretix/api/serializers/discount.py
index e6f5d5f0a6..65e075e8c5 100644
--- a/src/pretix/api/serializers/discount.py
+++ b/src/pretix/api/serializers/discount.py
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
+from django.db.models import QuerySet
+from django.utils.functional import lazy
from rest_framework import serializers
from pretix.api.serializers import SalesChannelMigrationMixin
@@ -38,7 +40,8 @@ class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
class Meta:
model = Discount
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
- 'available_from', 'available_until', 'subevent_mode', 'subevent_date_from', 'subevent_date_until',
+ 'available_from', 'available_until', 'require_membership', 'require_membership_types',
+ 'subevent_mode', 'subevent_date_from', 'subevent_date_until',
'condition_all_products', 'condition_limit_products', 'condition_apply_to_addons',
'condition_min_count', 'condition_min_value', 'benefit_discount_matching_percent',
'benefit_only_apply_to_cheapest_n_matches', 'benefit_same_products', 'benefit_limit_products',
@@ -50,6 +53,7 @@ class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
+ self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
def validate(self, data):
data = super().validate(data)
diff --git a/src/pretix/api/views/discount.py b/src/pretix/api/views/discount.py
index 92bbd671e0..41a4ef55ac 100644
--- a/src/pretix/api/views/discount.py
+++ b/src/pretix/api/views/discount.py
@@ -62,7 +62,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.discounts.prefetch_related(
- 'limit_sales_channels',
+ 'limit_sales_channels', 'require_membership_types',
)
@transaction.atomic()
diff --git a/src/pretix/base/models/discount.py b/src/pretix/base/models/discount.py
index f39a95b175..65d10b62e9 100644
--- a/src/pretix/base/models/discount.py
+++ b/src/pretix/base/models/discount.py
@@ -96,6 +96,16 @@ class Discount(LoggedModel):
choices=SUBEVENT_MODE_CHOICES,
)
+ require_membership = models.BooleanField(
+ verbose_name=_('Require a valid membership'),
+ default=False,
+ )
+ require_membership_types = models.ManyToManyField(
+ 'MembershipType',
+ verbose_name=_('Allowed membership types'),
+ blank=True,
+ )
+
condition_all_products = models.BooleanField(
default=True,
verbose_name=_("Apply to all products (including newly created ones)")
diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index 235ab89f9c..44ea0d6547 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -1081,6 +1081,7 @@ class Event(EventMixin, LoggedModel):
c_items = list(d.condition_limit_products.all())
b_items = list(d.benefit_limit_products.all())
limit_sales_channels = list(d.limit_sales_channels.all())
+ require_membership_types = list(d.require_membership_types.all())
d.pk = None
d.event = self
d._prefetched_objects_cache = {}
@@ -1092,9 +1093,10 @@ class Event(EventMixin, LoggedModel):
for i in b_items:
if i.pk in item_map:
d.benefit_limit_products.add(item_map[i.pk])
-
if not d.all_sales_channels:
d.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
+ if require_membership_types and not is_cross_organizer:
+ d.require_membership_types.set(require_membership_types)
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
diff --git a/src/pretix/base/services/memberships.py b/src/pretix/base/services/memberships.py
index f6495e3512..942f02026c 100644
--- a/src/pretix/base/services/memberships.py
+++ b/src/pretix/base/services/memberships.py
@@ -86,7 +86,7 @@ def create_membership(customer: Customer, position: OrderPosition):
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
valid_from_not_chosen=False):
"""
- Validate that a set of cart or order positions. This currently does not validate
+ Validate that a set of cart or order positions.
:param customer: Customer to validate for
:param positions: List of order or cart positions
diff --git a/src/pretix/control/forms/discounts.py b/src/pretix/control/forms/discounts.py
index e73babde3f..7f134c431e 100644
--- a/src/pretix/control/forms/discounts.py
+++ b/src/pretix/control/forms/discounts.py
@@ -22,6 +22,7 @@
from decimal import Decimal
from django import forms
+from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelMultipleChoiceField
from pretix.base.channels import get_all_sales_channel_types
@@ -45,6 +46,8 @@ class DiscountForm(I18nModelForm):
'limit_sales_channels',
'available_from',
'available_until',
+ 'require_membership',
+ 'require_membership_types',
'subevent_date_from',
'subevent_date_until',
'subevent_mode',
@@ -107,6 +110,13 @@ class DiscountForm(I18nModelForm):
self.fields['condition_min_value'].required = False
self.fields['condition_min_value'].widget.is_required = False
+ qs = self.event.organizer.membership_types.all()
+ if qs:
+ self.fields['require_membership_types'].queryset = qs
+ else:
+ del self.fields['require_membership']
+ del self.fields['require_membership_types']
+
if not self.event.has_subevents:
del self.fields['subevent_mode']
@@ -123,4 +133,12 @@ class DiscountForm(I18nModelForm):
d['condition_min_count'] = 0
if d.get('condition_min_value') is None:
d['condition_min_value'] = Decimal('0.00')
+
+ if d.get('require_membership') and not d.get('require_membership_types'):
+ self.add_error(
+ 'require_membership_types',
+ _(
+ "If a valid membership is required, at least one valid membership type needs to be selected."
+ )
+ )
return d
diff --git a/src/pretix/control/templates/pretixcontrol/items/discount.html b/src/pretix/control/templates/pretixcontrol/items/discount.html
index 5265729600..0945f4e6e6 100644
--- a/src/pretix/control/templates/pretixcontrol/items/discount.html
+++ b/src/pretix/control/templates/pretixcontrol/items/discount.html
@@ -17,6 +17,12 @@
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %}
{% bootstrap_field form.limit_sales_channels layout="control" %}
+ {% if form.require_membership %}
+ {% bootstrap_field form.require_membership layout="control" %}
+
+ {% bootstrap_field form.require_membership_types layout="control" %}
+
+ {% endif %}