mirror of
https://github.com/pretix/pretix.git
synced 2026-10-30 23:59:51 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a49592f22 | ||
|
|
def22684de | ||
|
|
bdf3b5edf6 | ||
|
|
ffd3ca96ab |
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,6 +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 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)
|
||||
|
||||
@@ -61,6 +61,7 @@ dependencies = [
|
||||
"dnspython==2.8.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==5.*",
|
||||
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.6.*",
|
||||
|
||||
@@ -607,7 +607,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use', 'plugin_data')
|
||||
read_only_fields = (
|
||||
@@ -1027,18 +1026,56 @@ 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']:
|
||||
raise ValidationError(
|
||||
'The specified tax rate does not belong to this event.'
|
||||
'The specified tax rule does not belong to this event.'
|
||||
)
|
||||
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.']
|
||||
})
|
||||
|
||||
if 'value_includes_rounding_correction' in attrs:
|
||||
# Make all computations like discounts on the price before rounding, later compare against rounding again
|
||||
attrs['value'] = attrs['value'] - attrs['value_includes_rounding_correction']
|
||||
return attrs
|
||||
|
||||
|
||||
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
@@ -1054,13 +1091,27 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
|
||||
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
tax_rate = serializers.DecimalField(required=False, decimal_places=4, max_digits=7)
|
||||
tax_value = serializers.DecimalField(required=False, decimal_places=2, max_digits=13)
|
||||
price_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 = OrderPosition
|
||||
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', 'discount')
|
||||
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', '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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1098,6 +1149,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
return item
|
||||
|
||||
def validate_tax_rule(self, tax_rule):
|
||||
if tax_rule.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified tax rule does not belong to this event.'
|
||||
)
|
||||
return tax_rule
|
||||
|
||||
def validate_subevent(self, subevent):
|
||||
if self.context['event'].has_subevents:
|
||||
if not subevent:
|
||||
@@ -1161,6 +1219,15 @@ 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.']
|
||||
})
|
||||
|
||||
if 'price_includes_rounding_correction' in data:
|
||||
# Make all computations like discounts on the price before rounding, later compare against rounding again
|
||||
data['price'] = data['price'] - data['price_includes_rounding_correction']
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -1218,6 +1285,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)
|
||||
@@ -1232,7 +1300,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):
|
||||
@@ -1572,6 +1640,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])
|
||||
@@ -1603,7 +1672,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium')})
|
||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in (
|
||||
'answers',
|
||||
'_quotas',
|
||||
'use_reusable_medium',
|
||||
'tax_value',
|
||||
'price_includes_rounding_correction',
|
||||
'tax_value_includes_rounding_correction',
|
||||
)})
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
@@ -1646,7 +1722,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)
|
||||
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
|
||||
@@ -1678,7 +1759,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)
|
||||
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)
|
||||
@@ -1777,7 +1863,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)
|
||||
@@ -1786,9 +1876,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
|
||||
@@ -1820,7 +1918,47 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
"value", "value_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
||||
])
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
pos = pos_data['__instance']
|
||||
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 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 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 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
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from importlib import metadata
|
||||
from typing import List
|
||||
|
||||
import importlib_metadata as metadata
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
@@ -23,10 +23,10 @@ import json
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
from importlib import metadata
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import urlopen
|
||||
|
||||
import importlib_metadata as metadata
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
from importlib import metadata as metadata
|
||||
|
||||
import importlib_metadata as metadata
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
|
||||
@@ -41,7 +41,7 @@ from json import loads
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from importlib import metadata
|
||||
import importlib_metadata as metadata
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.crypto import get_random_string
|
||||
from kombu import Queue
|
||||
|
||||
@@ -1012,6 +1012,58 @@ def test_order_create_tax_rules(token_client, organizer, event, item, quota, que
|
||||
assert pos.tax_rule == taxrule
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_tax_rule_explicit(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]['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('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
|
||||
def test_order_create_fee_type_validation(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
@@ -1107,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)
|
||||
@@ -1136,6 +1257,7 @@ def test_order_create_tax_rule_wrong_event(token_client, organizer, event, item,
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['tax_rule'] = taxrule2.pk
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['tax_rule'] = taxrule2.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
@@ -1143,7 +1265,10 @@ def test_order_create_tax_rule_wrong_event(token_client, organizer, event, item,
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'fees': [{'tax_rule': ['The specified tax rate does not belong to this event.']}]}
|
||||
assert resp.data == {
|
||||
'positions': [{'tax_rule': ['The specified tax rule does not belong to this event.']}],
|
||||
'fees': [{'tax_rule': ['The specified tax rule does not belong to this event.']}],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -3693,3 +3818,119 @@ def test_order_create_use_gift_card_invalid_secret(token_client, organizer, even
|
||||
assert o.payments.count() == 2
|
||||
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode,data,error", [
|
||||
("line", {"tax_value": "2.40"}, "Tax value 2.40 in position 1 does not match backend computation of 2.39."),
|
||||
("line", {"tax_value": "2.39"}, None),
|
||||
("line", {"price_includes_rounding_correction": "0.01"}, "Price rounding 0.01 in position 1 does not match backend computation of 0.00."),
|
||||
("line", {"tax_value_includes_rounding_correction": "0.01"}, "Tax rounding 0.01 in position 1 does not match backend computation of 0.00."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "2.39"},
|
||||
"Tax value 2.39 in position 1 does not match backend computation of 2.40."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "2.40", "price": "15.01", "price_includes_rounding_correction": "0.01"},
|
||||
None),
|
||||
("sum_by_net",
|
||||
{"tax_value": "2.40", "price": "15.00", "price_includes_rounding_correction": "0.00"},
|
||||
"Price rounding 0.00 in position 1 does not match backend computation of 0.01."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "2.40", "price": "15.01", "price_includes_rounding_correction": "0.01", "tax_value_includes_rounding_correction": "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, data, error):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
del res['fees']
|
||||
res['tax_rounding_mode'] = rounding_mode
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['price'] = "15.00"
|
||||
res['positions'][0]['tax_rule'] = taxrule.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0].update(data)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
if error:
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == [error]
|
||||
else:
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode,data,error", [
|
||||
("line", {"tax_value": "1.61"}, "Tax value 1.61 in fee does not match backend computation of 1.60."),
|
||||
("line", {"tax_value": "1.60"}, None),
|
||||
("line", {"value_includes_rounding_correction": "0.01"},
|
||||
"Price rounding 0.01 in fee does not match backend computation of 0.00."),
|
||||
("line", {"tax_value_includes_rounding_correction": "0.01"},
|
||||
"Tax rounding 0.01 in fee does not match backend computation of 0.00."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "1.60"},
|
||||
"Tax value 1.60 in fee does not match backend computation of 1.59."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "1.59", "value": "9.99", "value_includes_rounding_correction": "-0.01"},
|
||||
None),
|
||||
("sum_by_net",
|
||||
{"tax_value": "1.59", "value": "10.00", "value_includes_rounding_correction": "0.00"},
|
||||
"Price rounding 0.00 in fee does not match backend computation of -0.01."),
|
||||
("sum_by_net",
|
||||
{"tax_value": "1.59", "value": "9.99", "value_includes_rounding_correction": "-0.01",
|
||||
"tax_value_includes_rounding_correction": "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, data, 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].update(data)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
if error:
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == [error]
|
||||
else:
|
||||
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 it's 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['positions'][0]['answers'][0]['question'] = question.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