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 () {