Example #1
0
def test_validate_membership_test_mode(event, customer, membership,
                                       requiring_ticket, membership_type):
    with pytest.raises(ValidationError) as excinfo:
        membership.testmode = True
        membership.save()
        validate_memberships_in_order(
            customer,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None,
            testmode=False,
        )
    assert "test mode" in str(excinfo.value)
    with pytest.raises(ValidationError) as excinfo:
        membership.testmode = False
        membership.save()
        validate_memberships_in_order(
            customer,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None,
            testmode=True,
        )
    assert "test mode" in str(excinfo.value)
Example #2
0
def test_validate_membership_max_usages(event, customer, membership,
                                        requiring_ticket, membership_type):
    membership_type.max_usages = 1
    membership_type.allow_parallel_usage = True
    membership_type.save()
    o1 = Order.objects.create(
        status=Order.STATUS_PENDING,
        event=event,
        email='admin@localhost',
        datetime=now() - timedelta(days=3),
        expires=now() + timedelta(days=11),
        total=Decimal("23"),
    )
    OrderPosition.objects.create(order=o1,
                                 item=requiring_ticket,
                                 used_membership=membership,
                                 variation=None,
                                 price=Decimal("23"),
                                 attendee_name_parts={'full_name': "Peter"})

    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(
            customer,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None)
    assert "more than 1 time" in str(excinfo.value)
    membership_type.max_usages = 2
    membership_type.save()
    validate_memberships_in_order(
        customer,
        [CartPosition(item=requiring_ticket, used_membership=membership)],
        event,
        lock=False,
        ignored_order=None)

    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(customer, [
            CartPosition(item=requiring_ticket, used_membership=membership),
            CartPosition(item=requiring_ticket, used_membership=membership),
        ],
                                      event,
                                      lock=False,
                                      ignored_order=None)
    assert "more than 2 times" in str(excinfo.value)
Example #3
0
    def _perform_operations(self):
        vouchers_ok = self._get_voucher_availability()
        quotas_ok = self._get_quota_availability()
        err = None
        new_cart_positions = []

        self._operations.sort(key=lambda a: self.order[type(a)])

        for op in self._operations:
            if isinstance(op, self.RemoveOperation):
                op.position.delete()

            elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
                # Create a CartPosition for as much items as we can
                requested_count = quota_available_count = voucher_available_count = op.count

                if op.quotas:
                    quota_available_count = min(requested_count, min(quotas_ok[q] for q in op.quotas))

                if op.voucher:
                    voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])

                if quota_available_count < 1:
                    err = err or error_messages['unavailable']
                elif quota_available_count < requested_count:
                    err = err or error_messages['in_part']

                if voucher_available_count < 1:
                    err = err or error_messages['voucher_redeemed']
                elif voucher_available_count < requested_count:
                    err = err or error_messages['voucher_redeemed_partial'] % voucher_available_count

                available_count = min(quota_available_count, voucher_available_count)

                for q in op.quotas:
                    quotas_ok[q] -= available_count
                if op.voucher:
                    vouchers_ok[op.voucher] -= available_count

                if isinstance(op, self.AddOperation):
                    for k in range(available_count):
                        new_cart_positions.append(CartPosition(
                            event=self.event, item=op.item, variation=op.variation,
                            price=op.price, expires=self._expiry,
                            cart_id=self.cart_id, voucher=op.voucher
                        ))
                elif isinstance(op, self.ExtendOperation):
                    if available_count == 1:
                        op.position.expires = self._expiry
                        op.position.price = op.price
                        op.position.save()
                    elif available_count == 0:
                        op.position.delete()
                    else:
                        raise AssertionError("ExtendOperation cannot affect more than one item")

        CartPosition.objects.bulk_create(new_cart_positions)
        return err
Example #4
0
def test_validate_membership_required(event, customer, membership,
                                      requiring_ticket, membership_type):
    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(customer,
                                      [CartPosition(item=requiring_ticket, )],
                                      event,
                                      lock=False,
                                      ignored_order=None)
    assert "requires an active" in str(excinfo.value)
Example #5
0
def test_validate_membership_wrong_type(event, customer, membership,
                                        requiring_ticket, membership_type):
    requiring_ticket.require_membership_types.clear()
    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(
            customer,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None)
    assert "not allowed for the product" in str(excinfo.value)
Example #6
0
def test_validate_membership_wrong_customer(event, customer, membership,
                                            requiring_ticket, membership_type):
    customer2 = event.organizer.customers.create(email="*****@*****.**")
    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(
            customer2,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None)
    assert "different customer" in str(excinfo.value)
