forked from CGM_Public/pretix_original
API: Trust discounts assigned by pretixPOS, do not assign differently (#5531)
This commit is contained in:
@@ -1058,6 +1058,7 @@ Creating orders
|
||||
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
||||
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
||||
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
|
||||
@@ -1005,7 +1005,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
||||
'requested_valid_from', 'use_reusable_medium')
|
||||
'requested_valid_from', 'use_reusable_medium', 'discount')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1101,6 +1101,10 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
if data.get('price') is None and data.get('discount'):
|
||||
raise ValidationError(
|
||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -1160,6 +1164,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
self.fields['positions'].child.fields['discount'].queryset = self.context['event'].discounts.all()
|
||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||
self.fields['expires'].required = False
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
@@ -1567,19 +1572,22 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
|
||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||
discount_results = apply_discounts(
|
||||
self.context['event'],
|
||||
order.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
for cp in order_positions
|
||||
]
|
||||
)
|
||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||
if new_price != pos.price and pos._auto_generated_price:
|
||||
pos.price = new_price
|
||||
pos.discount = discount
|
||||
if not any([p.get("discount") for p in positions_data]):
|
||||
# If any discount is set by the client (i.e. pretixPOS), we do not recalculate but believe the client
|
||||
# to avoid differences in end results.
|
||||
discount_results = apply_discounts(
|
||||
self.context['event'],
|
||||
order.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
for cp in order_positions
|
||||
]
|
||||
)
|
||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||
if new_price != pos.price and pos._auto_generated_price:
|
||||
pos.price = new_price
|
||||
pos.discount = discount
|
||||
|
||||
# Save instances
|
||||
for pos_data in positions_data:
|
||||
|
||||
@@ -3134,3 +3134,128 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota,
|
||||
m = organizer.reusable_media.get(identifier=i)
|
||||
assert m.linked_orderposition == o.positions.first()
|
||||
assert m.type == "barcode"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_pricing_discount(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
event.discounts.create(
|
||||
condition_min_count=2,
|
||||
benefit_discount_matching_percent=50,
|
||||
benefit_only_apply_to_cheapest_n_matches=1,
|
||||
)
|
||||
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
del res['positions'][0]['positionid']
|
||||
del res['positions'][0]['price']
|
||||
res['positions'].append(dict(res['positions'][0]))
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p1 = o.positions.first()
|
||||
p2 = o.positions.last()
|
||||
assert p1.price == Decimal('23')
|
||||
assert p2.price == Decimal('11.50')
|
||||
assert o.total == Decimal('34.75')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_pricing_do_not_discount_if_price_explcitly_set(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
event.discounts.create(
|
||||
condition_min_count=2,
|
||||
benefit_discount_matching_percent=50,
|
||||
benefit_only_apply_to_cheapest_n_matches=1,
|
||||
)
|
||||
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
del res['positions'][0]['positionid']
|
||||
res['positions'].append(dict(res['positions'][0]))
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p1 = o.positions.first()
|
||||
p2 = o.positions.last()
|
||||
assert p1.price == Decimal('23.00')
|
||||
assert p2.price == Decimal('23.00')
|
||||
assert o.total == Decimal('46.25')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_pricing_believe_wrong_discounts_by_client(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
discount = event.discounts.create(
|
||||
condition_min_count=2,
|
||||
benefit_discount_matching_percent=50,
|
||||
benefit_only_apply_to_cheapest_n_matches=1,
|
||||
)
|
||||
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
del res['positions'][0]['positionid']
|
||||
res['positions'].append(dict(res['positions'][0]))
|
||||
res['positions'][0]['price'] = Decimal("10.00")
|
||||
res['positions'][1]['price'] = Decimal("7.00")
|
||||
res['positions'][1]['discount'] = discount.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p1 = o.positions.first()
|
||||
p2 = o.positions.last()
|
||||
assert p1.price == Decimal('10.00')
|
||||
assert p1.discount is None
|
||||
assert p2.price == Decimal('7.00')
|
||||
assert p2.discount == discount
|
||||
assert o.total == Decimal('17.25')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_pricing_explicit_discount_not_allowed(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
discount = event.discounts.create(
|
||||
condition_min_count=2,
|
||||
benefit_discount_matching_percent=50,
|
||||
benefit_only_apply_to_cheapest_n_matches=1,
|
||||
)
|
||||
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
del res['positions'][0]['positionid']
|
||||
del res['positions'][0]['price']
|
||||
res['positions'].append(dict(res['positions'][0]))
|
||||
res['positions'][1]['discount'] = discount.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
"positions": [
|
||||
{},
|
||||
{
|
||||
"discount": ["You can only specify a discount if you do the price computation, but price is not set."]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user