mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
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:
@@ -12,7 +12,7 @@ from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -377,9 +377,18 @@ class Event(EventMixin, LoggedModel):
|
||||
if img:
|
||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
def free_seats(self, ignore_voucher=None):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
event=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
redeemed__lt=F('max_usages'),
|
||||
).filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
return self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
@@ -394,8 +403,11 @@ class Event(EventMixin, LoggedModel):
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
),
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False, blocked=False)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
@@ -986,9 +998,19 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
def free_seats(self, ignore_voucher=None):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
event_id=self.event_id,
|
||||
subevent=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
redeemed__lt=F('max_usages'),
|
||||
).filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
return self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
@@ -1005,8 +1027,11 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
),
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
).filter(has_order=False, has_cart=False, blocked=False, has_voucher=False)
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ import jsonschema
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, ugettext_lazy as _
|
||||
@@ -110,15 +111,21 @@ class Seat(models.Model):
|
||||
return self.name
|
||||
return ', '.join(parts)
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None):
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None):
|
||||
from .orders import Order
|
||||
|
||||
if self.blocked:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
cpqs = self.cartposition_set.filter(expires__gte=now())
|
||||
if ignore_cart:
|
||||
vqs = self.vouchers.filter(
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
|
||||
Q(redeemed__lt=F('max_usages'))
|
||||
)
|
||||
if ignore_cart and ignore_cart is not True:
|
||||
cpqs = cpqs.exclude(pk=ignore_cart.pk)
|
||||
if ignore_orderpos:
|
||||
opqs = opqs.exclude(pk=ignore_orderpos.pk)
|
||||
return not opqs.exists() and not cpqs.exists()
|
||||
if ignore_voucher_id:
|
||||
vqs = vqs.exclude(pk=ignore_voucher_id)
|
||||
return not opqs.exists() and (ignore_cart is True or not cpqs.exists()) and not vqs.exists()
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models import SeatCategoryMapping
|
||||
from pretix.base.models import Seat, SeatCategoryMapping
|
||||
|
||||
from ..decimal import round_decimal
|
||||
from .base import LoggedModel
|
||||
@@ -171,6 +171,12 @@ class Voucher(LoggedModel):
|
||||
"If enabled, the voucher is valid for any product affected by this quota."
|
||||
)
|
||||
)
|
||||
seat = models.ForeignKey(
|
||||
Seat, related_name='vouchers',
|
||||
null=True, blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("Specific seat"),
|
||||
)
|
||||
tag = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Tag"),
|
||||
@@ -335,6 +341,38 @@ class Voucher(LoggedModel):
|
||||
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
|
||||
raise ValidationError(_('A voucher with this code already exists.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_seat_id(data, item, event, pk):
|
||||
try:
|
||||
if event.has_subevents:
|
||||
if not data.get('subevent'):
|
||||
raise ValidationError(_('You need to choose a date if you select a seat.'))
|
||||
seat = event.seats.get(seat_guid=data.get('seat'), subevent=data.get('subevent'))
|
||||
else:
|
||||
seat = event.seats.get(seat_guid=data.get('seat'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError(_('The specified seat ID "{id}" does not exist for this event.').format(
|
||||
id=data.get('seat')))
|
||||
|
||||
if not seat.is_available(ignore_voucher_id=pk, ignore_cart=True):
|
||||
raise ValidationError(_('The seat "{id}" is currently unavailable (blocked, already sold or a '
|
||||
'different voucher).').format(
|
||||
id=seat.seat_guid))
|
||||
|
||||
if not item:
|
||||
raise ValidationError(_('You need to choose a specific product if you select a seat.'))
|
||||
|
||||
if data.get('max_usages', 1) > 1:
|
||||
raise ValidationError(_('Seat-specific vouchers can only be used once.'))
|
||||
|
||||
if seat.product != item:
|
||||
raise ValidationError(_('You need to choose the product "{prod}" for this seat.').format(prod=seat.product))
|
||||
|
||||
if not seat.is_available(ignore_voucher_id=pk):
|
||||
raise ValidationError(_('The seat "{id}" is already sold or currently blocked.').format(id=seat.seat_guid))
|
||||
|
||||
return seat
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.code = self.code.upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user