Compare commits

..
Author SHA1 Message Date
Raphael Michel 3a49592f22 Fix handling of inbound prices 2026-10-30 15:16:37 +01:00
Raphael Michel def22684de Fix test on postgresql 2026-10-30 11:54:49 +01:00
Raphael Michel bdf3b5edf6 Fixes, fee support and total check 2026-10-30 10:55:35 +01:00
Raphael Michel ffd3ca96ab API: Allow pretixPOS to communicate its binding tax calculation 2026-10-29 22:30:28 +01:00
Richard Schreiber e6f8f178af Extend log_action filter and make recursive (Z#23247315) (#6598)
* Extend log_action filter and make recursive

* fix flake8

* fix type mismatch

* add "salt" to sensitive keys

* match "key" (e.g. see payone)

* add "auth" to sensitive keys

* clean lists recursively

* improve clean method name as it also cleans list recursively

* Add test
2026-10-29 10:52:00 +01:00
Raphael Michel 5f5380e8b1 Bump version to 2026.10.0.dev0 2026-10-29 09:59:11 +01:00
7 changed files with 504 additions and 25 deletions

No files matched your search

+4
View File
@@ -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)
+1 -1
View File
@@ -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"
+153 -15
View File
@@ -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()
+21 -4
View File
@@ -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:
+6 -4
View File
@@ -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
+242 -1
View File
@@ -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
+77
View File
@@ -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(