Order change manager: Allow to add multiple products

This commit is contained in:
Raphael Michel
2019-10-10 12:59:16 +02:00
parent d4d046ca60
commit 4bfe0e3784
4 changed files with 159 additions and 59 deletions

View File

@@ -196,10 +196,6 @@ class OtherOperationsForm(forms.Form):
class OrderPositionAddForm(forms.Form): class OrderPositionAddForm(forms.Form):
do = forms.BooleanField(
label=_('Add a new product to the order'),
required=False
)
itemvar = forms.ChoiceField( itemvar = forms.ChoiceField(
label=_('Product') label=_('Product')
) )
@@ -291,6 +287,28 @@ class OrderPositionAddForm(forms.Form):
change_decimal_field(self.fields['price'], order.event.currency) 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): class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField( itemvar = forms.ChoiceField(
required=False, required=False,

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/event/base.html" %} {% extends "pretixcontrol/event/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load formset_tags %}
{% load money %} {% load money %}
{% block title %} {% block title %}
{% blocktrans trimmed with code=order.code %} {% blocktrans trimmed with code=order.code %}
@@ -64,10 +65,10 @@
{% endif %} {% endif %}
{% if position.addon_to %} {% if position.addon_to %}
<em> <em>
{% blocktrans trimmed with posid=position.addon_to.positionid %} {% blocktrans trimmed with posid=position.addon_to.positionid %}
Add-On to position #{{ posid }} Add-On to position #{{ posid }}
{% endblocktrans %} {% endblocktrans %}
</em> </em>
{% endif %} {% endif %}
</h3> </h3>
</div> </div>
@@ -173,33 +174,87 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title"> <div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
{% trans "Add product" %} {{ add_formset.management_form }}
</h3> {% bootstrap_formset_errors add_formset %}
</div> <div data-formset-body>
<div class="panel-body"> {% for add_form in add_formset %}
<div class="form-horizontal"> <div class="panel panel-default items" data-formset-form>
{% bootstrap_form_errors add_form %} <div class="panel-heading">
{% if add_form.custom_error %} <h3 class="panel-title">
<div class="alert alert-danger"> <button type="button" class="btn btn-danger btn-xs pull-right flip"
{{ add_form.custom_error }} data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add product" %}
<div class="sr-only">
{{ add_form.id }}
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div> </div>
{% endif %} <div class="panel-body">
{% bootstrap_field add_form.do layout="control" %} <div class="form-horizontal">
{% bootstrap_field add_form.itemvar layout="control" %} {% bootstrap_form_errors add_form %}
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %} {% if add_form.custom_error %}
{% if add_form.addon_to %} <div class="alert alert-danger">
{% bootstrap_field add_form.addon_to layout="control" %} {{ add_form.custom_error }}
{% endif %} </div>
{% if add_form.subevent %} {% endif %}
{% bootstrap_field add_form.subevent layout="control" %} {% bootstrap_field add_form.itemvar layout="control" %}
{% endif %} {% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
{% bootstrap_field add_form.seat layout="control" %} {% if add_form.addon_to %}
</div> {% 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" %}
</div>
</div>
</div>
{% endfor %}
</div> </div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default items" data-formset-form>
<div class="panel-heading">
<h3 class="panel-title">
<button type="button" class="btn btn-danger btn-xs pull-right flip"
data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add product" %}
<div class="sr-only">
{{ add_formset.empty_form.id }}
{% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_formset.empty_form.addon_to %}
{% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
{% endif %}
{% if add_formset.empty_form.subevent %}
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
{% endif %}
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add product" %}</button>
</p>
</div> </div>
<div class="panel panel-default items"> <div class="panel panel-default items">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">

View File

@@ -14,6 +14,7 @@ from django.db import transaction
from django.db.models import ( from django.db.models import (
Count, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Subquery, Sum, Count, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Subquery, Sum,
) )
from django.forms import formset_factory
from django.http import ( from django.http import (
FileResponse, Http404, HttpResponseNotAllowed, JsonResponse, FileResponse, Http404, HttpResponseNotAllowed, JsonResponse,
) )
@@ -70,8 +71,8 @@ from pretix.control.forms.filter import (
from pretix.control.forms.orders import ( from pretix.control.forms.orders import (
CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm,
MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm, MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm,
OrderPositionAddForm, OrderPositionChangeForm, OrderRefundForm, OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
OtherOperationsForm, OrderRefundForm, OtherOperationsForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
@@ -1188,9 +1189,16 @@ class OrderChange(OrderView):
data=self.request.POST if self.request.method == "POST" else None) data=self.request.POST if self.request.method == "POST" else None)
@cached_property @cached_property
def add_form(self): def add_formset(self):
return OrderPositionAddForm(prefix='add', order=self.order, ff = formset_factory(
data=self.request.POST if self.request.method == "POST" else None) 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 @cached_property
def positions(self): def positions(self):
@@ -1208,7 +1216,7 @@ class OrderChange(OrderView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['positions'] = self.positions ctx['positions'] = self.positions
ctx['add_form'] = self.add_form ctx['add_formset'] = self.add_formset
ctx['other_form'] = self.other_form ctx['other_form'] = self.other_form
return ctx return ctx
@@ -1221,16 +1229,17 @@ class OrderChange(OrderView):
return True return True
def _process_add(self, ocm): def _process_add(self, ocm):
if 'add-do' not in self.request.POST: if not self.add_formset.is_valid():
return True
if not self.add_form.is_valid():
return False return False
else: else:
if self.add_form.cleaned_data['do']: for f in self.add_formset.forms:
if '-' in self.add_form.cleaned_data['itemvar']: if f in self.add_formset.deleted_forms or not f.has_changed():
itemid, varid = self.add_form.cleaned_data['itemvar'].split('-') continue
if '-' in f.cleaned_data['itemvar']:
itemid, varid = f.cleaned_data['itemvar'].split('-')
else: 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) item = Item.objects.get(pk=itemid, event=self.request.event)
if varid: if varid:
@@ -1239,12 +1248,12 @@ class OrderChange(OrderView):
variation = None variation = None
try: try:
ocm.add_position(item, variation, ocm.add_position(item, variation,
self.add_form.cleaned_data['price'], f.cleaned_data['price'],
self.add_form.cleaned_data.get('addon_to'), f.cleaned_data.get('addon_to'),
self.add_form.cleaned_data.get('subevent'), f.cleaned_data.get('subevent'),
self.add_form.cleaned_data.get('seat')) f.cleaned_data.get('seat'))
except OrderError as e: except OrderError as e:
self.add_form.custom_error = str(e) f.custom_error = str(e)
return False return False
return True return True

View File

@@ -1154,9 +1154,12 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code 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-{}-itemvar'.format(self.op1.pk): str(self.shirt.pk),
'op-{}-price'.format(self.op1.pk): str('12.00'), 'op-{}-price'.format(self.op1.pk): str('12.00'),
'add-itemvar': str(self.ticket.pk),
}) })
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
@@ -1183,9 +1186,11 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code 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), '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.op1.refresh_from_db()
self.op2.refresh_from_db() self.op2.refresh_from_db()
@@ -1197,12 +1202,15 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code 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-{}-operation'.format(self.op1.pk): 'price',
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk), 'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op1.pk): '24.00', 'op-{}-price'.format(self.op1.pk): '24.00',
'op-{}-operation'.format(self.op2.pk): '', 'op-{}-operation'.format(self.op2.pk): '',
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk), 'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),
'add-itemvar': str(self.ticket.pk),
}) })
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
@@ -1214,8 +1222,11 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code 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', 'op-{}-operation_cancel'.format(self.op1.pk): 'on',
'add-itemvar': str(self.ticket.pk),
}) })
self.order.refresh_from_db() self.order.refresh_from_db()
with scopes_disabled(): with scopes_disabled():
@@ -1226,9 +1237,13 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code self.event.organizer.slug, self.event.slug, self.order.code
), { ), {
'add-itemvar': str(self.shirt.pk), 'add-TOTAL_FORMS': '1',
'add-do': 'on', 'add-INITIAL_FORMS': '0',
'add-price': '14.00', '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(): with scopes_disabled():
assert self.order.positions.count() == 3 assert self.order.positions.count() == 3
@@ -1251,8 +1266,11 @@ class OrderChangeTests(SoupTest):
self.client.post('/control/event/{}/{}/orders/{}/change'.format( self.client.post('/control/event/{}/{}/orders/{}/change'.format(
self.event.organizer.slug, self.event.slug, self.order.code 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', 'other-recalculate_taxes': 'on',
'add-itemvar': str(self.ticket.pk),
'op-{}-operation'.format(self.op1.pk): '', 'op-{}-operation'.format(self.op1.pk): '',
'op-{}-operation'.format(self.op2.pk): '', 'op-{}-operation'.format(self.op2.pk): '',
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk), 'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),