Example #7
0
def test_validate_membership_wrong_date(event, customer, membership,
                                        requiring_ticket, membership_type):
    membership.date_start -= timedelta(days=100)
    membership.date_end -= timedelta(days=100)
    membership.save()
    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(
            customer,
            [CartPosition(item=requiring_ticket, used_membership=membership)],
            event,
            lock=False,
            ignored_order=None)
    assert "taking place at" in str(excinfo.value)
Example #8
0
def test_validate_membership_not_required(event, customer, membership,
                                          granting_ticket, membership_type):
    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(
            customer,
            [CartPosition(
                item=granting_ticket,
                used_membership=membership,
            )],
            event,
            lock=False,
            ignored_order=None)
    assert "does not require" in str(excinfo.value)
Example #9
0
def test_validate_membership_ensure_locking(event, customer, membership,
                                            requiring_ticket, membership_type,
                                            django_assert_num_queries):
    with django_assert_num_queries(4) as captured:
        validate_memberships_in_order(customer, [
            CartPosition(
                item=requiring_ticket,
                used_membership=membership,
            )
        ],
                                      event,
                                      lock=True,
                                      ignored_order=None)
    if 'sqlite' not in settings.DATABASES['default']['ENGINE']:
        assert any('FOR UPDATE' in s['sql'] for s in captured)
    def create_cart(self, sc, expires):
        with transaction.atomic():
            with self.request.event.lock():
                positions = []
                quotas = Counter()

                for form in self.formset.forms:
                    if '-' in form.cleaned_data['itemvar']:
                        itemid, varid = form.cleaned_data['itemvar'].split('-')
                    else:
                        itemid, varid = form.cleaned_data['itemvar'], None

                    item = Item.objects.get(pk=itemid,
                                            event=self.request.event)
                    variation = ItemVariation.objects.get(
                        pk=varid, item=item) if varid else None
                    price = form.cleaned_data['price']
                    if not price:
                        price = (variation.default_price if variation
                                 and variation.default_price is not None else
                                 item.default_price)

                    for quota in item.quotas.all():
                        quotas[quota] += form.cleaned_data['count']

                    for i in range(form.cleaned_data['count']):
                        positions.append(
                            CartPosition(item=item,
                                         variation=variation,
                                         event=self.request.event,
                                         cart_id=sc.cart_id,
                                         expires=expires,
                                         price=price))

                for quota, diff in quotas.items():
                    avail = quota.availability()
                    if avail[0] != Quota.AVAILABILITY_OK or (
                            avail[1] is not None and avail[1] < diff):
                        raise CartError(self.error_messages['quota'].format(
                            name=quota.name))

                sc.expires = expires
                sc.event = self.request.event
                sc.total = sum([p.price for p in positions])
                sc.save()
                CartPosition.objects.bulk_create(positions)
Example #11
0
def test_validate_membership_parallel(event, customer, membership, subevent,
                                      requiring_ticket, membership_type):
    se2 = event.subevents.create(
        name='Foo',
        date_from=TZ.localize(datetime(2021, 4, 28, 10, 0, 0, 0)),
    )

    membership_type.allow_parallel_usage = False
    membership_type.save()

    o1 = Order.objects.create(
        status=Order.STATUS_PENDING,
        event=event,
        email='admin@localhost',
        datetime=now() - timedelta(days=3),
        expires=now() + timedelta(days=11),
        total=Decimal("23"),
    )
    OrderPosition.objects.create(order=o1,
                                 item=requiring_ticket,
                                 used_membership=membership,
                                 variation=None,
                                 subevent=subevent,
                                 price=Decimal("23"),
                                 attendee_name_parts={'full_name': "Peter"})

    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(customer, [
            CartPosition(item=requiring_ticket,
                         used_membership=membership,
                         subevent=subevent)
        ],
                                      event,
                                      lock=False,
                                      ignored_order=None)
    assert "different ticket at the same time" in str(excinfo.value)

    validate_memberships_in_order(customer, [
        CartPosition(
            item=requiring_ticket, used_membership=membership, subevent=se2)
    ],
                                  event,
                                  lock=False,
                                  ignored_order=None)

    with pytest.raises(ValidationError) as excinfo:
        validate_memberships_in_order(customer, [
            CartPosition(item=requiring_ticket,
                         used_membership=membership,
                         subevent=se2),
            CartPosition(item=requiring_ticket,
                         used_membership=membership,
                         subevent=se2)
        ],
                                      event,
                                      lock=False,
                                      ignored_order=None)
    assert "different ticket at the same time" in str(excinfo.value)

    membership_type.allow_parallel_usage = True
    membership_type.save()
    validate_memberships_in_order(customer, [
        CartPosition(item=requiring_ticket,
                     used_membership=membership,
                     subevent=subevent)
    ],
                                  event,
                                  lock=False,
                                  ignored_order=None)
