Seat-specific vouchers (#1486)

* Basic functionality

* API

* Do not delete seats with vouchers

* Show seat in list of seats

* Validate availability of seats

* Fix invalid logic in Seat.is_available

* Show voucher name in edit form
This commit is contained in:
Raphael Michel
2019-11-15 10:56:34 +01:00
committed by GitHub
parent f79df47b78
commit a2c1c69d7e
19 changed files with 474 additions and 42 deletions

View File

@@ -80,6 +80,7 @@ error_messages = {
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
'voucher_item_not_available': _(
'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
@@ -252,6 +253,9 @@ class CartManager:
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
if op.voucher and op.voucher.seat and op.voucher.seat != op.seat:
raise CartError(error_messages['voucher_invalid_seat'])
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
raise CartError(error_messages['voucher_invalid_subevent'])
@@ -824,7 +828,7 @@ class CartManager:
available_count = 0
if isinstance(op, self.AddOperation):
if op.seat and not op.seat.is_available():
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None):
available_count = 0
err = err or error_messages['seat_unavailable']
@@ -872,7 +876,8 @@ class CartManager:
new_cart_positions.append(cp)
elif isinstance(op, self.ExtendOperation):
if op.seat and not op.seat.is_available(ignore_cart=op.position):
if op.seat and not op.seat.is_available(ignore_cart=op.position,
ignore_voucher_id=op.position.voucher_id):
err = err or error_messages['seat_unavailable']
op.position.addons.all().delete()
op.position.delete()

View File

@@ -509,9 +509,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
break
if cp.seat:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every time, since we absolutely
# can not overbook a seat.
if not cp.seat.is_available(ignore_cart=cp) or cp.seat.blocked:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
# time, since we absolutely can not overbook a seat.
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id) or cp.seat.blocked:
err = err or error_messages['seat_unavailable']
cp.delete()
continue

View File

@@ -10,10 +10,9 @@ class SeatProtected(Exception):
def validate_plan_change(event, subevent, plan):
current_taken_seats = set(
event.seats.select_related('product')
.annotate(has_op=Count('orderposition'))
.filter(subevent=subevent, has_op=True)
.values_list('seat_guid', flat=True)
event.seats.select_related('product').annotate(
has_op=Count('orderposition')
).annotate(has_v=Count('vouchers')).filter(subevent=subevent, has_op=True).values_list('seat_guid', flat=True)
)
new_seats = {
ss.guid for ss in plan.iter_all_seats()
@@ -26,7 +25,9 @@ def validate_plan_change(event, subevent, plan):
def generate_seats(event, subevent, plan, mapping):
current_seats = {}
for s in event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent):
for s in event.seats.select_related('product').annotate(
has_op=Count('orderposition'), has_v=Count('vouchers')
).filter(subevent=subevent):
if s.seat_guid in current_seats:
s.delete() # Duplicates should not exist
else: