diff --git a/src/pretix/base/migrations/0161_order_changes_retain_old_default.py b/src/pretix/base/migrations/0161_order_changes_retain_old_default.py new file mode 100644 index 0000000000..91f2705ba9 --- /dev/null +++ b/src/pretix/base/migrations/0161_order_changes_retain_old_default.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +def migrate_change_allow_user_price(apps, schema_editor): + # Previously, the "gt" value was meant to represent "greater or equal", which became an issue the moment + # we introduced a "greater" and "greater or equal" option. This migrates any previous "greater or equal" + # selection to the new "gte". + Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore') + Event_SettingsStore.objects.filter(key="change_allow_user_price", value="gt").update(value="gte") + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0160_multiple_confirm_texts'), + ] + + operations = [ + migrations.RunPython(migrate_change_allow_user_price, migrations.RunPython.noop), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 20de183be3..b65c65cc7f 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -492,6 +492,10 @@ class Order(LockModel, LoggedModel): if self.cancellation_requests.exists(): return False + + if self.require_approval: + return False + positions = list( self.positions.all().annotate( has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))), diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 7f3da187e3..911d37d62d 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -960,13 +960,14 @@ DEFAULTS = { ) }, 'change_allow_user_price': { - 'default': 'gt', + 'default': 'gte', 'type': str, 'form_class': forms.ChoiceField, 'serializer_class': serializers.ChoiceField, 'serializer_kwargs': dict( choices=( - ('gt', _('Only allow changes if the resulting price is higher or equal than the previous price.')), + ('gte', _('Only allow changes if the resulting price is higher or equal than the previous price.')), + ('gt', _('Only allow changes if the resulting price is higher than the previous price.')), ('eq', _('Only allow changes if the resulting price is equal to the previous price.')), ('any', _('Allow changes regardless of price, even if this results in a refund.')), ) @@ -974,7 +975,8 @@ DEFAULTS = { 'form_kwargs': dict( label=_("Requirement for changed prices"), choices=( - ('gt', _('Only allow changes if the resulting price is higher or equal than the previous price.')), + ('gte', _('Only allow changes if the resulting price is higher or equal than the previous price.')), + ('gt', _('Only allow changes if the resulting price is higher than the previous price.')), ('eq', _('Only allow changes if the resulting price is equal to the previous price.')), ('any', _('Allow changes regardless of price, even if this results in a refund.')), ), diff --git a/src/pretix/presale/forms/order.py b/src/pretix/presale/forms/order.py index adaee75c18..149c73b819 100644 --- a/src/pretix/presale/forms/order.py +++ b/src/pretix/presale/forms/order.py @@ -60,7 +60,9 @@ class OrderPositionChangeForm(forms.Form): invoice_address=invoice_address) current_price = TaxedPrice(tax=instance.tax_value, gross=instance.price, net=instance.price - instance.tax_value, name=instance.tax_rule.name if instance.tax_rule else '', rate=instance.tax_rate) - if new_price.gross < current_price.gross and event.settings.change_allow_user_price == 'gt': + if new_price.gross < current_price.gross and event.settings.change_allow_user_price == 'gte': + continue + if new_price.gross <= current_price.gross and event.settings.change_allow_user_price == 'gt': continue if new_price.gross != current_price.gross and event.settings.change_allow_user_price == 'eq': continue diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index 004609756b..0d0f58aa72 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -1565,6 +1565,12 @@ class OrderTestCase(BaseQuotaTestCase): self.event.settings.change_allow_user_variation = True assert self.order.user_change_allowed + self.event.settings.change_allow_user_variation = False + self.order.require_approval = True + assert not self.order.user_change_allowed + self.event.settings.change_allow_user_variation = True + assert not self.order.user_change_allowed + @classscope(attr='o') def test_can_change_order_with_giftcard(self): item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23, diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py index 475d12b767..7d46f688ef 100644 --- a/src/tests/presale/test_orders.py +++ b/src/tests/presale/test_orders.py @@ -1374,6 +1374,86 @@ class OrdersTest(BaseOrdersTest): assert self.order.status == Order.STATUS_PENDING assert self.order.total == Decimal('37.00') + shirt_pos.variation = self.shirt_blue + shirt_pos.price = Decimal('14.00') + shirt_pos.save() + + response = self.client.post( + '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), + { + f'op-{shirt_pos.pk}-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}', + f'op-{self.ticket_pos.pk}-itemvar': f'{self.ticket.pk}', + }, + follow=True + ) + shirt_pos.refresh_from_db() + assert 'alert-danger' in response.rendered_content + assert shirt_pos.variation == self.shirt_blue + assert shirt_pos.price == Decimal('14.00') + + def test_change_variation_require_higher_equal_price(self): + self.event.settings.change_allow_user_variation = True + self.event.settings.change_allow_user_price = 'gte' + + with scopes_disabled(): + shirt_pos = OrderPosition.objects.create( + order=self.order, + item=self.shirt, + variation=self.shirt_red, + price=Decimal("14"), + ) + response = self.client.get( + '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret) + ) + assert response.status_code == 200 + response = self.client.post( + '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), { + f'op-{shirt_pos.pk}-itemvar': f'{self.shirt.pk}-{self.shirt_blue.pk}', + f'op-{self.ticket_pos.pk}-itemvar': f'{self.ticket.pk}', + }, follow=True) + assert response.status_code == 200 + assert 'alert-danger' in response.rendered_content + + shirt_pos.variation = self.shirt_blue + shirt_pos.price = Decimal('12.00') + shirt_pos.save() + + response = self.client.post( + '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), + { + f'op-{shirt_pos.pk}-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}', + f'op-{self.ticket_pos.pk}-itemvar': f'{self.ticket.pk}', + }, + follow=True + ) + self.assertRedirects(response, + '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, + self.order.secret), + target_status_code=200) + shirt_pos.refresh_from_db() + assert shirt_pos.variation == self.shirt_red + assert shirt_pos.price == Decimal('14.00') + self.order.refresh_from_db() + assert self.order.status == Order.STATUS_PENDING + assert self.order.total == Decimal('37.00') + + shirt_pos.variation = self.shirt_blue + shirt_pos.price = Decimal('14.00') + shirt_pos.save() + + response = self.client.post( + '/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), + { + f'op-{shirt_pos.pk}-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}', + f'op-{self.ticket_pos.pk}-itemvar': f'{self.ticket.pk}', + }, + follow=True + ) + shirt_pos.refresh_from_db() + assert 'alert-success' in response.rendered_content + assert shirt_pos.variation == self.shirt_red + assert shirt_pos.price == Decimal('14.00') + def test_change_variation_require_equal_price(self): self.event.settings.change_allow_user_variation = True self.event.settings.change_allow_user_price = 'eq'