Example #12
0
    def _perform_operations(self):
        vouchers_ok = self._get_voucher_availability()
        quotas_ok = self._get_quota_availability()
        err = None
        new_cart_positions = []

        err = err or self._check_min_per_product()

        self._operations.sort(key=lambda a: self.order[type(a)])

        for op in self._operations:
            if isinstance(op, self.RemoveOperation):
                if op.position.expires > self.now_dt:
                    for q in op.position.quotas:
                        quotas_ok[q] += 1
                op.position.addons.all().delete()
                op.position.delete()

            elif isinstance(op, self.AddOperation) or isinstance(
                    op, self.ExtendOperation):
                # Create a CartPosition for as much items as we can
                requested_count = quota_available_count = voucher_available_count = op.count

                if op.quotas:
                    quota_available_count = min(
                        requested_count, min(quotas_ok[q] for q in op.quotas))

                if op.voucher:
                    voucher_available_count = min(voucher_available_count,
                                                  vouchers_ok[op.voucher])

                if quota_available_count < 1:
                    err = err or error_messages['unavailable']
                elif quota_available_count < requested_count:
                    err = err or error_messages['in_part']

                if voucher_available_count < 1:
                    if op.voucher in self._voucher_depend_on_cart:
                        err = err or error_messages[
                            'voucher_redeemed_cart'] % self.event.settings.reservation_time
                    else:
                        err = err or error_messages['voucher_redeemed']
                elif voucher_available_count < requested_count:
                    err = err or error_messages[
                        'voucher_redeemed_partial'] % voucher_available_count

                available_count = min(quota_available_count,
                                      voucher_available_count)

                for q in op.quotas:
                    quotas_ok[q] -= available_count
                if op.voucher:
                    vouchers_ok[op.voucher] -= available_count

                if isinstance(op, self.AddOperation):
                    for k in range(available_count):
                        new_cart_positions.append(
                            CartPosition(
                                event=self.event,
                                item=op.item,
                                variation=op.variation,
                                price=op.price.gross,
                                expires=self._expiry,
                                cart_id=self.cart_id,
                                voucher=op.voucher,
                                addon_to=op.addon_to if op.addon_to else None,
                                subevent=op.subevent,
                                includes_tax=op.includes_tax))
                elif isinstance(op, self.ExtendOperation):
                    if available_count == 1:
                        op.position.expires = self._expiry
                        op.position.price = op.price.gross
                        op.position.save()
                    elif available_count == 0:
                        op.position.delete()
                    else:
                        raise AssertionError(
                            "ExtendOperation cannot affect more than one item")

        CartPosition.objects.bulk_create(new_cart_positions)
        return err
