forked from CGM_Public/pretix_original
Change semantics of changing orders (#1260)
* Change semantics of changing orders This basically does two things to the "Change products" view of orders and the OrderChangeManager program API: 1) It decouples changing items or subevents from changing prices. OrderChangeManager.change_item() and .change_subevent() no longer touch the price of a position. Instead .change_price() needs to be called explicitly. However, a client-side JavaScript component now *proposes* a new price based on the changed item or subevent. 2) The user interface now exposes the possibility of doing multiple things at the same time, i.e. changing the item, subevent and price in the same operation. OrderChangeManager already allowed this before. (1) is basically a consequence of (2), while (2) is a prerequesite for e.g. the `seating` branch, where changing the subevent will always require changing the seat. * Add tests for price calculation API
This commit is contained in:
@@ -9,9 +9,7 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -150,15 +148,6 @@ class CommentForm(I18nModelForm):
|
||||
}
|
||||
|
||||
|
||||
class SubEventChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
p = get_price(self.instance.item, self.instance.variation,
|
||||
voucher=self.instance.voucher,
|
||||
subevent=obj)
|
||||
return '{} – {} ({})'.format(obj.name, obj.get_date_range_display(),
|
||||
p.print(self.instance.order.event.currency))
|
||||
|
||||
|
||||
class OtherOperationsForm(forms.Form):
|
||||
recalculate_taxes = forms.BooleanField(
|
||||
label=_('Re-calculate taxes'),
|
||||
@@ -265,12 +254,13 @@ class OrderPositionAddForm(forms.Form):
|
||||
|
||||
|
||||
class OrderPositionChangeForm(forms.Form):
|
||||
itemvar = forms.ChoiceField()
|
||||
subevent = SubEventChoiceField(
|
||||
itemvar = forms.ChoiceField(
|
||||
required=False,
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
SubEvent.objects.none(),
|
||||
label=pgettext_lazy('subevent', 'New date'),
|
||||
required=True,
|
||||
empty_label=None
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
@@ -278,53 +268,49 @@ class OrderPositionChangeForm(forms.Form):
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
operation = forms.ChoiceField(
|
||||
operation_secret = forms.BooleanField(
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('product', 'Change product'),
|
||||
('price', 'Change price'),
|
||||
('subevent', 'Change event date'),
|
||||
('cancel', 'Remove product'),
|
||||
('split', 'Split into new order'),
|
||||
('secret', 'Regenerate secret'),
|
||||
)
|
||||
label=_('Generate a new secret')
|
||||
)
|
||||
operation_cancel = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Cancel this position')
|
||||
)
|
||||
operation_split = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Split into new order')
|
||||
)
|
||||
change_product_keep_price = forms.BooleanField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.pop('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
try:
|
||||
ia = instance.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
|
||||
if instance:
|
||||
try:
|
||||
if instance.variation:
|
||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||
elif instance.item:
|
||||
initial['itemvar'] = str(instance.item.pk)
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
|
||||
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
|
||||
initial['price'] = instance.price - instance.tax_value
|
||||
else:
|
||||
initial['price'] = instance.price
|
||||
initial['subevent'] = instance.subevent
|
||||
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
|
||||
initial['price'] = instance.price - instance.tax_value
|
||||
else:
|
||||
initial['price'] = instance.price
|
||||
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
if instance.order.event.has_subevents:
|
||||
self.fields['subevent'].instance = instance
|
||||
self.fields['subevent'].queryset = instance.order.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': instance.order.event.slug,
|
||||
'organizer': instance.order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('(Unchanged)')
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = []
|
||||
choices = [
|
||||
('', _('(Unchanged)'))
|
||||
]
|
||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||
pname = str(i)
|
||||
if not i.is_available():
|
||||
@@ -333,14 +319,10 @@ class OrderPositionChangeForm(forms.Form):
|
||||
|
||||
if variations:
|
||||
for v in variations:
|
||||
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s (%s)' % (pname, v.value, p.print(instance.order.event.currency))))
|
||||
'%s – %s' % (pname, v.value)))
|
||||
else:
|
||||
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency))))
|
||||
choices.append((str(i.pk), pname))
|
||||
self.fields['itemvar'].choices = choices
|
||||
change_decimal_field(self.fields['price'], instance.order.event.currency)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
@@ -71,92 +72,87 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-inline form-order-change">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}">
|
||||
{% bootstrap_form_errors position.form %}
|
||||
{% if position.custom_error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ position.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="help-block">
|
||||
{% trans "Ticket secret:" %} {{ position.secret|slice:":12" }}…
|
||||
</p>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value=""
|
||||
{% if not position.form.operation.value %}checked="checked"{% endif %}>
|
||||
{% trans "Keep unchanged" %}
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-sm-offset-3">
|
||||
<strong>{% trans "Current value" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<strong>{% trans "Change to" %}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="product"
|
||||
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}>
|
||||
{% trans "Change product to" %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Product" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.item }}
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.itemvar layout='inline' %}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
{{ position.form.change_product_keep_price }}
|
||||
{% trans "Keep price the same" %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="subevent"
|
||||
{% if position.form.operation.value == "subevent" %}checked="checked"{% endif %}>
|
||||
{% trans "Change date to" context "subevent" %}
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Date" context "subevent" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.subevent }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.subevent layout='inline' %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="price"
|
||||
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
|
||||
{% trans "Change price to" %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Price" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.price|money:request.event.currency }}<br>
|
||||
{% if position.tax_rate %}
|
||||
<small>{% blocktrans trimmed with rate=position.tax_rate name=position.tax_rule.name %}
|
||||
<strong>incl.</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 field-container">
|
||||
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
|
||||
{% if position.apply_tax %}
|
||||
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
|
||||
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
|
||||
<strong>plus</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
{% elif position.item.tax_rule %}
|
||||
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
|
||||
<strong>incl.</strong> {{ rate }}% {{ name }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% trans "no taxes apply" %}
|
||||
{% endif %}
|
||||
</label>
|
||||
<small><strong>{% trans "including all taxes" %}</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="split"
|
||||
{% if position.form.operation.value == "split" %}checked="checked"{% endif %}>
|
||||
{% trans "Split into new order" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="secret"
|
||||
{% if position.form.operation.value == "secret" %}checked="checked"{% endif %}>
|
||||
{% trans "Generate a new secret" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel"
|
||||
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}>
|
||||
{% trans "Cancel position" %}
|
||||
{% if position.addons.exists %}
|
||||
<em class="text-danger">
|
||||
{% trans "Removing this position will also remove all add-ons to this position." %}
|
||||
</em>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Ticket secret" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.secret|slice:":12" }}…
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field position.form.operation_cancel layout='inline' %}
|
||||
{% bootstrap_field position.form.operation_split layout='inline' %}
|
||||
{% if position.addons.exists %}
|
||||
<em class="text-danger">
|
||||
{% trans "Removing this position will also remove all add-ons to this position." %}
|
||||
</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1240,7 +1240,11 @@ class OrderChange(OrderView):
|
||||
return False
|
||||
|
||||
try:
|
||||
if p.form.cleaned_data['operation'] == 'product':
|
||||
if p.form.cleaned_data['operation_cancel']:
|
||||
ocm.cancel(p)
|
||||
continue
|
||||
|
||||
if p.form.cleaned_data['itemvar']:
|
||||
if '-' in p.form.cleaned_data['itemvar']:
|
||||
itemid, varid = p.form.cleaned_data['itemvar'].split('-')
|
||||
else:
|
||||
@@ -1251,16 +1255,19 @@ class OrderChange(OrderView):
|
||||
variation = ItemVariation.objects.get(pk=varid, item=item)
|
||||
else:
|
||||
variation = None
|
||||
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price'])
|
||||
elif p.form.cleaned_data['operation'] == 'price':
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
elif p.form.cleaned_data['operation'] == 'subevent':
|
||||
if item != p.item or variation != p.variation:
|
||||
ocm.change_item(p, item, variation)
|
||||
|
||||
if self.request.event.has_subevents and p.form.cleaned_data['subevent'] and p.form.cleaned_data['subevent'] != p.subevent:
|
||||
ocm.change_subevent(p, p.form.cleaned_data['subevent'])
|
||||
elif p.form.cleaned_data['operation'] == 'cancel':
|
||||
ocm.cancel(p)
|
||||
elif p.form.cleaned_data['operation'] == 'split':
|
||||
|
||||
if p.form.cleaned_data['price'] != p.price:
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
|
||||
if p.form.cleaned_data['operation_split']:
|
||||
ocm.split(p)
|
||||
elif p.form.cleaned_data['operation'] == 'secret':
|
||||
|
||||
if p.form.cleaned_data['operation_secret']:
|
||||
ocm.regenerate_secret(p)
|
||||
|
||||
except OrderError as e:
|
||||
|
||||
Reference in New Issue
Block a user