diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 318830f1d..734a46b96 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -196,10 +196,6 @@ class OtherOperationsForm(forms.Form): class OrderPositionAddForm(forms.Form): - do = forms.BooleanField( - label=_('Add a new product to the order'), - required=False - ) itemvar = forms.ChoiceField( label=_('Product') ) @@ -291,6 +287,28 @@ class OrderPositionAddForm(forms.Form): change_decimal_field(self.fields['price'], order.event.currency) +class OrderPositionAddFormset(forms.BaseFormSet): + def __init__(self, *args, **kwargs): + self.order = kwargs.pop('order', None) + super().__init__(*args, **kwargs) + + def _construct_form(self, i, **kwargs): + kwargs['order'] = self.order + return super()._construct_form(i, **kwargs) + + @property + def empty_form(self): + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + use_required_attribute=False, + order=self.order, + ) + self.add_fields(form, None) + return form + + class OrderPositionChangeForm(forms.Form): itemvar = forms.ChoiceField( required=False, diff --git a/src/pretix/control/templates/pretixcontrol/order/change.html b/src/pretix/control/templates/pretixcontrol/order/change.html index 6a929d0cb..fadbdb495 100644 --- a/src/pretix/control/templates/pretixcontrol/order/change.html +++ b/src/pretix/control/templates/pretixcontrol/order/change.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/event/base.html" %} {% load i18n %} {% load bootstrap3 %} +{% load formset_tags %} {% load money %} {% block title %} {% blocktrans trimmed with code=order.code %} @@ -64,10 +65,10 @@ {% endif %} {% if position.addon_to %} – - {% blocktrans trimmed with posid=position.addon_to.positionid %} - Add-On to position #{{ posid }} - {% endblocktrans %} - + {% blocktrans trimmed with posid=position.addon_to.positionid %} + Add-On to position #{{ posid }} + {% endblocktrans %} + {% endif %} @@ -173,33 +174,87 @@ {% endfor %} -
-
-

- {% trans "Add product" %} -

-
-
-
- {% bootstrap_form_errors add_form %} - {% if add_form.custom_error %} -
- {{ add_form.custom_error }} + + +
+ {{ add_formset.management_form }} + {% bootstrap_formset_errors add_formset %} +
+ {% for add_form in add_formset %} +
+
+

+ + {% trans "Add product" %} +
+ {{ add_form.id }} + {% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %} +
+

- {% endif %} - {% bootstrap_field add_form.do layout="control" %} - {% bootstrap_field add_form.itemvar layout="control" %} - {% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %} - {% if add_form.addon_to %} - {% bootstrap_field add_form.addon_to layout="control" %} - {% endif %} - {% if add_form.subevent %} - {% bootstrap_field add_form.subevent layout="control" %} - {% endif %} - {% bootstrap_field add_form.seat layout="control" %} -
+
+
+ {% bootstrap_form_errors add_form %} + {% if add_form.custom_error %} +
+ {{ add_form.custom_error }} +
+ {% endif %} + {% bootstrap_field add_form.itemvar layout="control" %} + {% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %} + {% if add_form.addon_to %} + {% bootstrap_field add_form.addon_to layout="control" %} + {% endif %} + {% if add_form.subevent %} + {% bootstrap_field add_form.subevent layout="control" %} + {% endif %} + {% bootstrap_field add_form.seat layout="control" %} +
+
+
+ {% endfor %}
+ +

+ +

+

diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index a745471a9..c16adb032 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -14,6 +14,7 @@ from django.db import transaction from django.db.models import ( Count, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Subquery, Sum, ) +from django.forms import formset_factory from django.http import ( FileResponse, Http404, HttpResponseNotAllowed, JsonResponse, ) @@ -70,8 +71,8 @@ from pretix.control.forms.filter import ( from pretix.control.forms.orders import ( CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm, - OrderPositionAddForm, OrderPositionChangeForm, OrderRefundForm, - OtherOperationsForm, + OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm, + OrderRefundForm, OtherOperationsForm, ) from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.views import PaginationMixin @@ -1188,9 +1189,16 @@ class OrderChange(OrderView): data=self.request.POST if self.request.method == "POST" else None) @cached_property - def add_form(self): - return OrderPositionAddForm(prefix='add', order=self.order, - data=self.request.POST if self.request.method == "POST" else None) + def add_formset(self): + ff = formset_factory( + OrderPositionAddForm, formset=OrderPositionAddFormset, + can_order=False, can_delete=True, extra=0 + ) + return ff( + prefix='add', + order=self.order, + data=self.request.POST if self.request.method == "POST" else None + ) @cached_property def positions(self): @@ -1208,7 +1216,7 @@ class OrderChange(OrderView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['positions'] = self.positions - ctx['add_form'] = self.add_form + ctx['add_formset'] = self.add_formset ctx['other_form'] = self.other_form return ctx @@ -1221,16 +1229,17 @@ class OrderChange(OrderView): return True def _process_add(self, ocm): - if 'add-do' not in self.request.POST: - return True - if not self.add_form.is_valid(): + if not self.add_formset.is_valid(): return False else: - if self.add_form.cleaned_data['do']: - if '-' in self.add_form.cleaned_data['itemvar']: - itemid, varid = self.add_form.cleaned_data['itemvar'].split('-') + for f in self.add_formset.forms: + if f in self.add_formset.deleted_forms or not f.has_changed(): + continue + + if '-' in f.cleaned_data['itemvar']: + itemid, varid = f.cleaned_data['itemvar'].split('-') else: - itemid, varid = self.add_form.cleaned_data['itemvar'], None + itemid, varid = f.cleaned_data['itemvar'], None item = Item.objects.get(pk=itemid, event=self.request.event) if varid: @@ -1239,12 +1248,12 @@ class OrderChange(OrderView): variation = None try: ocm.add_position(item, variation, - self.add_form.cleaned_data['price'], - self.add_form.cleaned_data.get('addon_to'), - self.add_form.cleaned_data.get('subevent'), - self.add_form.cleaned_data.get('seat')) + f.cleaned_data['price'], + f.cleaned_data.get('addon_to'), + f.cleaned_data.get('subevent'), + f.cleaned_data.get('seat')) except OrderError as e: - self.add_form.custom_error = str(e) + f.custom_error = str(e) return False return True diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index 430cb8358..5b2e4381d 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -1154,9 +1154,12 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { + 'add-TOTAL_FORMS': '0', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', 'op-{}-itemvar'.format(self.op1.pk): str(self.shirt.pk), 'op-{}-price'.format(self.op1.pk): str('12.00'), - 'add-itemvar': str(self.ticket.pk), }) self.op1.refresh_from_db() self.order.refresh_from_db() @@ -1183,9 +1186,11 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { + 'add-TOTAL_FORMS': '0', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', 'op-{}-subevent'.format(self.op1.pk): str(se2.pk), - 'add-itemvar': str(self.ticket.pk), - 'add-subevent': str(se1.pk), }) self.op1.refresh_from_db() self.op2.refresh_from_db() @@ -1197,12 +1202,15 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { + 'add-TOTAL_FORMS': '0', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', 'op-{}-operation'.format(self.op1.pk): 'price', 'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk), 'op-{}-price'.format(self.op1.pk): '24.00', 'op-{}-operation'.format(self.op2.pk): '', 'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk), - 'add-itemvar': str(self.ticket.pk), }) self.op1.refresh_from_db() self.order.refresh_from_db() @@ -1214,8 +1222,11 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { + 'add-TOTAL_FORMS': '0', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', 'op-{}-operation_cancel'.format(self.op1.pk): 'on', - 'add-itemvar': str(self.ticket.pk), }) self.order.refresh_from_db() with scopes_disabled(): @@ -1226,9 +1237,13 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { - 'add-itemvar': str(self.shirt.pk), - 'add-do': 'on', - 'add-price': '14.00', + 'add-TOTAL_FORMS': '1', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', + 'add-0-itemvar': str(self.shirt.pk), + 'add-0-do': 'on', + 'add-0-price': '14.00', }) with scopes_disabled(): assert self.order.positions.count() == 3 @@ -1251,8 +1266,11 @@ class OrderChangeTests(SoupTest): self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.event.organizer.slug, self.event.slug, self.order.code ), { + 'add-TOTAL_FORMS': '0', + 'add-INITIAL_FORMS': '0', + 'add-MIN_NUM_FORMS': '0', + 'add-MAX_NUM_FORMS': '100', 'other-recalculate_taxes': 'on', - 'add-itemvar': str(self.ticket.pk), 'op-{}-operation'.format(self.op1.pk): '', 'op-{}-operation'.format(self.op2.pk): '', 'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),