mirror of
https://github.com/pretix/pretix.git
synced 2026-05-20 17:44:02 +00:00
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.
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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,82 @@ 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')
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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'),
|
operation_cancel = forms.BooleanField(
|
||||||
('price', 'Change price'),
|
required=False,
|
||||||
('subevent', 'Change event date'),
|
label=_('Cancel this position')
|
||||||
('cancel', 'Remove product'),
|
)
|
||||||
('split', 'Split into new order'),
|
operation_split = forms.BooleanField(
|
||||||
('secret', 'Regenerate secret'),
|
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:
|
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
|
||||||
ia = instance.order.invoice_address
|
initial['price'] = instance.price - instance.tax_value
|
||||||
except InvoiceAddress.DoesNotExist:
|
else:
|
||||||
ia = None
|
initial['price'] = instance.price
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
</div>
|
||||||
<label>
|
<div class="col-sm-4">
|
||||||
<input name="{{ position.form.prefix }}-operation" type="radio" value=""
|
<strong>{% trans "Change to" %}</strong>
|
||||||
{% if not position.form.operation.value %}checked="checked"{% endif %}>
|
</div>
|
||||||
{% trans "Keep unchanged" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="radio">
|
<div class="row">
|
||||||
<label>
|
<div class="col-sm-3">
|
||||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="product"
|
<strong>{% trans "Product" %}</strong>
|
||||||
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}>
|
</div>
|
||||||
{% trans "Change product to" %}
|
<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>
|
</div>
|
||||||
<label class="checkbox">
|
|
||||||
{{ position.form.change_product_keep_price }}
|
|
||||||
{% trans "Keep price the same" %}
|
|
||||||
</label>
|
|
||||||
</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>
|
||||||
|
<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' %}
|
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
|
||||||
{% if position.apply_tax %}
|
<small><strong>{% trans "including all taxes" %}</strong></small>
|
||||||
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
|
</div>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="radio">
|
|
||||||
<label>
|
<div class="row">
|
||||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="split"
|
<div class="col-sm-3">
|
||||||
{% if position.form.operation.value == "split" %}checked="checked"{% endif %}>
|
<strong>{% trans "Ticket secret" %}</strong>
|
||||||
{% trans "Split into new order" %}
|
</div>
|
||||||
</label>
|
<div class="col-sm-5">
|
||||||
</div>
|
{{ position.secret|slice:":12" }}…
|
||||||
<div class="radio">
|
</div>
|
||||||
<label>
|
<div class="col-sm-4">
|
||||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="secret"
|
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
||||||
{% if position.form.operation.value == "secret" %}checked="checked"{% endif %}>
|
</div>
|
||||||
{% 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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
51
src/pretix/static/pretixcontrol/js/ui/orderchange.js
Normal file
51
src/pretix/static/pretixcontrol/js/ui/orderchange.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user