Example #13
0
    def _perform_operations(self):
        vouchers_ok = self._get_voucher_availability()
        quotas_ok = self._get_quota_availability()
        err = None
        new_cart_positions = []

        err = err or self._check_min_per_product()

        self._operations.sort(key=lambda a: self.order[type(a)])

        for op in self._operations:
            if isinstance(op, self.RemoveOperation):
                if op.position.expires > self.now_dt:
                    for q in op.position.quotas:
                        quotas_ok[q] += 1
                op.position.addons.all().delete()
                op.position.delete()

            elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
                # Create a CartPosition for as much items as we can
                requested_count = quota_available_count = voucher_available_count = op.count

                if op.quotas:
                    quota_available_count = min(requested_count, min(quotas_ok[q] for q in op.quotas))

                if op.voucher:
                    voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])

                if quota_available_count < 1:
                    err = err or error_messages['unavailable']
                elif quota_available_count < requested_count:
                    err = err or error_messages['in_part']

                if voucher_available_count < 1:
                    if op.voucher in self._voucher_depend_on_cart:
                        err = err or error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time
                    else:
                        err = err or error_messages['voucher_redeemed']
                elif voucher_available_count < requested_count:
                    err = err or error_messages['voucher_redeemed_partial'] % voucher_available_count

                available_count = min(quota_available_count, voucher_available_count)

                for q in op.quotas:
                    quotas_ok[q] -= available_count
                if op.voucher:
                    vouchers_ok[op.voucher] -= available_count

                if isinstance(op, self.AddOperation):
                    for k in range(available_count):
                        cp = CartPosition(
                            event=self.event, item=op.item, variation=op.variation,
                            price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
                            voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
                            subevent=op.subevent, includes_tax=op.includes_tax
                        )
                        if self.event.settings.attendee_names_asked:
                            scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
                            if 'attendee-name' in self._widget_data:
                                cp.attendee_name_parts = {'_legacy': self._widget_data['attendee-name']}
                            if any('attendee-name-{}'.format(k.replace('_', '-')) in self._widget_data for k, l, w
                                   in scheme['fields']):
                                cp.attendee_name_parts = {
                                    k: self._widget_data.get('attendee-name-{}'.format(k.replace('_', '-')), '')
                                    for k, l, w in scheme['fields']
                                }
                        if self.event.settings.attendee_emails_asked and 'email' in self._widget_data:
                            cp.attendee_email = self._widget_data.get('email')

                        cp._answers = {}
                        for k, v in self._widget_data.items():
                            if not k.startswith('question-'):
                                continue
                            q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
                            if q:
                                try:
                                    cp._answers[q] = q.clean_answer(v)
                                except ValidationError:
                                    pass

                        new_cart_positions.append(cp)
                elif isinstance(op, self.ExtendOperation):
                    if available_count == 1:
                        op.position.expires = self._expiry
                        op.position.price = op.price.gross
                        op.position.save()
                    elif available_count == 0:
                        op.position.delete()
                    else:
                        raise AssertionError("ExtendOperation cannot affect more than one item")

        for p in new_cart_positions:
            if p._answers:
                p.save()
                _save_answers(p, {}, p._answers)
        CartPosition.objects.bulk_create([p for p in new_cart_positions if not p._answers])
        return err
Example #14
0
    def _perform_operations(self):
        vouchers_ok = self._get_voucher_availability()
        quotas_ok = self._get_quota_availability()
        err = None
        new_cart_positions = []

        err = err or self._check_min_per_product()

        self._operations.sort(key=lambda a: self.order[type(a)])

        for op in self._operations:
            if isinstance(op, self.RemoveOperation):
                if op.position.expires > self.now_dt:
                    for q in op.position.quotas:
                        quotas_ok[q] += 1
                op.position.addons.all().delete()
                op.position.delete()

            elif isinstance(op, self.AddOperation) or isinstance(
                    op, self.ExtendOperation):
                # Create a CartPosition for as much items as we can
                requested_count = quota_available_count = voucher_available_count = op.count

                if op.quotas:
                    quota_available_count = min(
                        requested_count, min(quotas_ok[q] for q in op.quotas))

                if op.voucher:
                    voucher_available_count = min(voucher_available_count,
                                                  vouchers_ok[op.voucher])

                if quota_available_count < 1:
                    err = err or error_messages['unavailable']
                elif quota_available_count < requested_count:
                    err = err or error_messages['in_part']

                if voucher_available_count < 1:
                    if op.voucher in self._voucher_depend_on_cart:
                        err = err or error_messages[
                            'voucher_redeemed_cart'] % self.event.settings.reservation_time
                    else:
                        err = err or error_messages['voucher_redeemed']
                elif voucher_available_count < requested_count:
                    err = err or error_messages[
                        'voucher_redeemed_partial'] % voucher_available_count

                available_count = min(quota_available_count,
                                      voucher_available_count)

                for q in op.quotas:
                    quotas_ok[q] -= available_count
                if op.voucher:
                    vouchers_ok[op.voucher] -= available_count

                if isinstance(op, self.AddOperation):
                    for k in range(available_count):
                        cp = CartPosition(
                            event=self.event,
                            item=op.item,
                            variation=op.variation,
                            price=op.price.gross,
                            expires=self._expiry,
                            cart_id=self.cart_id,
                            voucher=op.voucher,
                            addon_to=op.addon_to if op.addon_to else None,
                            subevent=op.subevent,
                            includes_tax=op.includes_tax)
                        if self.event.settings.attendee_names_asked:
                            scheme = PERSON_NAME_SCHEMES.get(
                                self.event.settings.name_scheme)
                            if 'attendee-name' in self._widget_data:
                                cp.attendee_name_parts = {
                                    '_legacy':
                                    self._widget_data['attendee-name']
                                }
                            if any('attendee-name-{}'.format(
                                    k.replace('_', '-')) in self._widget_data
                                   for k, l, w in scheme['fields']):
                                cp.attendee_name_parts = {
                                    k: self._widget_data.get(
                                        'attendee-name-{}'.format(
                                            k.replace('_', '-')), '')
                                    for k, l, w in scheme['fields']
                                }
                        if self.event.settings.attendee_emails_asked and 'email' in self._widget_data:
                            cp.attendee_email = self._widget_data.get('email')

                        cp._answers = {}
                        for k, v in self._widget_data.items():
                            if not k.startswith('question-'):
                                continue
                            q = cp.item.questions.filter(
                                ask_during_checkin=False,
                                identifier__iexact=k[9:]).first()
                            if q:
                                try:
                                    cp._answers[q] = q.clean_answer(v)
                                except ValidationError:
                                    pass

                        new_cart_positions.append(cp)
                elif isinstance(op, self.ExtendOperation):
                    if available_count == 1:
                        op.position.expires = self._expiry
                        op.position.price = op.price.gross
                        op.position.save()
                    elif available_count == 0:
                        op.position.addons.all().delete()
                        op.position.delete()
                    else:
                        raise AssertionError(
                            "ExtendOperation cannot affect more than one item")

        for p in new_cart_positions:
            if p._answers:
                p.save()
                _save_answers(p, {}, p._answers)
        CartPosition.objects.bulk_create(
            [p for p in new_cart_positions if not p._answers])
        return err
