diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 6ae14591c2..7e3d53db1b 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -181,10 +181,11 @@ class WaitingListEntry(LoggedModel): block_quota=True, item_id=self.item_id, subevent_id=self.subevent_id, - waitinglistentries__isnull=False + waitinglistentries__isnull=False, + seat__isnull=True ).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0 free_seats = num_free_seats_for_product - num_valid_vouchers_for_product - if not free_seats: + if free_seats < 1: raise WaitingListException(_('No seat with this product is currently available.')) if '@' not in self.email: diff --git a/src/pretix/control/views/waitinglist.py b/src/pretix/control/views/waitinglist.py index cbfd335b5c..bde434133d 100644 --- a/src/pretix/control/views/waitinglist.py +++ b/src/pretix/control/views/waitinglist.py @@ -280,11 +280,12 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa block_quota=True, item_id=wle.item_id, subevent=wle.subevent_id, - waitinglistentries__isnull=False + waitinglistentries__isnull=False, + seat__isnull=True ).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0 free_seats = num_free_seats_for_product - num_valid_vouchers_for_product wle.availability = ( - Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0], + Quota.AVAILABILITY_GONE if free_seats < 1 else wle.availability[0], min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats, ) diff --git a/src/tests/control/test_waitinglist.py b/src/tests/control/test_waitinglist.py index 178d00bd9d..c23caeda44 100644 --- a/src/tests/control/test_waitinglist.py +++ b/src/tests/control/test_waitinglist.py @@ -29,6 +29,8 @@ from pretix.base.models import ( Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher, WaitingListEntry, ) +from pretix.base.models.seating import Seat, SeatingPlan +from pretix.base.models.waitinglist import WaitingListException from pretix.control.views.dashboards import waitinglist_widgets @@ -55,11 +57,11 @@ def env(): WaitingListEntry.objects.create( event=event, item=item1, email='success@example.org', voucher=v ) - v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5)) + v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5)) WaitingListEntry.objects.create( event=event, item=item2, email='expired@example.org', voucher=v ) - v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5)) + v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5)) WaitingListEntry.objects.create( event=event, item=item2, email='valid@example.org', voucher=v ) @@ -345,5 +347,75 @@ def test_dashboard(client, env): quota.items.add(env['item1']) w = waitinglist_widgets(env['event']) - assert '1' in w[0]['content'] + assert '2' in w[0]['content'] assert '5' in w[1]['content'] + + +@pytest.mark.django_db +def test_waitinglist_seat_calc(client, env): + item = env['item1'] + event = env['event'] + wle = env['wle'] + + SeatingPlan.objects.create( + name="Plan", organizer=event.organizer, layout="{}" + ) + event.seat_category_mappings.create( + layout_category='Stalls', product=item + ) + for i in range(2): + event.seats.create(seat_number=f"A{i}", product=item, seat_guid=f"A{i}") + + quota = Quota.objects.create(event=event, size=10) + quota.items.add(item) + + client.login(email='dummy@dummy.dummy', password='dummy') + + # Calculated availability should not be more than number of available seats + response = client.get('/control/event/dummy/dummy/waitinglist/') + assert len(response.context['entries']) == 5 + for entry in response.context['entries']: + assert entry.availability == (Quota.AVAILABILITY_OK, 2) + + # Sending out a voucher reduces availability by 1 + with scopes_disabled(): + wle.send_voucher() + + voucher = wle.voucher + assert voucher + + response = client.get('/control/event/dummy/dummy/waitinglist/') + assert len(response.context['entries']) == 4 + for entry in response.context['entries']: + assert entry.availability == (Quota.AVAILABILITY_OK, 1) + + # Assigning a seat to a voucher does not decrease availability further + with scopes_disabled(): + voucher.seat = Seat.objects.get(seat_guid="A0") + voucher.save() + + response = client.get('/control/event/dummy/dummy/waitinglist/') + assert len(response.context['entries']) == 4 + for entry in response.context['entries']: + assert entry.availability == (Quota.AVAILABILITY_OK, 1) + + with scopes_disabled(): + wle2 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first() + wle2.send_voucher() + + # Overbooking is handled correctly + # Regression test for calculation that used `not free_seats` instead of `free_seats < 1` + with scopes_disabled(): + # Block seat + seat = Seat.objects.get(seat_guid="A1") + seat.blocked = True + seat.save() + + response = client.get('/control/event/dummy/dummy/waitinglist/') + assert len(response.context['entries']) == 3 + for entry in response.context['entries']: + assert entry.availability == (Quota.AVAILABILITY_GONE, -1) + + with scopes_disabled(), pytest.raises(WaitingListException): + wle3 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first() + wle3.send_voucher()