forked from CGM_Public/pretix_original
Add add-on products
This commit is contained in:
@@ -11,6 +11,7 @@ from pretix.base.forms import I18nFormSet, I18nModelForm
|
||||
from pretix.base.models import (
|
||||
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
|
||||
|
||||
class CategoryForm(I18nModelForm):
|
||||
@@ -19,7 +20,8 @@ class CategoryForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'description'
|
||||
'description',
|
||||
'is_addon'
|
||||
]
|
||||
|
||||
|
||||
@@ -208,3 +210,58 @@ class ItemVariationForm(I18nModelForm):
|
||||
'active',
|
||||
'default_price',
|
||||
]
|
||||
|
||||
|
||||
class ItemAddOnsFormSet(I18nFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.get('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['event'] = self.event
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
categories = set()
|
||||
for i in range(0, self.total_form_count()):
|
||||
form = self.forms[i]
|
||||
if self.can_delete:
|
||||
if self._should_delete_form(form):
|
||||
# This form is going to be deleted so any of its errors
|
||||
# should not cause the entire formset to be invalid.
|
||||
continue
|
||||
|
||||
if form.cleaned_data['addon_category'] in categories:
|
||||
raise ValidationError(_('You added the same add-on category twice'))
|
||||
|
||||
categories.add(form.cleaned_data['addon_category'])
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
self.is_valid()
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
locales=self.locales,
|
||||
event=self.event
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
|
||||
class ItemAddOnForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['addon_category'].queryset = self.event.categories.all()
|
||||
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'addon_category',
|
||||
'min_count',
|
||||
'max_count',
|
||||
]
|
||||
|
||||
@@ -95,6 +95,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
|
||||
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
|
||||
'pretix.event.item.variation.changed': _('The variation "{value}" has been modified.'),
|
||||
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
|
||||
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
|
||||
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
|
||||
'pretix.event.quota.added': _('The quota has been added.'),
|
||||
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
||||
'pretix.event.quota.changed': _('The quota has been modified.'),
|
||||
|
||||
86
src/pretix/control/templates/pretixcontrol/item/addons.html
Normal file
86
src/pretix/control/templates/pretixcontrol/item/addons.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block inside %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
With add-ons, you can specify products that can be bought as an addition to this product. For example, if
|
||||
you host a conference with a base conference ticket and a number of workshops, you could define the
|
||||
workshops as add-ons to the conference ticket. With this configuration, the workshops cannot be bought
|
||||
on their own but only in combination with a conference ticket. You can here specify categories of products
|
||||
that can be used as add-ons to this product. You can also specify the minimum and maximum number of
|
||||
add-ons of the given category that can or need to be chosen. The user can buy every add-on from the
|
||||
category at most once. If an add-on product has multiple variations, only one of them can be bought.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-horizontal branches" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.addon_category layout='horizontal' %}
|
||||
{% bootstrap_field form.min_count layout='horizontal' %}
|
||||
{% bootstrap_field form.max_count layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.addon_category layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.min_count layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.max_count layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new add-on" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -4,20 +4,25 @@
|
||||
{% block content %}
|
||||
{% if object.id %}
|
||||
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
|
||||
{% if object.has_variations %}
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "event.item" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
|
||||
{% trans "General information" %}
|
||||
</a>
|
||||
</li>
|
||||
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
|
||||
{% trans "Variations" %}
|
||||
{% if object.has_variations %}
|
||||
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
|
||||
{% trans "Variations" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li {% if "event.item.addons" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.item.addons' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
|
||||
{% trans "Add-Ons" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h1>{% trans "Create product" %}</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
@@ -26,12 +31,12 @@
|
||||
{% endif %}
|
||||
{% if object.id and not object.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
{% blocktrans trimmed %}
|
||||
Please note that your product will <strong>not</strong> be available for sale until you have added your
|
||||
item to an existing or newly created quota.
|
||||
{% endblocktrans %}
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.description layout="horizontal" %}
|
||||
{% bootstrap_field form.is_addon layout="horizontal" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if category %}
|
||||
|
||||
@@ -61,6 +61,8 @@ urlpatterns = [
|
||||
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
|
||||
name='event.item.variations'),
|
||||
url(r'^items/(?P<item>\d+)/addons', item.ItemAddOns.as_view(),
|
||||
name='event.item.addons'),
|
||||
url(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
|
||||
url(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
|
||||
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
|
||||
|
||||
@@ -21,9 +21,11 @@ from pretix.base.models import (
|
||||
CachedTicket, Item, ItemCategory, ItemVariation, Order, Question,
|
||||
QuestionAnswer, QuestionOption, Quota, Voucher,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemCreateForm,
|
||||
ItemUpdateForm, ItemVariationForm, ItemVariationsFormSet, QuestionForm,
|
||||
QuestionOptionForm, QuotaForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
@@ -927,6 +929,74 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
return context
|
||||
|
||||
|
||||
class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/item/addons.html'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.item = None
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Item, ItemAddOn,
|
||||
form=ItemAddOnForm, formset=ItemAddOnsFormSet,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
|
||||
event=self.request.event)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
if self.formset.is_valid():
|
||||
for form in self.formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.addons.removed', user=self.request.user, data={
|
||||
'category': form.instance.addon_category.pk
|
||||
}
|
||||
)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
|
||||
forms = [
|
||||
ef for ef in self.formset.extra_forms + self.formset.initial_forms
|
||||
if ef not in self.formset.deleted_forms
|
||||
]
|
||||
for i, form in enumerate(forms):
|
||||
form.instance.base_item = self.get_object()
|
||||
created = not form.instance.pk
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.addons.changed' if not created else
|
||||
'pretix.event.item.addons.added',
|
||||
user=self.request.user, data=change_data
|
||||
)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item.addons', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.get_object().id,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
self.object = self.get_object()
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['formset'] = self.formset
|
||||
return context
|
||||
|
||||
|
||||
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Item
|
||||
template_name = 'pretixcontrol/item/delete.html'
|
||||
|
||||
Reference in New Issue
Block a user