Example #15
0
File: cart.py Project: akuks/pretix
    def process(self):
        expiry = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int))
        self._extend_existing(expiry)

        self._initial_checks()

        # Fetch items from the database
        items_cache = {
            i.identity: i for i
            in Item.objects.current.filter(
                event=self.request.event,
                identity__in=[i[0] for i in self.items]
            ).prefetch_related("quotas")
        }
        variations_cache = {
            v.identity: v for v
            in ItemVariation.objects.current.filter(
                item__event=self.request.event,
                identity__in=[i[1] for i in self.items if i[1] is not None]
            ).select_related("item", "item__event").prefetch_related("quotas", "values", "values__prop")
        }
        try:
            with self.request.event.lock():
                # Process the request itself
                for i in self.items:
                    # Check whether the specified items are part of what we just fetched from the database
                    # If they are not, the user supplied item IDs which either do not exist or belong to
                    # a different event
                    if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache):
                        self.error_message(self.error_messages['not_for_sale'])
                        return redirect(self.get_failure_url())

                    item = items_cache[i[0]]
                    variation = variations_cache[i[1]] if i[1] is not None else None

                    # Execute restriction plugins to check whether they (a) change the price or
                    # (b) make the item/variation unavailable. If neither is the case, check_restriction
                    # will correctly return the default price
                    price = item.check_restrictions() if variation is None else variation.check_restrictions()

                    # Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
                    quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())

                    if price is False or len(quotas) == 0 or not item.active:
                        self.error_message(self.error_messages['unavailable'])
                        continue

                    # Assume that all quotas allow us to buy i[2] instances of the object
                    quota_ok = i[2]
                    for quota in quotas:
                        avail = quota.availability()
                        if avail[1] < i[2]:
                            # This quota is not available or less than i[2] items are left, so we have to
                            # reduce the number of bought items
                            self.error_message(
                                self.error_messages['unavailable']
                                if avail[0] != Quota.AVAILABILITY_OK
                                else self.error_messages['in_part']
                            )
                            quota_ok = min(quota_ok, avail[1])

                    # Create a CartPosition for as much items as we can
                    for k in range(quota_ok):
                        if len(i) > 3 and i[2] == 1:
                            # Recreating
                            cp = i[3].clone()
                            cp.expires = expiry
                            cp.price = price
                            cp.save()
                        else:
                            cp = CartPosition(
                                event=self.request.event,
                                item=item,
                                variation=variation,
                                price=price,
                                expires=expiry
                            )
                            if self.request.user.is_authenticated():
                                cp.user = self.request.user
                            else:
                                cp.session = self.request.session.session_key
                            cp.save()

                self._delete_expired()

            if not self.msg_some_unavailable:
                messages.success(self.request, _('The products have been successfully added to your cart.'))

            return redirect(self.get_success_url())
        except EventLock.LockTimeoutException:
            # Is raised when there are too many threads asking for quota locks and we were
            # unaible to get one
            self.error_message(self.error_messages['busy'], important=True)
