diff --git a/doc/api/resources/items.rst b/doc/api/resources/items.rst
index 980d3d5e34..d1112aed95 100644
--- a/doc/api/resources/items.rst
+++ b/doc/api/resources/items.rst
@@ -52,9 +52,12 @@ available_from datetime The first dat
(or ``null``).
available_until datetime The last date time at which this item can be bought
(or ``null``).
-hidden_if_available integer The internal ID of a quota object, or ``null``. If
+hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
+hidden_if_item_available integer The internal ID of a different item, or ``null``. If
+ set, this item won't be shown publicly as long as this
+ other item is available.
require_voucher boolean If ``true``, this item can only be bought using a
voucher that is specifically assigned to this item.
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
@@ -204,6 +207,11 @@ meta_data object Values set fo
The ``free_price_suggestion`` and ``variations[x].free_price_suggestion`` attributes have been added.
+.. versionchanged:: 2023.10
+
+ The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
+ deprecated.
+
Notes
-----
@@ -268,6 +276,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
+ "hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -402,6 +411,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
+ "hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -517,6 +527,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
+ "hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -619,6 +630,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
+ "hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -753,6 +765,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
+ "hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"generate_tickets": null,
diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py
index 09e9777e01..313de305e6 100644
--- a/src/pretix/api/serializers/item.py
+++ b/src/pretix/api/serializers/item.py
@@ -239,7 +239,8 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
- 'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
+ 'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
+ 'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',
diff --git a/src/pretix/base/migrations/0249_hidden_if_item_available.py b/src/pretix/base/migrations/0249_hidden_if_item_available.py
new file mode 100644
index 0000000000..648f351cba
--- /dev/null
+++ b/src/pretix/base/migrations/0249_hidden_if_item_available.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.4 on 2023-10-30 11:50
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("pretixbase", "0248_item_free_price_suggestion"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="item",
+ name="hidden_if_item_available",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="pretixbase.item",
+ ),
+ ),
+ ]
diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index be2d67397a..cf02419658 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -857,6 +857,10 @@ class Event(EventMixin, LoggedModel):
v.item = i
v.save(force_insert=True)
+ for i in self.items.filter(hidden_if_item_available__isnull=False):
+ i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
+ i.save()
+
for imv in ItemMetaValue.objects.filter(item__event=other):
imv.pk = None
imv.property = item_meta_properties_map[imv.property_id]
diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py
index 44c2dbaff2..15750ff8c6 100644
--- a/src/pretix/base/models/items.py
+++ b/src/pretix/base/models/items.py
@@ -494,13 +494,24 @@ class Item(LoggedModel):
'Quota',
null=True, blank=True,
on_delete=models.SET_NULL,
- verbose_name=_("Only show after sellout of"),
+ verbose_name=pgettext_lazy("hidden_if_available_legacy", "Only show after sellout of"),
help_text=_("If you select a quota here, this product will only be shown when that quota is "
"unavailable. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once they are sold out. There might be a short period "
"in which both products are visible while all tickets in the referenced quota are reserved, "
"but not yet sold.")
)
+ hidden_if_item_available = models.ForeignKey(
+ 'Item',
+ null=True, blank=True,
+ on_delete=models.SET_NULL,
+ verbose_name=_("Only show after sellout of"),
+ help_text=_("If you select a product here, this product will only be shown when that product is "
+ "sold out. If combined with the option to hide sold-out products, this allows you to "
+ "swap out products for more expensive ones once the cheaper option is sold out. There might "
+ "be a short period in which both products are visible while all tickets of the referenced "
+ "product are reserved, but not yet sold.")
+ )
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,
diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py
index aec549810e..81d77d76c0 100644
--- a/src/pretix/control/forms/item.py
+++ b/src/pretix/control/forms/item.py
@@ -45,7 +45,7 @@ from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
from django.utils.functional import cached_property
-from django.utils.html import escape
+from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import (
gettext as __, gettext_lazy as _, pgettext_lazy,
@@ -387,6 +387,7 @@ class ItemCreateForm(I18nModelForm):
'allow_waitinglist',
'show_quota_left',
'hidden_if_available',
+ 'hidden_if_item_available',
'require_bundling',
'require_membership',
'grant_membership_type',
@@ -550,19 +551,43 @@ class ItemUpdateForm(I18nModelForm):
widget=forms.CheckboxSelectMultiple
)
change_decimal_field(self.fields['default_price'], self.event.currency)
- self.fields['hidden_if_available'].queryset = self.event.quotas.all()
- self.fields['hidden_if_available'].widget = Select2(
+
+ if self.instance.hidden_if_available_id:
+ self.fields['hidden_if_available'].queryset = self.event.quotas.all()
+ self.fields['hidden_if_available'].help_text = format_html(
+ "{} {}",
+ _("This option is deprecated. For new products, use the newer option below that refers to another "
+ "product instead of a quota."),
+ self.fields['hidden_if_available'].help_text
+ )
+ self.fields['hidden_if_available'].widget = Select2(
+ attrs={
+ 'data-model-select2': 'generic',
+ 'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
+ 'event': self.event.slug,
+ 'organizer': self.event.organizer.slug,
+ }),
+ 'data-placeholder': _('Shown independently of other products')
+ }
+ )
+ self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
+ self.fields['hidden_if_available'].required = False
+ else:
+ del self.fields['hidden_if_available']
+
+ self.fields['hidden_if_item_available'].queryset = self.event.items.exclude(id=self.instance.id)
+ self.fields['hidden_if_item_available'].widget = Select2(
attrs={
'data-model-select2': 'generic',
- 'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
+ 'data-select2-url': reverse('control:event.items.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('Shown independently of other products')
}
)
- self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
- self.fields['hidden_if_available'].required = False
+ self.fields['hidden_if_item_available'].widget.choices = self.fields['hidden_if_item_available'].choices
+ self.fields['hidden_if_item_available'].required = False
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['category'].widget = Select2(
@@ -683,6 +708,7 @@ class ItemUpdateForm(I18nModelForm):
'require_bundling',
'show_quota_left',
'hidden_if_available',
+ 'hidden_if_item_available',
'issue_giftcard',
'require_membership',
'require_membership_types',
@@ -709,6 +735,7 @@ class ItemUpdateForm(I18nModelForm):
'validity_fixed_from': SplitDateTimeField,
'validity_fixed_until': SplitDateTimeField,
'hidden_if_available': SafeModelChoiceField,
+ 'hidden_if_item_available': SafeModelChoiceField,
'grant_membership_type': SafeModelChoiceField,
'require_membership_types': SafeModelMultipleChoiceField,
}
diff --git a/src/pretix/control/templates/pretixcontrol/item/base.html b/src/pretix/control/templates/pretixcontrol/item/base.html
index 3fdd5603eb..6c0c4a59b6 100644
--- a/src/pretix/control/templates/pretixcontrol/item/base.html
+++ b/src/pretix/control/templates/pretixcontrol/item/base.html
@@ -32,6 +32,14 @@
{% endblocktrans %}
{% endif %}
+ {% if not request.event.has_subevents and object.hidden_if_item_available and object.hidden_if_item_available.check_quotas.0 == 100 %}
+
+ {% blocktrans trimmed %}
+ This product is currently not being shown since you configured below that it should only be visible
+ if a certain other product is already sold out.
+ {% endblocktrans %}
+