diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 988e809620..e999debe99 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -441,7 +441,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer): class Meta: model = ItemCategory - fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon') + fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon', 'cross_selling_mode', 'cross_selling_condition') class QuestionOptionSerializer(I18nAwareModelSerializer): diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 04af185003..9d6f54398c 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -111,6 +111,31 @@ class ItemCategory(LoggedModel): 'only be bought in combination with a product that has this category configured as a possible ' 'source for add-ons.') ) + CROSS_SELLING_MODES = ( + (None, _('Normal category')), + ('both', _('Combined category')), + ('only', _('Cross-selling category')), + ) + cross_selling_mode = models.CharField( + choices=CROSS_SELLING_MODES, + null=True, + ) + CROSS_SELLING_CONDITION = ( + ('always', _('Always show in cross-selling step')), + ('products', _('Only if the cart contains one of these products')), + ('discounts', _('Only show products affected by discount rules')), + ) + cross_selling_condition = models.CharField( + verbose_name=_("Cross-selling condition"), + choices=CROSS_SELLING_CONDITION, + null=True, + ) + cross_selling_match_products = models.ManyToManyField( + 'pretixbase.Item', + blank=True, + verbose_name=_("Cross-selling condition products"), + related_name="matched_by_cross_selling_categories", + ) class Meta: verbose_name = _("Product category") diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index a2e07f4dfd..a1b992b972 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -41,6 +41,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError from django.db.models import Max +from django.forms import ChoiceField, RadioSelect from django.forms.formsets import DELETION_FIELD_NAME from django.urls import reverse from django.utils.functional import cached_property @@ -79,11 +80,63 @@ class CategoryForm(I18nModelForm): 'name', 'internal_name', 'description', - 'is_addon' + #'is_addon' + 'cross_selling_condition', + 'cross_selling_match_products', ] widgets = { 'description': I18nMarkdownTextarea, + 'cross_selling_condition': RadioSelect, + #'is_addon': BooleanRadio( + # 'normal', _('Normal category'), + # 'addon', _('Products in this category are add-on products'), + #) } + field_classes = { + 'cross_selling_match_products': SafeModelMultipleChoiceField, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + #self.fields['is_addon'].label = _('Category type') + self.fields['category_type'] = ChoiceField(widget=RadioSelect, choices=( + ('normal', mark_safe('{}   {}'.format(_('Normal category'), _('Products in this category are regular products displayed on the front page.'))),), + ('addon', mark_safe('{}   {}'.format(_('Add-on product category'), _('Products in this category are add-on products and can only be bought as add-ons.'))),), + ('only', mark_safe('{}   {}'.format(_('Cross-selling category'), _('Products in this category are regular products, but are only shown in the cross-selling step, according to the configuration below.'))),), + ('both', mark_safe('{}   {}'.format(_('Combined category'), _('Products in this category are regular products displayed on the front page, but are additionally shown in the cross-selling step, according to the configuration below.'))),), + )) + self.fields['category_type'].initial = 'addon' if self.instance.is_addon else (self.instance.cross_selling_mode or 'normal') + #self.fields['show_in_cross_selling'] = BooleanField(widget=CheckboxInput(attrs={ + # 'data-display-dependency': '#id_category_type_0', + #}), + # help_text='Products are additionally shown in the cross-selling step, according to the configuration below') + self.fields['cross_selling_condition'].widget.attrs['data-display-dependency'] = '#id_category_type_2,#id_category_type_3' + self.fields['cross_selling_condition'].widget.attrs['data-disable-dependent'] = 'true' + self.fields['cross_selling_condition'].widget.choices = self.fields['cross_selling_condition'].widget.choices[1:] + self.fields['cross_selling_condition'].required = False + + self.fields['cross_selling_match_products'].widget = forms.CheckboxSelectMultiple( + attrs={ + 'class': 'scrolling-multiple-choice', + 'data-display-dependency': '#id_cross_selling_condition_1' + } + ) + self.fields['cross_selling_match_products'].queryset = self.event.items.all() + + def clean(self): + d = super().clean() + if d.get('category_type') == 'only' or d.get('category_type') == 'both': + if not d.get('cross_selling_condition'): + raise ValidationError({'cross_selling_condition': [_('This field is required')]}) + self.instance.cross_selling_mode = d.get('category_type') + else: + self.instance.cross_selling_mode = None + if d.get('category_type') == 'only' or d.get('category_type') == 'both' or d.get('category_type') == 'normal': + self.instance.is_addon = False + elif d.get('category_type') == 'addon': + self.instance.is_addon = True + d['category_type'] = 'normal' + return d class QuestionForm(I18nModelForm): diff --git a/src/pretix/control/templates/pretixcontrol/items/category.html b/src/pretix/control/templates/pretixcontrol/items/category.html index b2b9c16ec5..0efc4a336d 100644 --- a/src/pretix/control/templates/pretixcontrol/items/category.html +++ b/src/pretix/control/templates/pretixcontrol/items/category.html @@ -16,7 +16,9 @@ {% bootstrap_field form.internal_name layout="control" %} {% bootstrap_field form.description layout="control" %} - {% bootstrap_field form.is_addon layout="control" %} + {% bootstrap_field form.category_type layout="control" %} + {% bootstrap_field form.cross_selling_condition layout="control" %} + {% bootstrap_field form.cross_selling_match_products layout="control" %} {% if category %} diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 60603604e9..3b17fb973d 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -381,6 +381,9 @@ var form_handlers = function (el) { enabled = !enabled; } var $toggling = dependent; + if (dependent.attr("data-disable-dependent")) { + $toggling.attr('disabled', !enabled); + } if (dependent.get(0).tagName.toLowerCase() !== "div") { $toggling = dependent.closest('.form-group'); } @@ -395,8 +398,10 @@ var form_handlers = function (el) { } }; update(); - dependency.closest('.form-group').find('[name=' + dependency.attr("name") + ']').on("change", update); - dependency.closest('.form-group').find('[name=' + dependency.attr("name") + ']').on("dp.change", update); + dependency.each(function() { + $(this).closest('.form-group').find('[name=' + $(this).attr("name") + ']').on("change", update); + $(this).closest('.form-group').find('[name=' + $(this).attr("name") + ']').on("dp.change", update); + }) }); el.find("input[data-required-if], select[data-required-if], textarea[data-required-if]").each(function () {