Compare commits

...

2 Commits

Author SHA1 Message Date
Raphael Michel
1279c8720f Add tests for price calculation API 2019-04-22 11:14:33 +02:00
Raphael Michel
7a3afde7b1 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.
2019-04-21 23:02:32 +02:00
13 changed files with 531 additions and 251 deletions

View File

@@ -14,8 +14,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition, Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
Question, QuestionAnswer, OrderPosition, Question, QuestionAnswer, SubEvent,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund, CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -288,6 +288,23 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance return instance
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
else:
del self.fields['subevent']
class AnswerCreateSerializer(I18nAwareModelSerializer): class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta: class Meta:

View File

@@ -24,11 +24,12 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.order import ( from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer, InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer, OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
) )
from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order, CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken, Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
generate_position_secret, generate_secret, generate_position_secret, generate_secret,
) )
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
@@ -42,10 +43,12 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order, OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded, extend_order, mark_order_expired, mark_order_refunded,
) )
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
from pretix.base.signals import ( from pretix.base.signals import (
order_modified, order_placed, register_ticket_outputs, order_modified, order_placed, register_ticket_outputs,
) )
from pretix.base.templatetags.money import money_filter
class OrderFilter(FilterSet): class OrderFilter(FilterSet):
@@ -622,6 +625,83 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
return prov return prov
raise NotFound('Unknown output provider.') raise NotFound('Unknown output provider.')
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""
This calculates the price assuming a change of product or subevent. This endpoint
is deliberately not documented and considered a private API, only to be used by
pretix' web interface.
Sample input:
{
"item": 2,
"variation": null,
"subevent": 3
}
Sample output:
{
"gross": "2.34",
"gross_formatted": "2,34",
"net": "2.34",
"tax": "0.00",
"rate": "0.00",
"name": "VAT"
}
"""
serializer = PriceCalcSerializer(data=request.data, event=request.event)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
pos = self.get_object()
try:
ia = pos.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress()
kwargs = {
'item': pos.item,
'variation': pos.variation,
'voucher': pos.voucher,
'subevent': pos.subevent,
'addon_to': pos.addon_to,
'invoice_address': ia,
}
if data.get('item'):
item = data.get('item')
kwargs['item'] = item
if item.has_variations:
variation = data.get('variation') or pos.variation
if not variation:
raise ValidationError('No variation given')
if variation.item != item:
raise ValidationError('Variation does not belong to item')
kwargs['variation'] = variation
else:
variation = None
kwargs['variation'] = None
if pos.voucher and not pos.voucher.applies_to(item, variation):
kwargs['voucher'] = None
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
price = get_price(**kwargs)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
'net': price.net,
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)') @action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs): def download(self, request, output, **kwargs):
provider = self._get_output_provider(output) provider = self._get_output_provider(output)

View File

@@ -846,8 +846,8 @@ class OrderChangeManager:
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'), 'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': _('You need to choose a subevent for the new position.'), 'subevent_required': _('You need to choose a subevent for the new position.'),
} }
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price')) ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent', 'price')) SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price')) PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
CancelOperation = namedtuple('CancelOperation', ('position',)) CancelOperation = namedtuple('CancelOperation', ('position',))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent')) AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
@@ -867,33 +867,18 @@ class OrderChangeManager:
self.notify = notify self.notify = notify
self._invoice_dirty = False self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation], keep_price=False): def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation']) raise OrderError(self.error_messages['product_without_variation'])
if keep_price:
price = TaxedPrice(gross=position.price, net=position.price - position.tax_value,
tax=position.tax_value, rate=position.tax_rate,
name=position.tax_rule.name if position.tax_rule else None)
else:
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (variation.quotas.filter(subevent=position.subevent) new_quotas = (variation.quotas.filter(subevent=position.subevent)
if variation else item.quotas.filter(subevent=position.subevent)) if variation else item.quotas.filter(subevent=position.subevent))
if not new_quotas: if not new_quotas:
raise OrderError(self.error_messages['quota_missing']) raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff += price.gross - position.price
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
self._operations.append(self.ItemOperation(position, item, variation, price)) self._operations.append(self.ItemOperation(position, item, variation))
def change_subevent(self, position: OrderPosition, subevent: SubEvent): def change_subevent(self, position: OrderPosition, subevent: SubEvent):
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent, price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
@@ -907,19 +892,15 @@ class OrderChangeManager:
if not new_quotas: if not new_quotas:
raise OrderError(self.error_messages['quota_missing']) raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff += price.gross - position.price
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
self._operations.append(self.SubeventOperation(position, subevent, price)) self._operations.append(self.SubeventOperation(position, subevent))
def regenerate_secret(self, position: OrderPosition): def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position)) self._operations.append(self.RegenerateSecretOperation(position))
def change_price(self, position: OrderPosition, price: Decimal): def change_price(self, position: OrderPosition, price: Decimal):
price = position.item.tax(price) price = position.item.tax(price, base_price_is='gross')
self._totaldiff += price.gross - position.price self._totaldiff += price.gross - position.price
@@ -1076,14 +1057,11 @@ class OrderChangeManager:
'new_variation': op.variation.pk if op.variation else None, 'new_variation': op.variation.pk if op.variation else None,
'old_price': op.position.price, 'old_price': op.position.price,
'addon_to': op.position.addon_to_id, 'addon_to': op.position.addon_to_id,
'new_price': op.price.gross 'new_price': op.position.price
}) })
op.position.item = op.item op.position.item = op.item
op.position.variation = op.variation op.position.variation = op.variation
op.position.price = op.price.gross op.position._calculate_tax()
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_rule = op.item.tax_rule
op.position.save() op.position.save()
elif isinstance(op, self.SubeventOperation): elif isinstance(op, self.SubeventOperation):
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={ self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
@@ -1092,13 +1070,9 @@ class OrderChangeManager:
'old_subevent': op.position.subevent.pk, 'old_subevent': op.position.subevent.pk,
'new_subevent': op.subevent.pk, 'new_subevent': op.subevent.pk,
'old_price': op.position.price, 'old_price': op.position.price,
'new_price': op.price.gross 'new_price': op.position.price
}) })
op.position.subevent = op.subevent op.position.subevent = op.subevent
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_rule = op.position.item.tax_rule
op.position.save() op.position.save()
elif isinstance(op, self.PriceOperation): elif isinstance(op, self.PriceOperation):
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={ self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
@@ -1109,9 +1083,7 @@ class OrderChangeManager:
'new_price': op.price.gross 'new_price': op.price.gross
}) })
op.position.price = op.price.gross op.position.price = op.price.gross
op.position.tax_rate = op.price.rate op.position._calculate_tax()
op.position.tax_value = op.price.tax
op.position.tax_rule = op.position.item.tax_rule
op.position.save() op.position.save()
elif isinstance(op, self.CancelOperation): elif isinstance(op, self.CancelOperation):
for opa in op.position.addons.all(): for opa in op.position.addons.all():

View File

@@ -9,9 +9,7 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.models import ( from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
)
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.control.forms.widgets import Select2 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): class OtherOperationsForm(forms.Form):
recalculate_taxes = forms.BooleanField( recalculate_taxes = forms.BooleanField(
label=_('Re-calculate taxes'), label=_('Re-calculate taxes'),
@@ -265,12 +254,13 @@ class OrderPositionAddForm(forms.Form):
class OrderPositionChangeForm(forms.Form): class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField() itemvar = forms.ChoiceField(
subevent = SubEventChoiceField( required=False,
)
subevent = forms.ModelChoiceField(
SubEvent.objects.none(), SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'New date'), required=False,
required=True, empty_label=_('(Unchanged)')
empty_label=None
) )
price = forms.DecimalField( price = forms.DecimalField(
required=False, required=False,
@@ -278,53 +268,49 @@ class OrderPositionChangeForm(forms.Form):
localize=True, localize=True,
label=_('New price (gross)') label=_('New price (gross)')
) )
operation = forms.ChoiceField( operation_secret = forms.BooleanField(
required=False, required=False,
widget=forms.RadioSelect, label=_('Generate a new secret')
choices=(
('product', 'Change product'),
('price', 'Change price'),
('subevent', 'Change event date'),
('cancel', 'Remove product'),
('split', 'Split into new order'),
('secret', 'Regenerate 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): def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance') instance = kwargs.pop('instance')
initial = kwargs.get('initial', {}) 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: if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value initial['price'] = instance.price - instance.tax_value
else: else:
initial['price'] = instance.price initial['price'] = instance.price
initial['subevent'] = instance.subevent
kwargs['initial'] = initial kwargs['initial'] = initial
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if instance.order.event.has_subevents: if instance.order.event.has_subevents:
self.fields['subevent'].instance = instance
self.fields['subevent'].queryset = instance.order.event.subevents.all() 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: else:
del self.fields['subevent'] del self.fields['subevent']
choices = [] choices = [
('', _('(Unchanged)'))
]
for i in instance.order.event.items.prefetch_related('variations').all(): for i in instance.order.event.items.prefetch_related('variations').all():
pname = str(i) pname = str(i)
if not i.is_available(): if not i.is_available():
@@ -333,14 +319,10 @@ class OrderPositionChangeForm(forms.Form):
if variations: if variations:
for v in 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), 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: else:
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent, choices.append((str(i.pk), pname))
invoice_address=ia)
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency))))
self.fields['itemvar'].choices = choices self.fields['itemvar'].choices = choices
change_decimal_field(self.fields['price'], instance.order.event.currency) change_decimal_field(self.fields['price'], instance.order.event.currency)

View File

@@ -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/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.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/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/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.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> <script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>

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 money %}
{% block title %} {% block title %}
{% blocktrans trimmed with code=order.code %} {% blocktrans trimmed with code=order.code %}
Change order: {{ code }} Change order: {{ code }}
@@ -71,92 +72,87 @@
</h3> </h3>
</div> </div>
<div class="panel-body"> <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 %} {% bootstrap_form_errors position.form %}
{% if position.custom_error %} {% if position.custom_error %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{{ position.custom_error }} {{ position.custom_error }}
</div> </div>
{% endif %} {% endif %}
<p class="help-block"> <div class="row">
{% trans "Ticket secret:" %} {{ position.secret|slice:":12" }}… <div class="col-sm-5 col-sm-offset-3">
</p> <strong>{% trans "Current value" %}</strong>
<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> </div>
<div class="radio"> <div class="col-sm-4">
<label> <strong>{% trans "Change to" %}</strong>
<input name="{{ position.form.prefix }}-operation" type="radio" value="product" </div>
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}> </div>
{% 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' %} {% 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>
</div>
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<div class="radio"> <div class="row">
<label> <div class="col-sm-3">
<input name="{{ position.form.prefix }}-operation" type="radio" value="subevent" <strong>{% trans "Date" context "subevent" %}</strong>
{% if position.form.operation.value == "subevent" %}checked="checked"{% endif %}> </div>
{% trans "Change date to" context "subevent" %} <div class="col-sm-5">
{{ position.subevent }}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.subevent layout='inline' %} {% bootstrap_field position.form.subevent layout='inline' %}
</label> </div>
</div> </div>
{% endif %} {% endif %}
<div class="radio">
<label> <div class="row">
<input name="{{ position.form.prefix }}-operation" type="radio" value="price" <div class="col-sm-3">
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}> <strong>{% trans "Price" %}</strong>
{% trans "Change price to" %} </div>
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %} <div class="col-sm-5">
{% if position.apply_tax %} {{ position.price|money:request.event.currency }}<br>
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %} {% if position.tax_rate %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %} <small>{% blocktrans trimmed with rate=position.tax_rate name=position.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 }} <strong>incl.</strong> {{ rate }}% {{ name }}
{% endblocktrans %} {% endblocktrans %}</small>
{% endif %} {% endif %}
{% else %}
{% trans "no taxes apply" %}
{% endif %}
</label>
</div> </div>
<div class="radio"> <div class="col-sm-4 field-container">
<label> {% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
<input name="{{ position.form.prefix }}-operation" type="radio" value="split" <small><strong>{% trans "including all taxes" %}</strong></small>
{% if position.form.operation.value == "split" %}checked="checked"{% endif %}>
{% trans "Split into new order" %}
</label>
</div> </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>
<div class="radio">
<label> <div class="row">
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel" <div class="col-sm-3">
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}> <strong>{% trans "Ticket secret" %}</strong>
{% trans "Cancel position" %} </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 %} {% if position.addons.exists %}
<em class="text-danger"> <em class="text-danger">
{% trans "Removing this position will also remove all add-ons to this position." %} {% trans "Removing this position will also remove all add-ons to this position." %}
</em> </em>
{% endif %} {% endif %}
</label>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1240,7 +1240,11 @@ class OrderChange(OrderView):
return False return False
try: 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']: if '-' in p.form.cleaned_data['itemvar']:
itemid, varid = p.form.cleaned_data['itemvar'].split('-') itemid, varid = p.form.cleaned_data['itemvar'].split('-')
else: else:
@@ -1251,16 +1255,19 @@ class OrderChange(OrderView):
variation = ItemVariation.objects.get(pk=varid, item=item) variation = ItemVariation.objects.get(pk=varid, item=item)
else: else:
variation = None variation = None
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price']) if item != p.item or variation != p.variation:
elif p.form.cleaned_data['operation'] == 'price': ocm.change_item(p, item, variation)
ocm.change_price(p, p.form.cleaned_data['price'])
elif p.form.cleaned_data['operation'] == 'subevent': 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']) ocm.change_subevent(p, p.form.cleaned_data['subevent'])
elif p.form.cleaned_data['operation'] == 'cancel':
ocm.cancel(p) if p.form.cleaned_data['price'] != p.price:
elif p.form.cleaned_data['operation'] == 'split': ocm.change_price(p, p.form.cleaned_data['price'])
if p.form.cleaned_data['operation_split']:
ocm.split(p) ocm.split(p)
elif p.form.cleaned_data['operation'] == 'secret':
if p.form.cleaned_data['operation_secret']:
ocm.regenerate_secret(p) ocm.regenerate_secret(p)
except OrderError as e: except OrderError as e:

View File

@@ -0,0 +1,51 @@
/*global $, gettext*/
$(function () {
// Question view
if (!$(".form-order-change").length) {
return;
}
$(".form-order-change").each(function () {
var url = $(this).attr("data-pricecalc-endpoint");
var $itemvar = $(this).find("[name*=itemvar]");
var $subevent = $(this).find("[name*=subevent]");
var $price = $(this).find("[name*=price]");
var update_price = function () {
console.log(url);
var itemvar = $itemvar.val();
var item = null;
var variation = null;
if (itemvar.indexOf("-")) {
item = parseInt(itemvar.split("-")[0]);
variation = parseInt(itemvar.split("-")[1]);
} else {
item = parseInt(itemvar);
}
$price.closest(".field-container").append("<small class=\"loading-indicator\"><span class=\"fa fa-cog fa-spin\"></span> " +
gettext("Calculating default price…") + "</small>");
$.ajax(
{
'type': 'POST',
'url': url,
'headers': {'X-CSRFToken': $("input[name=csrfmiddlewaretoken]").val()},
'data': JSON.stringify({
'item': item,
'variation': variation,
'subevent': $subevent.val(),
'locale': $("body").attr("data-pretixlocale"),
}),
'contentType': "application/json",
'success': function (data) {
$price.val(data.gross_formatted);
$price.closest(".field-container").find(".loading-indicator").remove();
},
// 'error': …
'context': this,
'dataType': 'json',
'timeout': 30000
}
);
};
$itemvar.on("change", update_price);
$subevent.on("change", update_price);
});
});

View File

@@ -407,6 +407,12 @@ table td > .checkbox input[type="checkbox"] {
} }
} }
.form-order-change {
.row {
padding: 5px 0;
}
}
@media(max-width: $screen-xs-max) { @media(max-width: $screen-xs-max) {
.nameparts-form-group { .nameparts-form-group {
display: block; display: block;
@@ -455,3 +461,4 @@ table td > .checkbox input[type="checkbox"] {
} }
} }
} }

View File

@@ -2798,3 +2798,222 @@ def test_order_resend_link(token_client, organizer, event, order):
), format='json', data={} ), format='json', data={}
) )
assert resp.status_code == 400 assert resp.status_code == 400
@pytest.mark.django_db
def test_orderposition_price_calculation(token_client, organizer, event, order, item):
op = order.positions.first()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('23.00'),
'gross_formatted': '23.00',
'name': '',
'net': Decimal('23.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_item_with_tax(token_client, organizer, event, order, item, taxrule):
item2 = event.items.create(name="Budget Ticket", default_price=23, tax_rule=taxrule)
op = order.positions.first()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('23.00'),
'gross_formatted': '23.00',
'name': '',
'net': Decimal('19.33'),
'rate': Decimal('19.00'),
'tax': Decimal('3.67')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_item_with_variation(token_client, organizer, event, order):
item2 = event.items.create(name="Budget Ticket", default_price=23)
var = item2.variations.create(default_price=12, value="XS")
op = order.positions.first()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
'variation': var.pk
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('12.00'),
'gross_formatted': '12.00',
'name': '',
'net': Decimal('12.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_subevent(token_client, organizer, event, order, subevent):
item2 = event.items.create(name="Budget Ticket", default_price=23)
op = order.positions.first()
op.subevent = subevent
op.save()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
'subevent': subevent.pk
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('23.00'),
'gross_formatted': '23.00',
'name': '',
'net': Decimal('23.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_subevent_with_override(token_client, organizer, event, order, subevent):
item2 = event.items.create(name="Budget Ticket", default_price=23)
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
se2.subeventitem_set.create(item=item2, price=12)
op = order.positions.first()
op.subevent = subevent
op.save()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
'subevent': se2.pk
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('12.00'),
'gross_formatted': '12.00',
'name': '',
'net': Decimal('12.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_voucher_matching(token_client, organizer, event, order, subevent, item):
item2 = event.items.create(name="Budget Ticket", default_price=23)
q = event.quotas.create(name="Quota")
q.items.add(item)
q.items.add(item2)
voucher = event.vouchers.create(price_mode="set", value=15, quota=q)
op = order.positions.first()
op.voucher = voucher
op.save()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('15.00'),
'gross_formatted': '15.00',
'name': '',
'net': Decimal('15.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_voucher_not_matching(token_client, organizer, event, order, subevent, item):
item2 = event.items.create(name="Budget Ticket", default_price=23)
q = event.quotas.create(name="Quota")
q.items.add(item)
voucher = event.vouchers.create(price_mode="set", value=15, quota=q)
op = order.positions.first()
op.voucher = voucher
op.save()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('23.00'),
'gross_formatted': '23.00',
'name': '',
'net': Decimal('23.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_net_price(token_client, organizer, event, order, subevent, item, taxrule):
taxrule.price_includes_tax = False
taxrule.save()
item2 = event.items.create(name="Budget Ticket", default_price=10, tax_rule=taxrule)
op = order.positions.first()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('11.90'),
'gross_formatted': '11.90',
'name': '',
'net': Decimal('10.00'),
'rate': Decimal('19.00'),
'tax': Decimal('1.90')
}
@pytest.mark.django_db
def test_orderposition_price_calculation_reverse_charge(token_client, organizer, event, order, subevent, item, taxrule):
taxrule.price_includes_tax = False
taxrule.eu_reverse_charge = True
taxrule.home_country = Country('DE')
taxrule.save()
order.invoice_address.is_business = True
order.invoice_address.vat_id = 'ATU1234567'
order.invoice_address.vat_id_validated = True
order.invoice_address.country = Country('AT')
order.invoice_address.save()
item2 = event.items.create(name="Budget Ticket", default_price=10, tax_rule=taxrule)
op = order.positions.first()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/price_calc/'.format(organizer.slug, event.slug, op.pk),
data={
'item': item2.pk,
}
)
assert resp.status_code == 200
assert resp.data == {
'gross': Decimal('10.00'),
'gross_formatted': '10.00',
'name': '',
'net': Decimal('10.00'),
'rate': Decimal('0.00'),
'tax': Decimal('0.00')
}

View File

@@ -25,6 +25,7 @@ event_permission_sub_urls = [
('get', 'can_view_orders', 'orders/', 200), ('get', 'can_view_orders', 'orders/', 200),
('get', 'can_view_orders', 'orderpositions/', 200), ('get', 'can_view_orders', 'orderpositions/', 200),
('delete', 'can_change_orders', 'orderpositions/1/', 404), ('delete', 'can_change_orders', 'orderpositions/1/', 404),
('post', 'can_change_orders', 'orderpositions/1/price_calc/', 404),
('get', 'can_view_vouchers', 'vouchers/', 200), ('get', 'can_view_vouchers', 'vouchers/', 200),
('get', 'can_view_orders', 'invoices/', 200), ('get', 'can_view_orders', 'invoices/', 200),
('get', 'can_view_orders', 'invoices/1/', 404), ('get', 'can_view_orders', 'invoices/1/', 404),

View File

@@ -611,49 +611,27 @@ class OrderChangeManagerTests(TestCase):
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.op1.subevent == se2 assert self.op1.subevent == se2
assert self.op1.price == 12 assert self.op1.price == Decimal('23.00')
assert self.order.total == self.op1.price + self.op2.price assert self.order.total == self.op1.price + self.op2.price
def test_change_subevent_reverse_charge(self): def test_change_subevent_with_price_success(self):
self._enable_reverse_charge()
self.event.has_subevents = True self.event.has_subevents = True
self.event.save() self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now()) se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now()) se2 = self.event.subevents.create(name="Bar", date_from=now())
SubEventItem.objects.create(subevent=se2, item=self.ticket, price=10.7) SubEventItem.objects.create(subevent=se2, item=self.ticket, price=12)
self.op1.subevent = se1 self.op1.subevent = se1
self.op1.save() self.op1.save()
self.quota.subevent = se2 self.quota.subevent = se2
self.quota.save() self.quota.save()
self.ocm.change_subevent(self.op1, se2) self.ocm.change_subevent(self.op1, se2)
self.ocm.change_price(self.op1, Decimal('12.00'))
self.ocm.commit() self.ocm.commit()
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.op1.subevent == se2 assert self.op1.subevent == se2
assert self.op1.price == Decimal('10.00') assert self.op1.price == Decimal('12.00')
assert self.op1.tax_value == Decimal('0.00')
assert self.order.total == self.op1.price + self.op2.price
def test_change_subevent_net_price(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now())
self.tr7.price_includes_tax = False
self.tr7.save()
SubEventItem.objects.create(subevent=se2, item=self.ticket, price=10)
self.op1.subevent = se1
self.op1.save()
self.quota.subevent = se2
self.quota.save()
self.ocm.change_subevent(self.op1, se2)
self.ocm.commit()
self.op1.refresh_from_db()
self.order.refresh_from_db()
assert self.op1.subevent == se2
assert self.op1.price == Decimal('10.70')
assert self.order.total == self.op1.price + self.op2.price assert self.order.total == self.op1.price + self.op2.price
def test_change_subevent_sold_out(self): def test_change_subevent_sold_out(self):
@@ -680,14 +658,14 @@ class OrderChangeManagerTests(TestCase):
def test_change_item_keep_price(self): def test_change_item_keep_price(self):
p = self.op1.price p = self.op1.price
tv = self.op1.tax_value self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.change_item(self.op1, self.shirt, None, keep_price=True)
self.ocm.commit() self.ocm.commit()
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.op1.item == self.shirt assert self.op1.item == self.shirt
assert self.op1.price == p assert self.op1.price == p
assert self.op1.tax_value == tv assert self.op1.tax_value == Decimal('3.67')
assert self.op1.tax_rule == self.shirt.tax_rule
def test_change_item_success(self): def test_change_item_success(self):
self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op1, self.shirt, None)
@@ -695,36 +673,23 @@ class OrderChangeManagerTests(TestCase):
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.op1.item == self.shirt assert self.op1.item == self.shirt
assert self.op1.price == self.shirt.default_price assert self.op1.price == Decimal('23.00')
assert self.op1.tax_rate == self.shirt.tax_rule.rate assert self.op1.tax_rate == self.shirt.tax_rule.rate
assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value
assert self.order.total == self.op1.price + self.op2.price assert self.order.total == self.op1.price + self.op2.price
def test_change_item_net_price_success(self): def test_change_item_with_price_success(self):
self.tr19.price_includes_tax = False
self.tr19.save()
self.ocm.change_item(self.op1, self.shirt, None) self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.change_price(self.op1, Decimal('12.00'))
self.ocm.commit() self.ocm.commit()
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.op1.item == self.shirt assert self.op1.item == self.shirt
assert self.op1.price == Decimal('14.28') assert self.op1.price == Decimal('12.00')
assert self.op1.tax_rate == self.shirt.tax_rule.rate assert self.op1.tax_rate == self.shirt.tax_rule.rate
assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value
assert self.order.total == self.op1.price + self.op2.price assert self.order.total == self.op1.price + self.op2.price
def test_change_item_reverse_charge(self):
self._enable_reverse_charge()
self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.commit()
self.op1.refresh_from_db()
self.order.refresh_from_db()
assert self.op1.item == self.shirt
assert self.op1.price == Decimal('10.08')
assert self.op1.tax_rate == Decimal('0.00')
assert self.op1.tax_value == Decimal('0.00')
assert self.order.total == self.op1.price + self.op2.price
def test_change_price_success(self): def test_change_price_success(self):
self.ocm.change_price(self.op1, Decimal('24.00')) self.ocm.change_price(self.op1, Decimal('24.00'))
self.ocm.commit() self.ocm.commit()
@@ -738,7 +703,7 @@ class OrderChangeManagerTests(TestCase):
def test_change_price_net_success(self): def test_change_price_net_success(self):
self.tr7.price_includes_tax = False self.tr7.price_includes_tax = False
self.tr7.save() self.tr7.save()
self.ocm.change_price(self.op1, Decimal('10.00')) self.ocm.change_price(self.op1, Decimal('10.70'))
self.ocm.commit() self.ocm.commit()
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()

View File

@@ -935,11 +935,9 @@ 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
), { ), {
'op-{}-operation'.format(self.op1.pk): 'product',
'op-{}-itemvar'.format(self.op1.pk): str(self.shirt.pk), 'op-{}-itemvar'.format(self.op1.pk): str(self.shirt.pk),
'op-{}-operation'.format(self.op2.pk): '', 'op-{}-price'.format(self.op1.pk): str('12.00'),
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk), 'add-itemvar': str(self.ticket.pk),
'add-itemvar'.format(self.op2.pk): str(self.ticket.pk),
}) })
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.order.refresh_from_db() self.order.refresh_from_db()
@@ -965,14 +963,9 @@ 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
), { ), {
'op-{}-operation'.format(self.op1.pk): 'subevent',
'op-{}-subevent'.format(self.op1.pk): str(se2.pk), 'op-{}-subevent'.format(self.op1.pk): str(se2.pk),
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk), 'add-itemvar': str(self.ticket.pk),
'op-{}-operation'.format(self.op2.pk): '', 'add-subevent': str(se1.pk),
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),
'op-{}-subevent'.format(self.op2.pk): str(se1.pk),
'add-itemvar'.format(self.op2.pk): str(self.ticket.pk),
'add-subevent'.format(self.op2.pk): str(se1.pk),
}) })
self.op1.refresh_from_db() self.op1.refresh_from_db()
self.op2.refresh_from_db() self.op2.refresh_from_db()
@@ -989,7 +982,7 @@ class OrderChangeTests(SoupTest):
'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'.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()
@@ -1001,13 +994,8 @@ 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
), { ), {
'op-{}-operation'.format(self.op1.pk): 'cancel', 'op-{}-operation_cancel'.format(self.op1.pk): 'on',
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk), 'add-itemvar': str(self.ticket.pk),
'op-{}-price'.format(self.op1.pk): str(self.op1.price),
'op-{}-operation'.format(self.op2.pk): '',
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op2.pk): str(self.op2.price),
'add-itemvar'.format(self.op2.pk): str(self.ticket.pk),
}) })
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.order.positions.count() == 1 assert self.order.positions.count() == 1
@@ -1017,12 +1005,6 @@ 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
), { ), {
'op-{}-operation'.format(self.op1.pk): '',
'op-{}-operation'.format(self.op2.pk): '',
'op-{}-itemvar'.format(self.op2.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op2.pk): str(self.op2.price),
'op-{}-itemvar'.format(self.op1.pk): str(self.ticket.pk),
'op-{}-price'.format(self.op1.pk): str(self.op1.price),
'add-itemvar': str(self.shirt.pk), 'add-itemvar': str(self.shirt.pk),
'add-do': 'on', 'add-do': 'on',
'add-price': '14.00', 'add-price': '14.00',
@@ -1047,7 +1029,7 @@ class OrderChangeTests(SoupTest):
self.event.organizer.slug, self.event.slug, self.order.code self.event.organizer.slug, self.event.slug, self.order.code
), { ), {
'other-recalculate_taxes': 'on', 'other-recalculate_taxes': 'on',
'add-itemvar'.format(self.op2.pk): str(self.ticket.pk), '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),