mirror of
https://github.com/pretix/pretix.git
synced 2026-11-07 00:56:40 +00:00
Fixes, fee support and total check
This commit is contained in:
1 parent
ffd3ca96ab
commit
bdf3b5edf6
4 files changed
+289
-36
No files matched your search
@@ -1029,6 +1029,7 @@ Creating orders
|
||||
charge will be created), this is just informative in case you *handled the payment already*.
|
||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||
* ``tax_rounding_mode`` (optional)
|
||||
* ``total`` (optional, will be computed from backend and operation will fail if the computation has a different result)
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
@@ -1059,7 +1060,9 @@ Creating orders
|
||||
* ``item``
|
||||
* ``variation`` (optional)
|
||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``tax_rule`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``tax_rule`` (optional, if set to ``null`` or missing the tax rule will be selected from the given product)
|
||||
* ``tax_rate`` (optional, only works together with a tax rule, if set to ``null`` or missing the tax rate will be computed from the given tax rule)
|
||||
* ``tax_code`` (optional, only works together with a tax rule, if set to ``null`` or missing the tax code will be computed from the given tax rule)
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
|
||||
@@ -1026,10 +1026,25 @@ class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
_treat_value_as_percentage = serializers.BooleanField(default=False, required=False)
|
||||
_split_taxes_like_products = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
tax_rate = serializers.DecimalField(required=False, decimal_places=4, max_digits=7)
|
||||
tax_value = serializers.DecimalField(required=False, decimal_places=2, max_digits=13)
|
||||
value_includes_rounding_correction = serializers.DecimalField(required=False, decimal_places=2, max_digits=13)
|
||||
tax_value_includes_rounding_correction = serializers.DecimalField(required=False, decimal_places=2, max_digits=13)
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule',
|
||||
'_treat_value_as_percentage', '_split_taxes_like_products')
|
||||
fields = (
|
||||
'fee_type', 'value', 'description', 'internal_type', 'tax_rule',
|
||||
'_treat_value_as_percentage', '_split_taxes_like_products',
|
||||
# The following fields should usually not be set by the client but computed by the backend. However, it makes
|
||||
# sense to set them when the decision on the tax rate is made irrevertably on the client side, such as with
|
||||
# pretixPOS.
|
||||
'tax_rate', 'tax_code',
|
||||
# The following fields are undocumented and only used by pretixPOS. They do *NOT* override the calculations
|
||||
# by the backends but are checked against them and must match exactly, otherwise the entire operation is
|
||||
# aborted to avoid diverging tax values between POS reports and backend reports.
|
||||
'tax_value', 'value_includes_rounding_correction', 'tax_value_includes_rounding_correction',
|
||||
)
|
||||
|
||||
def validate_tax_rule(self, tr):
|
||||
if tr and tr.event != self.context['event']:
|
||||
@@ -1038,6 +1053,25 @@ class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
return tr
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs.get('_treat_value_as_percentage'):
|
||||
for k in ('tax_value', 'value_includes_rounding_correction', 'tax_value_includes_rounding_correction'):
|
||||
if k in attrs:
|
||||
raise ValidationError({
|
||||
k: ['You cannot ask for the value to be treated as a percentage if you already computed taxes.']
|
||||
})
|
||||
if attrs.get('_split_taxes_like_products'):
|
||||
for k in ('tax_rate', 'tax_value', 'value_includes_rounding_correction', 'tax_value_includes_rounding_correction'):
|
||||
if k in attrs:
|
||||
raise ValidationError({
|
||||
k: ['You cannot ask for the value to be distributed over tax rates if you already set the tax rate or computed taxes.']
|
||||
})
|
||||
if 'tax_rate' in attrs and 'tax_rule' not in attrs:
|
||||
raise ValidationError({
|
||||
'tax_rate': ['Tax rate cannot be stored without tax rule.']
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
@@ -1064,11 +1098,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
'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', 'discount', 'tax_rule',
|
||||
# These are currently undocumented and only used by pretixPOS. The backend still calculates these values, but the
|
||||
# results must match exactly, otherwise the operation is aborted to avoid diverging tax values between POS reports
|
||||
# and backend reports.
|
||||
'tax_rate', 'tax_code', 'tax_value', 'price_includes_rounding_correction', 'tax_value_includes_rounding_correction',
|
||||
'requested_valid_from', 'use_reusable_medium', 'discount',
|
||||
# The following fields should usually not be set by the client but computed by the backend. However, it makes
|
||||
# sense to set them when the decision on the tax rate is made irrevertably on the client side, such as with
|
||||
# pretixPOS.
|
||||
'tax_rule', 'tax_rate', 'tax_code',
|
||||
# The following fields are undocumented and only used by pretixPOS. They do *NOT* override the calculations
|
||||
# by the backends but are checked against them and must match exactly, otherwise the entire operation is
|
||||
# aborted to avoid diverging tax values between POS reports and backend reports.
|
||||
'tax_value', 'price_includes_rounding_correction', 'tax_value_includes_rounding_correction',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1177,6 +1215,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
||||
)
|
||||
|
||||
if 'tax_rate' in data and 'tax_rule' not in data and not data['item'].tax_rule_id:
|
||||
raise ValidationError({
|
||||
'tax_rate': ['Tax rate cannot be stored without tax rule.']
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -1234,6 +1277,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
|
||||
total = serializers.DecimalField(required=False, decimal_places=2, max_digits=13)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1248,7 +1292,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'total',
|
||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
@@ -1588,6 +1632,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if validated_data.get('locale', None) is None:
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
|
||||
expected_total = validated_data.pop('total', None)
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
@@ -1669,7 +1714,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
pos._calculate_tax(invoice_address=ia, tax_rule=pos_data.get('tax_rule'))
|
||||
pos._calculate_tax(
|
||||
invoice_address=ia,
|
||||
tax_rule=pos_data.get('tax_rule'),
|
||||
override_tax_rate=pos_data.get('tax_rate'),
|
||||
override_tax_code=pos_data.get('tax_code'),
|
||||
)
|
||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
@@ -1701,7 +1751,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||
pos = pos_data['__instance']
|
||||
pos._calculate_tax(invoice_address=ia, tax_rule=pos_data.get('tax_rule', None))
|
||||
pos._calculate_tax(
|
||||
invoice_address=ia,
|
||||
tax_rule=pos_data.get('tax_rule', None),
|
||||
override_tax_rate=pos_data.get('tax_rate'),
|
||||
override_tax_code=pos_data.get('tax_code'),
|
||||
)
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
@@ -1800,7 +1855,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
for tr, val in fee_values:
|
||||
fee_data['tax_rule'] = tr
|
||||
fee_data['value'] = val
|
||||
f = OrderFee(**fee_data)
|
||||
f = OrderFee(**{k: v for k, v in fee_data.items() if k not in (
|
||||
'tax_value',
|
||||
'value_includes_rounding_correction',
|
||||
'tax_value_includes_rounding_correction'
|
||||
)})
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
@@ -1809,9 +1868,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
f.save()
|
||||
else:
|
||||
f = OrderFee(**fee_data)
|
||||
f = OrderFee(**{k: v for k, v in fee_data.items() if k not in (
|
||||
'tax_value',
|
||||
'value_includes_rounding_correction',
|
||||
'tax_value_includes_rounding_correction'
|
||||
)})
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
f._calculate_tax(
|
||||
override_tax_rate=fee_data.get('tax_rate'),
|
||||
override_tax_code=fee_data.get('tax_code'),
|
||||
)
|
||||
fee_data['__instance'] = f
|
||||
fees.append(f)
|
||||
if simulate:
|
||||
f.id = 0
|
||||
@@ -1845,25 +1912,45 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
pos = pos_data['__instance']
|
||||
if 'tax_rate' in pos_data and pos_data['tax_rate'] != pos.tax_rate:
|
||||
raise ValidationError(f'Tax rate {pos_data["tax_rate"]} in position {pos.positionid} does not match backend value {pos.tax_rate}.')
|
||||
if 'tax_code' in pos_data and pos_data['tax_code'] != pos.tax_code:
|
||||
raise ValidationError(f'Tax code {pos_data["tax_code"]} in position {pos.positionid} does not match backend value {pos.tax_code}.')
|
||||
if 'tax_value' in pos_data and pos_data['tax_value'] != pos.tax_value:
|
||||
raise ValidationError(f'Tax value {pos_data["tax_value"]} in position {pos.positionid} does not match backend value {pos.tax_value}.')
|
||||
raise ValidationError(f'Tax value {pos_data["tax_value"]} in position {pos.positionid} does not match backend computation of {pos.tax_value}.')
|
||||
if 'price_includes_rounding_correction' in pos_data and pos_data['price_includes_rounding_correction'] != pos.price_includes_rounding_correction:
|
||||
raise ValidationError(
|
||||
f'Price rounding {pos_data['price_includes_rounding_correction']} in position {pos.positionid} does not '
|
||||
f'match backend value {pos.price_includes_rounding_correction}.'
|
||||
f'Price rounding {pos_data["price_includes_rounding_correction"]} in position {pos.positionid} does not '
|
||||
f'match backend computation of {pos.price_includes_rounding_correction}.'
|
||||
)
|
||||
if ('tax_value_includes_rounding_correction' in pos_data and
|
||||
pos_data['tax_value_includes_rounding_correction'] != pos.tax_value_includes_rounding_correction):
|
||||
raise ValidationError(
|
||||
f'Tax rounding {pos_data['tax_value_includes_rounding_correction']} in position {pos.positionid} does not '
|
||||
f'match backend value {pos.tax_value_includes_rounding_correction}.'
|
||||
f'Tax rounding {pos_data["tax_value_includes_rounding_correction"]} in position {pos.positionid} does not '
|
||||
f'match backend computation of {pos.tax_value_includes_rounding_correction}.'
|
||||
)
|
||||
|
||||
for i, fee_data in enumerate(fees_data):
|
||||
if '__instance' not in fee_data: # split fee not supported for this, validated in OrderFeeCreateSerializer
|
||||
continue
|
||||
fee = fee_data['__instance']
|
||||
if 'tax_value' in fee_data and fee_data['tax_value'] != fee.tax_value:
|
||||
raise ValidationError(f'Tax value {fee_data["tax_value"]} in fee does not match backend computation of {fee.tax_value}.')
|
||||
if 'value_includes_rounding_correction' in fee_data and fee_data['value_includes_rounding_correction'] != fee.value_includes_rounding_correction:
|
||||
raise ValidationError(
|
||||
f'Price rounding {fee_data["value_includes_rounding_correction"]} in fee does not '
|
||||
f'match backend computation of {fee.value_includes_rounding_correction}.'
|
||||
)
|
||||
if ('tax_value_includes_rounding_correction' in fee_data and
|
||||
fee_data['tax_value_includes_rounding_correction'] != fee.tax_value_includes_rounding_correction):
|
||||
raise ValidationError(
|
||||
f'Tax rounding {fee_data["tax_value_includes_rounding_correction"]} in fee does not '
|
||||
f'match backend computation of {fee.tax_value_includes_rounding_correction}.'
|
||||
)
|
||||
|
||||
order.total = sum([c.price for c in pos_map.values()]) + sum([f.value for f in fees])
|
||||
|
||||
if expected_total is not None and expected_total != order.total:
|
||||
raise ValidationError({
|
||||
'total': [f'Order total of {expected_total} does not match backend computation of {order.total}.']
|
||||
})
|
||||
|
||||
if simulate:
|
||||
order.fees = fees
|
||||
order.positions = pos_map.values()
|
||||
|
||||
@@ -2450,7 +2450,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
self.fee_type, self.value
|
||||
)
|
||||
|
||||
def _calculate_tax(self, tax_rule=None, invoice_address=None, event=None):
|
||||
def _calculate_tax(self, tax_rule=None, invoice_address=None, event=None, override_tax_rate=None, override_tax_code=None):
|
||||
if tax_rule:
|
||||
self.tax_rule = tax_rule
|
||||
|
||||
@@ -2469,7 +2469,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
self.tax_rule = event.cached_default_tax_rule
|
||||
|
||||
if self.tax_rule:
|
||||
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True)
|
||||
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True,
|
||||
override_tax_rate=override_tax_rate, override_tax_code=override_tax_code)
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_code = tax.code
|
||||
self.tax_value = tax.tax
|
||||
@@ -2832,14 +2833,15 @@ class OrderPosition(AbstractPosition):
|
||||
self.item.id, self.variation.id if self.variation else 0, self.order_id
|
||||
)
|
||||
|
||||
def _calculate_tax(self, tax_rule=None, invoice_address=None):
|
||||
def _calculate_tax(self, tax_rule=None, invoice_address=None, override_tax_rate=None, override_tax_code=None):
|
||||
self.tax_rule = tax_rule or self.item.tax_rule
|
||||
try:
|
||||
ia = invoice_address or self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
if self.tax_rule:
|
||||
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True)
|
||||
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True,
|
||||
override_tax_rate=override_tax_rate, override_tax_code=override_tax_code)
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_code = tax.code
|
||||
self.tax_value = tax.tax
|
||||
|
||||
@@ -1034,6 +1034,34 @@ def test_order_create_tax_rule_explicit(token_client, organizer, event, item, qu
|
||||
assert pos.tax_rate == Decimal('19.00')
|
||||
assert pos.tax_value == Decimal('3.67')
|
||||
assert pos.tax_rule == taxrule
|
||||
assert pos.tax_code == "S/standard"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_tax_rule_custom_rate_and_code(token_client, organizer, event, item, quota, question, taxrule):
|
||||
item.tax_rule = None
|
||||
item.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['tax_rule'] = taxrule.pk
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['tax_rule'] = taxrule.pk
|
||||
res['positions'][0]['tax_code'] = 'S/reduced'
|
||||
res['positions'][0]['tax_rate'] = "7.00"
|
||||
res['positions'][0]['answers'][0]['question'] = question.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'])
|
||||
pos = o.positions.first()
|
||||
assert pos.item == item
|
||||
assert pos.tax_rate == Decimal('7.00')
|
||||
assert pos.tax_value == Decimal('1.50')
|
||||
assert pos.tax_rule == taxrule
|
||||
assert pos.tax_code == "S/reduced"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -1131,6 +1159,75 @@ def test_order_create_fee_with_auto_tax(token_client, organizer, event, item, qu
|
||||
assert o.total == Decimal('25.30')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_with_explicit_tax_and_percentage(token_client, organizer, event, item, quota, question, taxrule):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['tax_rate'] = '7.00'
|
||||
res['fees'][0]['tax_rule'] = taxrule.pk
|
||||
res['fees'][0]['_split_taxes_like_products'] = False
|
||||
res['fees'][0]['_treat_value_as_percentage'] = True
|
||||
res['fees'][0]['value'] = '10.00'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
item.tax_rule = taxrule
|
||||
item.save()
|
||||
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'])
|
||||
fee = o.fees.first()
|
||||
assert fee.value == Decimal('2.30')
|
||||
assert fee.tax_rate == Decimal('7.00')
|
||||
assert fee.tax_value == Decimal('0.15')
|
||||
assert o.total == Decimal('25.30')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_with_explicit_tax_and_split_requested(token_client, organizer, event, item, quota, question, taxrule):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['tax_rate'] = '7.00'
|
||||
res['fees'][0]['tax_rule'] = taxrule.pk
|
||||
res['fees'][0]['_split_taxes_like_products'] = True
|
||||
res['fees'][0]['_treat_value_as_percentage'] = True
|
||||
res['fees'][0]['value'] = '10.00'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
item.tax_rule = taxrule
|
||||
item.save()
|
||||
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 == {
|
||||
'fees': [{'tax_rate': ['You cannot ask for the value to be distributed over tax rates if you already set the tax rate or computed taxes.']}],
|
||||
}
|
||||
|
||||
res['fees'][0]['tax_rate'] = '7.00'
|
||||
res['fees'][0]['tax_value'] = '1.50'
|
||||
res['fees'][0]['tax_rule'] = taxrule.pk
|
||||
res['fees'][0]['_treat_value_as_percentage'] = True
|
||||
res['fees'][0]['value'] = '10.00'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
item.tax_rule = taxrule
|
||||
item.save()
|
||||
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 == {
|
||||
'fees': [{'tax_value': ['You cannot ask for the value to be treated as a percentage if you already computed taxes.']}],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_negative_fee_with_auto_tax(token_client, organizer, event, item, quota, question, taxrule):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
@@ -3725,19 +3822,17 @@ def test_order_create_use_gift_card_invalid_secret(token_client, organizer, even
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode,field,right,wrong,error", [
|
||||
("line", "tax_rate", "19.00", "22.00", "Tax rate 22.0000 in position 1 does not match backend value 19."),
|
||||
("line", "tax_value", "2.39", "2.50", "Tax value 2.50 in position 1 does not match backend value 2.39."),
|
||||
("line", "tax_code", "S/standard", "S/reduced", "Tax code S/reduced in position 1 does not match backend value S/standard."),
|
||||
("line", "price_includes_rounding_correction", "0.00", "0.01", "Price rounding 0.01 in position 1 does not match backend value 0.00."),
|
||||
("line", "tax_value_includes_rounding_correction", "0.00", "0.01", "Tax rounding 0.01 in position 1 does not match backend value 0.00."),
|
||||
("sum_by_net", "tax_value", "2.40", "2.39", "Tax value 2.39 in position 1 does not match backend value 2.40."),
|
||||
("sum_by_net", "price_includes_rounding_correction", "0.01", "0.00", "Price rounding 0.00 in position 1 does not match backend value 0.01."),
|
||||
("sum_by_net", "tax_value_includes_rounding_correction", "0.01", "0.00", "Tax rounding 0.00 in position 1 does not match backend value 0.01."),
|
||||
("line", "tax_value", "2.39", "2.50", "Tax value 2.50 in position 1 does not match backend computation of 2.39."),
|
||||
("line", "price_includes_rounding_correction", "0.00", "0.01", "Price rounding 0.01 in position 1 does not match backend computation of 0.00."),
|
||||
("line", "tax_value_includes_rounding_correction", "0.00", "0.01", "Tax rounding 0.01 in position 1 does not match backend computation of 0.00."),
|
||||
("sum_by_net", "tax_value", "2.40", "2.39", "Tax value 2.39 in position 1 does not match backend computation of 2.40."),
|
||||
("sum_by_net", "price_includes_rounding_correction", "0.01", "0.00", "Price rounding 0.00 in position 1 does not match backend computation of 0.01."),
|
||||
("sum_by_net", "tax_value_includes_rounding_correction", "0.01", "0.00", "Tax rounding 0.00 in position 1 does not match backend computation of 0.01."),
|
||||
])
|
||||
def test_order_tax_match_validation(token_client, organizer, event, item, quota, question, taxrule,
|
||||
rounding_mode, field, right, wrong, error):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
del res['fees'] # not explicitly supported for this for now
|
||||
del res['fees']
|
||||
res['tax_rounding_mode'] = rounding_mode
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['price'] = "15.00"
|
||||
@@ -3759,3 +3854,69 @@ def test_order_tax_match_validation(token_client, organizer, event, item, quota,
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode,field,right,wrong,error", [
|
||||
("line", "tax_value", "1.60", "1.61", "Tax value 1.61 in fee does not match backend computation of 1.60."),
|
||||
("line", "value_includes_rounding_correction", "0.00", "0.01", "Price rounding 0.01 in fee does not match backend computation of 0.00."),
|
||||
("line", "tax_value_includes_rounding_correction", "0.00", "0.01", "Tax rounding 0.01 in fee does not match backend computation of 0.00."),
|
||||
("sum_by_net", "tax_value", "1.59", "1.60", "Tax value 1.60 in fee does not match backend computation of 1.59."),
|
||||
("sum_by_net", "value_includes_rounding_correction", "-0.01", "0.00", "Price rounding 0.00 in fee does not match backend computation of -0.01."),
|
||||
("sum_by_net", "tax_value_includes_rounding_correction", "-0.01", "0.00", "Tax rounding 0.00 in fee does not match backend computation of -0.01."),
|
||||
])
|
||||
def test_order_tax_match_validation_of_fee(token_client, organizer, event, item, quota, question, taxrule,
|
||||
rounding_mode, field, right, wrong, error):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['tax_rounding_mode'] = rounding_mode
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['price'] = "5.00"
|
||||
res['positions'][0]['tax_rule'] = taxrule.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['fees'][0]['value'] = "10.00"
|
||||
res['fees'][0]['tax_rule'] = taxrule.pk
|
||||
res['fees'][0][field] = wrong
|
||||
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 == [error]
|
||||
|
||||
res['fees'][0][field] = right
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_expect_total(token_client, organizer, event, item, quota, question, taxrule):
|
||||
# Force a rounding difference to make sure its the last step of total computation that is compared
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
del res['fees']
|
||||
res['tax_rounding_mode'] = "sum_by_net"
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['price'] = "15.00"
|
||||
res['positions'][0]['tax_rule'] = taxrule.pk
|
||||
res['total'] = "15.00"
|
||||
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 == {
|
||||
'total': ['Order total of 15.00 does not match backend computation of 15.01.']
|
||||
}
|
||||
|
||||
res['total'] = "15.01"
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
Reference in new issue
Block a user