diff --git a/src/pretix/presale/productlist.py b/src/pretix/presale/productlist.py index f34ee01d8e..538800d4b1 100644 --- a/src/pretix/presale/productlist.py +++ b/src/pretix/presale/productlist.py @@ -459,3 +459,16 @@ def _get_variant_unavailability_reason(variant, now_dt: Optional[datetime]=None, return 'available_until' else: return None + + +def _item_num_options(item): + if item.current_unavailability_reason: + return 0 + if item.has_variations: + return len([v for v in item.available_variations if not v.current_unavailability_reason]) + else: + return 1 + + +def get_item_option_count(items): + return sum(_item_num_options(item) for item in items) diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html index 13daae66c3..eb695663ef 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html @@ -31,7 +31,7 @@ {% endif %} {% for item in items %} {% if item.has_variations %} -
+
{% if item.picture %} @@ -85,7 +85,7 @@ {% endif %}
- {% if not event.settings.show_variations_expanded %} + {% if not event.settings.show_variations_expanded and not always_show_open %} {% if item.best_variation_availability <= 10 %} {% if not item.min_price %} {% trans "FULLY BOOKED" %} @@ -116,7 +116,7 @@
-
+
{% for var in item.available_variations %}
{% if var.cached_availability.0 == 100 and not item.current_unavailability_reason and not var.current_unavailability_reason %}
- {% if var.order_max == 1 %} - + {% else %} +
+ {% trans "Quantity" %} + + + +
+ {% endif %} {% endif %}
{% else %} @@ -387,37 +404,52 @@
{% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
- {% if item.order_max == 1 %} - + {% else %} +
+ {% trans "Quantity" %} + + + +
+ {% endif %} {% endif %}
{% else %} diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html index 72913c6a6b..5d6f5fa1b2 100644 --- a/src/pretix/presale/templates/pretixpresale/event/voucher.html +++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html @@ -38,7 +38,7 @@ {% endif %}

- {% if options == 0 and not seating_available %} + {% if itemnum == 0 and not seating_available %} {% if request.event.has_subevents and not voucher.subevent %} {% blocktrans trimmed %} For the selected date, there are currently no products available that can be bought with this voucher. Please try a different date or a different voucher. @@ -73,353 +73,10 @@ {% endif %} {% endif %} - {% for tup in items_by_category %} -

- {% if tup.0 %} -

{{ tup.0.name }}

- {% if tup.0.description %} -
{{ tup.0.description|localize|rich_text }}
- {% endif %} - {% endif %} - {% for item in tup.1 %} - {% if item.has_variations %} -
-
-
- {% if item.picture %} - - {{ item.name }} - - {% endif %} -
-

- {{ item.name }} -

- {% if item.description %} -
- {{ item.description|localize|rich_text }} -
- {% endif %} -
-
-
- {% if item.min_price != item.max_price or item.free_price %} - {% blocktrans trimmed with minprice=item.min_price|money:event.currency %} - from {{ minprice }} - {% endblocktrans %} - {% elif not item.min_price and not item.max_price %} - {% if not item.mandatory_priced_addons or voucher.all_addons_included %} - {% trans "free" context "price" %} - {% endif %} - {% else %} - {{ item.min_price|money:event.currency }} - {% endif %} -
-
-
-
-
-
- {% for var in item.available_variations %} -
-
-
{{ var }}
- {% if var.description %} -
- {{ var.description|localize|rich_text }} -
- {% endif %} - {% if item.do_show_quota_left %} - {% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %} - {% endif %} -
-
- {% if var.original_price %} -

- {% trans "Original price:" %} - {% if event.settings.display_net_prices %} - {{ var.original_price.net|money:event.currency }} - {% else %} - {{ var.original_price.gross|money:event.currency }} - {% endif %} - - {% if item.free_price %} -

- {% else %} - {% trans "New price:" %} - {% endif %} - {% elif not item.free_price %} -

- {% endif %} - {% if item.free_price %} - -

- {{ event.currency }} - -
-

- {% elif not var.display_price.gross %} - {% if not item.mandatory_priced_addons or var.original_price or voucher.all_addons_included %} - {% trans "free" context "price" %} - {% endif %} - {% elif event.settings.display_net_prices %} - {{ var.display_price.net|money:event.currency }} - {% else %} - {{ var.display_price.gross|money:event.currency }} - {% endif %} - {% if item.original_price or var.original_price %} - - {% endif %} - {% if item.includes_mixed_tax_rate %} - {% if event.settings.display_net_prices %} - {% trans "plus taxes" %} - {% else %} - {% trans "incl. taxes" %} - {% endif %} - {% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %} - - {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %} - plus {{ rate }}% {{ name }} - {% endblocktrans %} - - {% elif var.display_price.rate and var.display_price.gross %} - - {% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %} - incl. {{ rate }}% {{ name }} - {% endblocktrans %} - - {% endif %} -

-
- {% if var.cached_availability.0 == 100 and not item.current_unavailability_reason %} -
- {% if max_times > 1 %} - {% if var.order_max == 1 %} - - {% else %} -
- {% trans "Quantity" %} - - - -
- {% endif %} - {% else %} - - {% endif %} -
- {% else %} - {% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross original_price=var.original_price.gross avail=var.cached_availability.0 item=item %} - {% endif %} -
-
- {% endfor %} -
-
- {% else %} -
-
- {% if item.picture %} - - {{ item.name }} - - {% endif %} -
-

{{ item.name }}

- {% if item.description %} -
- {{ item.description|localize|rich_text }} -
- {% endif %} - {% if item.do_show_quota_left %} - {% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %} - {% endif %} - {% if item.min_per_order and item.min_per_order > 1 %} -

- - {% blocktrans trimmed with num=item.min_per_order %} - minimum amount to order: {{ num }} - {% endblocktrans %} - -

- {% endif %} -
-
-
- {% if item.original_price %} -

- {% trans "Original price:" %} - {% if event.settings.display_net_prices %} - {{ item.original_price.net|money:event.currency }} - {% else %} - {{ item.original_price.gross|money:event.currency }} - {% endif %} - - {% if item.free_price %} -

- {% else %} - {% trans "New price:" %} - {% endif %} - {% elif not item.free_price %} -

- {% endif %} - {% if item.free_price %} - -

- {{ event.currency }} - -
-

- {% elif not item.display_price.gross %} - {% if not item.mandatory_priced_addons or item.original_price or voucher.all_addons_included %} - {% trans "free" context "price" %} - {% endif %} - {% elif event.settings.display_net_prices %} - {{ item.display_price.net|money:event.currency }} - {% else %} - {{ item.display_price.gross|money:event.currency }} - {% endif %} - {% if item.original_price %} - - {% endif %} - {% if item.includes_mixed_tax_rate %} - {% if event.settings.display_net_prices %} - {% trans "plus taxes" %} - {% else %} - {% trans "incl. taxes" %} - {% endif %} - {% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %} - - {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %} - plus {{ rate }}% {{ name }} - {% endblocktrans %} - - {% elif item.display_price.rate and item.display_price.gross %} - - {% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %} - incl. {{ rate }}% {{ name }} - {% endblocktrans %} - - {% endif %} -

-
- {% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %} -
- {% if max_times > 1 %} - {% if item.order_max == 1 %} - - {% else %} -
- {% trans "Quantity" %} - - - -
- {% endif %} - {% else %} - - {% endif %} -
- {% else %} - {% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross original_price=item.original_price.gross avail=item.cached_availability.0 item=item %} - {% endif %} -
-
- {% endif %} - {% endfor %} -
- {% endfor %} + {% include "pretixpresale/event/fragment_product_list.html" with ev=event always_show_open=True %} + {% eventsignal event "pretix.presale.signals.voucher_redeem_info" voucher=voucher %} - {% if event.presale_is_running and options > 0 %} + {% if event.presale_is_running and itemnum > 0 %}
{% if voucher.min_usages_remaining > 1 %} diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 6285c67fc1..43728b6170 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -70,7 +70,7 @@ from pretix.helpers.http import redirect_to_url from pretix.helpers.safedownload import check_token from pretix.multidomain.urlreverse import eventreverse from pretix.presale.productlist import ( - item_group_by_category, prepare_item_list_for_shop, + get_item_option_count, item_group_by_category, prepare_item_list_for_shop, ) from pretix.presale.views import ( CartMixin, EventViewMixin, allow_cors_if_namespaced, @@ -239,9 +239,9 @@ def _items_from_post_data(request, warn_if_empty=True): # Compatibility patch that makes the frontend code a lot easier req_items = list(request.POST.lists()) - if '_voucher_item' in request.POST and '_voucher_code' in request.POST: + if '_selected_item' in request.POST: req_items.append(( - '%s' % request.POST['_voucher_item'], ('1',) + '%s' % request.POST['_selected_item'], ('1',) )) pass @@ -669,7 +669,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView context = super().get_context_data(**kwargs) context['voucher'] = self.voucher - context['max_times'] = self.voucher.max_usages - self.voucher.redeemed + context['max_choices'] = self.voucher.max_usages - self.voucher.redeemed # Fetch all items items, display_add_to_cart = prepare_item_list_for_shop( @@ -687,8 +687,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView # Calculate how many options the user still has. If there is only one option, we can # check the box right away ;) - context['options'] = sum([(len(item.available_variations) if item.has_variations else 1) - for item in items]) + context['itemnum'] = get_item_option_count(items) context['allfree'] = all( item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index fe54f21eb8..e924ddfe27 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -73,7 +73,7 @@ from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.ical import get_public_ical from pretix.presale.productlist import ( - item_group_by_category, prepare_item_list_for_shop, + get_item_option_count, item_group_by_category, prepare_item_list_for_shop, ) from pretix.presale.signals import seatingframe_html_head from pretix.presale.views.organizer import ( @@ -228,7 +228,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): break items = [i for i in items if not i.requires_seat] - context['itemnum'] = len(items) + context['itemnum'] = get_item_option_count(items) context['allfree'] = all( item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons for item in items if not item.has_variations diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js index 4543b8bc84..6639f8d47a 100644 --- a/src/pretix/static/pretixpresale/js/ui/main.js +++ b/src/pretix/static/pretixpresale/js/ui/main.js @@ -713,7 +713,7 @@ $(function () { $('[data-checked-onchange]').each(function () { let countInput = this $('#' + this.getAttribute('data-checked-onchange')).on('input', function () { - if (countInput.type === 'checkbox') { + if (countInput.type === 'checkbox' || countInput.type == "radio") { if (countInput.checked) return countInput.checked = true } else if (countInput.type === 'number' && !countInput.valueAsNumber) { diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index 25e405ed6e..d0bf3a3eb5 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -913,7 +913,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): self.q.size = 0 self.q.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) - assert "_voucher_item" not in html.rendered_content + assert "_selected_item" not in html.rendered_content def test_sold_out_blocking(self): self.q.size = 0 @@ -921,7 +921,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): self.v.block_quota = True self.v.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) - assert "_voucher_item" in html.rendered_content + assert "_selected_item" in html.rendered_content def test_sold_out_ignore(self): self.q.size = 0 @@ -929,7 +929,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): self.v.allow_ignore_quota = True self.v.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) - assert "_voucher_item" in html.rendered_content + assert "_selected_item" in html.rendered_content def test_variations_sold_out(self): with scopes_disabled(): @@ -940,7 +940,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): self.q.size = 0 self.q.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) - assert "_voucher_item" not in html.rendered_content + assert "_selected_item" not in html.rendered_content def test_variations_sold_out_blocking(self): with scopes_disabled(): @@ -953,7 +953,27 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): self.v.block_quota = True self.v.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) - assert "_voucher_item" in html.rendered_content + assert "_selected_item" in html.rendered_content + + def test_variation_not_available(self): + with scopes_disabled(): + var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1) + var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2, available_from=now() + datetime.timedelta(days=2)) + var3 = ItemVariation.objects.create(item=self.item, value='Purple', position=3, + available_from=now() + datetime.timedelta(days=2), available_from_mode=Item.UNAVAIL_MODE_INFO) + self.q.variations.add(var1) + self.q.variations.add(var2) + self.q.variations.add(var3) + self.q.size = 0 + self.q.save() + self.v.block_quota = True + self.v.save() + html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) + assert "_selected_item" in html.rendered_content + assert "Black" not in html.rendered_content + assert "Red" in html.rendered_content + assert "Purple" in html.rendered_content + assert "Not available yet" in html.rendered_content def test_voucher_price(self): self.v.value = Decimal("10.00")