This commit is contained in:
Raphael Michel
2026-09-13 20:32:39 +02:00
parent ea40a670f4
commit 8159a039c8
9 changed files with 58 additions and 4 deletions
+12
View File
@@ -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,
+5 -1
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
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)
+1 -1
View File
@@ -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()
+10
View File
@@ -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)")
+3 -1
View File
@@ -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'):
+1 -1
View File
@@ -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
+18
View File
@@ -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
@@ -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" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" %}
</div>
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Condition" context "discount" %}</legend>
+2
View File
@@ -46,6 +46,8 @@ TEST_DISCOUNT_RES = {
"sales_channels": ["bar", "baz", "web"],
"available_from": None,
"available_until": None,
"require_membership_hidden": False,
"require_membership_types": [],
"subevent_mode": "mixed",
"subevent_date_from": None,
"subevent_date_until": None,