forked from CGM_Public/pretix_original
Compare commits
2 Commits
stripe-con
...
orderchang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1279c8720f | ||
|
|
7a3afde7b1 |
@@ -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,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)
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
('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)
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -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"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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