Example #16
0
    def _perform_operations(self):
        vouchers_ok = self._get_voucher_availability()
        quotas_ok = self._get_quota_availability()
        err = None
        new_cart_positions = []

        err = err or self._check_min_per_product()

        self._operations.sort(key=lambda a: self.order[type(a)])

        for op in self._operations:
            if isinstance(op, self.RemoveOperation):
                if op.position.expires > self.now_dt:
                    for q in op.position.quotas:
                        quotas_ok[q] += 1
                op.position.addons.all().delete()
                op.position.delete()

            elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
                # Create a CartPosition for as much items as we can
                requested_count = quota_available_count = voucher_available_count = op.count

                if op.quotas:
                    quota_available_count = min(requested_count, min(quotas_ok[q] for q in op.quotas))

                if op.voucher:
                    voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])

                if quota_available_count < 1:
                    err = err or error_messages['unavailable']
                elif quota_available_count < requested_count:
                    err = err or error_messages['in_part']

                if voucher_available_count < 1:
                    if op.voucher in self._voucher_depend_on_cart:
                        err = err or error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time
                    else:
                        err = err or error_messages['voucher_redeemed']
                elif voucher_available_count < requested_count:
                    err = err or error_messages['voucher_redeemed_partial'] % voucher_available_count

                available_count = min(quota_available_count, voucher_available_count)

                if isinstance(op, self.AddOperation):
                    for b in op.bundled:
                        b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b.quotas))
                        if b_quota_available_count < b.count:
                            err = err or error_messages['unavailable']
                            available_count = 0
                        elif b_quota_available_count < available_count * b.count:
                            err = err or error_messages['in_part']
                            available_count = b_quota_available_count // b.count
                        for q in b.quotas:
                            quotas_ok[q] -= available_count * b.count
                            # TODO: is this correct?

                for q in op.quotas:
                    quotas_ok[q] -= available_count
                if op.voucher:
                    vouchers_ok[op.voucher] -= available_count

                if any(qa < 0 for qa in quotas_ok.values()):
                    # Safeguard, shouldn't happen
                    err = err or error_messages['unavailable']
                    available_count = 0

                if isinstance(op, self.AddOperation):
                    for k in range(available_count):
                        cp = CartPosition(
                            event=self.event, item=op.item, variation=op.variation,
                            price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
                            voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
                            subevent=op.subevent, includes_tax=op.includes_tax
                        )
                        if self.event.settings.attendee_names_asked:
                            scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
                            if 'attendee-name' in self._widget_data:
                                cp.attendee_name_parts = {'_legacy': self._widget_data['attendee-name']}
                            if any('attendee-name-{}'.format(k.replace('_', '-')) in self._widget_data for k, l, w
                                   in scheme['fields']):
                                cp.attendee_name_parts = {
                                    k: self._widget_data.get('attendee-name-{}'.format(k.replace('_', '-')), '')
                                    for k, l, w in scheme['fields']
                                }
                        if self.event.settings.attendee_emails_asked and 'email' in self._widget_data:
                            cp.attendee_email = self._widget_data.get('email')

                        cp._answers = {}
                        for k, v in self._widget_data.items():
                            if not k.startswith('question-'):
                                continue
                            q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
                            if q:
                                try:
                                    cp._answers[q] = q.clean_answer(v)
                                except ValidationError:
                                    pass

                        if op.bundled:
                            cp.save()  # Needs to be in the database already so we have a PK that we can reference
                            for b in op.bundled:
                                for j in range(b.count):
                                    new_cart_positions.append(CartPosition(
                                        event=self.event, item=b.item, variation=b.variation,
                                        price=b.price.gross, expires=self._expiry, cart_id=self.cart_id,
                                        voucher=None, addon_to=cp,
                                        subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True
                                    ))

                        new_cart_positions.append(cp)
                elif isinstance(op, self.ExtendOperation):
                    if available_count == 1:
                        op.position.expires = self._expiry
                        op.position.price = op.price.gross
                        try:
                            op.position.save(force_update=True)
                        except DatabaseError:
                            # Best effort... The position might have been deleted in the meantime!
                            pass
                    elif available_count == 0:
                        op.position.addons.all().delete()
                        op.position.delete()
                    else:
                        raise AssertionError("ExtendOperation cannot affect more than one item")

        for p in new_cart_positions:
            if getattr(p, '_answers', None):
                if not p.pk:  # We stored some to the database already before
                    p.save()
                _save_answers(p, {}, p._answers)
        CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
        return err