mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Cross-selling category configuration
This commit is contained in:
@@ -441,7 +441,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
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):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -111,6 +111,31 @@ class ItemCategory(LoggedModel):
|
|||||||
'only be bought in combination with a product that has this category configured as a possible '
|
'only be bought in combination with a product that has this category configured as a possible '
|
||||||
'source for add-ons.')
|
'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:
|
class Meta:
|
||||||
verbose_name = _("Product category")
|
verbose_name = _("Product category")
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
|
from django.forms import ChoiceField, RadioSelect
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME
|
from django.forms.formsets import DELETION_FIELD_NAME
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -79,11 +80,63 @@ class CategoryForm(I18nModelForm):
|
|||||||
'name',
|
'name',
|
||||||
'internal_name',
|
'internal_name',
|
||||||
'description',
|
'description',
|
||||||
'is_addon'
|
#'is_addon'
|
||||||
|
'cross_selling_condition',
|
||||||
|
'cross_selling_match_products',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'description': I18nMarkdownTextarea,
|
'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('{} <span class="text-muted">{}</span>'.format(_('Normal category'), _('Products in this category are regular products displayed on the front page.'))),),
|
||||||
|
('addon', mark_safe('{} <span class="text-muted">{}</span>'.format(_('Add-on product category'), _('Products in this category are add-on products and can only be bought as add-ons.'))),),
|
||||||
|
('only', mark_safe('{} <span class="text-muted">{}</span>'.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('{} <span class="text-muted">{}</span>'.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):
|
class QuestionForm(I18nModelForm):
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
{% bootstrap_field form.internal_name layout="control" %}
|
{% bootstrap_field form.internal_name layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
{% bootstrap_field form.description 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" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{% if category %}
|
{% if category %}
|
||||||
|
|||||||
@@ -381,6 +381,9 @@ var form_handlers = function (el) {
|
|||||||
enabled = !enabled;
|
enabled = !enabled;
|
||||||
}
|
}
|
||||||
var $toggling = dependent;
|
var $toggling = dependent;
|
||||||
|
if (dependent.attr("data-disable-dependent")) {
|
||||||
|
$toggling.attr('disabled', !enabled);
|
||||||
|
}
|
||||||
if (dependent.get(0).tagName.toLowerCase() !== "div") {
|
if (dependent.get(0).tagName.toLowerCase() !== "div") {
|
||||||
$toggling = dependent.closest('.form-group');
|
$toggling = dependent.closest('.form-group');
|
||||||
}
|
}
|
||||||
@@ -395,8 +398,10 @@ var form_handlers = function (el) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
update();
|
update();
|
||||||
dependency.closest('.form-group').find('[name=' + dependency.attr("name") + ']').on("change", update);
|
dependency.each(function() {
|
||||||
dependency.closest('.form-group').find('[name=' + dependency.attr("name") + ']').on("dp.change", update);
|
$(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 () {
|
el.find("input[data-required-if], select[data-required-if], textarea[data-required-if]").each(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user