mirror of
https://github.com/pretix/pretix.git
synced 2026-10-30 23:59:51 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a49592f22 | ||
|
|
def22684de | ||
|
|
bdf3b5edf6 | ||
|
|
ffd3ca96ab | ||
|
|
e6f8f178af | ||
|
|
5f5380e8b1 |
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)
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2026.9.0"
|
||||
__version__ = "2026.10.0.dev0"
|
||||
@@ -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()
|
||||
|
||||
@@ -151,12 +151,29 @@ class LoggingMixin:
|
||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
|
||||
organizer_id=organizer_id, **kwargs)
|
||||
if isinstance(data, dict):
|
||||
sensitivekeys = ['password', 'secret', 'api_key']
|
||||
# sensitivekeys are matched whether contained, not exactly
|
||||
sensitivekeys = [
|
||||
'auth',
|
||||
'key',
|
||||
'token',
|
||||
'password',
|
||||
'Password',
|
||||
'_pass',
|
||||
'salt',
|
||||
'secret',
|
||||
]
|
||||
|
||||
for sensitivekey in sensitivekeys:
|
||||
for k, v in data.items():
|
||||
if (sensitivekey in k) and v:
|
||||
def clean_recursive(d):
|
||||
data = d
|
||||
data_iterator = d.items() if isinstance(d, dict) else enumerate(d)
|
||||
for k, v in data_iterator:
|
||||
if v and any([sensitivekey in str(k) for sensitivekey in sensitivekeys]):
|
||||
data[k] = "********"
|
||||
elif isinstance(v, (list, dict)):
|
||||
data[k] = clean_recursive(v)
|
||||
return data
|
||||
|
||||
data = clean_recursive(data)
|
||||
|
||||
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
|
||||
elif data:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -94,6 +94,83 @@ def question(event, item):
|
||||
return q
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_log_action_filter(event, order):
|
||||
sensitivekeys = [
|
||||
'auth',
|
||||
'key',
|
||||
'token',
|
||||
'password',
|
||||
'Password',
|
||||
'_pass',
|
||||
'salt',
|
||||
'secret',
|
||||
]
|
||||
log_data = log_data_filtered = {
|
||||
'foo': 'bar',
|
||||
}
|
||||
for d in (log_data, log_data_filtered):
|
||||
value = '********' if d == log_data_filtered else '__sensitive_info__'
|
||||
for k in sensitivekeys:
|
||||
d[k] = d['prefix' + k] = d['prefix' + k + 'suffix'] = d[k + 'suffix'] = value
|
||||
|
||||
d['recursive_dict'] = {
|
||||
'test': 'dummy',
|
||||
'test_token': value,
|
||||
'd': {
|
||||
'baz': 'foo',
|
||||
'mypassword': value,
|
||||
'l': [
|
||||
'foo',
|
||||
'bar',
|
||||
{
|
||||
'baz': True,
|
||||
'superkey': value,
|
||||
},
|
||||
],
|
||||
},
|
||||
'l': [
|
||||
'okay',
|
||||
{
|
||||
'salt_to_secret': value,
|
||||
'name': 'okay'
|
||||
},
|
||||
'element',
|
||||
[
|
||||
'another',
|
||||
{
|
||||
'api_password': value,
|
||||
},
|
||||
'list'
|
||||
]
|
||||
]
|
||||
}
|
||||
d['recursive_list'] = [
|
||||
'foo',
|
||||
'bar',
|
||||
[
|
||||
'foo',
|
||||
'bar',
|
||||
{
|
||||
'foo': 'bar',
|
||||
'api_token': value,
|
||||
},
|
||||
'baz',
|
||||
],
|
||||
{
|
||||
'foo': 'bar',
|
||||
'MyPassword': value,
|
||||
}
|
||||
]
|
||||
l1 = order.log_action(
|
||||
'dummy.log',
|
||||
data=log_data,
|
||||
)
|
||||
|
||||
l1.refresh_from_db()
|
||||
assert json.loads(l1.data) == log_data_filtered
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_email_shredder(event, order):
|
||||
l1 = order.log_action(
|
||||
|
||||
Reference in new issue
Block a user