forked from CGM_Public/pretix_original
Memberships: Check valid_from/valid_until for parallel usage (#3975)
This commit is contained in:
@@ -49,7 +49,8 @@ class MembershipType(LoggedModel):
|
||||
allow_parallel_usage = models.BooleanField(
|
||||
verbose_name=_('Parallel usage is allowed'),
|
||||
help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note '
|
||||
'that this will only check for an identical start time of the events, not for any overlap between events.'),
|
||||
'that this will only check for an identical start time of the events, not for any overlap between events. An overlap '
|
||||
'check will be performed if there is a product-level validity of the ticket.'),
|
||||
default=False
|
||||
)
|
||||
max_usages = models.PositiveIntegerField(
|
||||
|
||||
@@ -29,8 +29,8 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, Customer, Event, Item, Membership, Order, OrderPosition,
|
||||
SubEvent,
|
||||
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
|
||||
OrderPosition, SubEvent,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
@@ -82,7 +82,8 @@ def create_membership(customer: Customer, position: OrderPosition):
|
||||
)
|
||||
|
||||
|
||||
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False):
|
||||
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
|
||||
valid_from_not_chosen=False):
|
||||
"""
|
||||
Validate that a set of cart or order positions. This currently does not validate
|
||||
|
||||
@@ -132,7 +133,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
||||
qs = qs.exclude(order_id=ignored_order.pk)
|
||||
m._used_at_dates = [
|
||||
(op.subevent or op.order.event).date_from
|
||||
for op in qs
|
||||
for op in qs if not op.valid_from or not op.valid_until
|
||||
]
|
||||
m._used_for_ranges = [
|
||||
(op.valid_from, op.valid_until)
|
||||
for op in qs if op.valid_from or op.valid_until
|
||||
]
|
||||
|
||||
for p in applicable_positions:
|
||||
@@ -192,13 +197,42 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
||||
m.usages += 1
|
||||
|
||||
if not m.membership_type.allow_parallel_usage:
|
||||
df = ev.date_from
|
||||
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
|
||||
raise ValidationError(
|
||||
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
|
||||
'however you already used the same membership for a different ticket at the same time.').format(
|
||||
type=m.membership_type.name,
|
||||
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||
if isinstance(p, (OrderPosition, CartPosition)):
|
||||
# override_ variants are for usage of fake cart in OrderChangeManager
|
||||
valid_from = getattr(p, 'override_valid_from', p.valid_from)
|
||||
valid_until = getattr(p, 'override_valid_until', p.valid_until)
|
||||
else: # future safety, not technically defined on AbstractPosition
|
||||
valid_from = None
|
||||
valid_until = None
|
||||
|
||||
if (valid_from or valid_until) and not (p.item.validity_dynamic_start_choice and valid_from_not_chosen):
|
||||
for used_range in m._used_for_ranges:
|
||||
if valid_from and valid_from > used_range[1]:
|
||||
continue
|
||||
if valid_until and valid_until < used_range[0]:
|
||||
continue
|
||||
raise ValidationError(
|
||||
_('You are trying to use a membership of type "{type}" for a ticket valid from {valid_from} '
|
||||
'until {valid_until}, however you already used the same membership for a different ticket '
|
||||
'that overlaps with this time frame ({conflict_from} – {conflict_until}).').format(
|
||||
type=m.membership_type.name,
|
||||
valid_from=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_from else _('start'),
|
||||
valid_until=date_format(valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_until else _('open end'),
|
||||
conflict_from=date_format(used_range[0].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[0] else _('start'),
|
||||
conflict_until=date_format(used_range[1].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[1] else _('open end'),
|
||||
)
|
||||
)
|
||||
)
|
||||
m._used_at_dates.append(ev.date_from)
|
||||
|
||||
m._used_for_ranges.append((p.valid_from, p.valid_until))
|
||||
|
||||
if not valid_from or not valid_until:
|
||||
df = ev.date_from
|
||||
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
|
||||
raise ValidationError(
|
||||
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
|
||||
'however you already used the same membership for a different ticket at the same time.').format(
|
||||
type=m.membership_type.name,
|
||||
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||
)
|
||||
)
|
||||
m._used_at_dates.append(ev.date_from)
|
||||
|
||||
@@ -2671,6 +2671,7 @@ class OrderChangeManager:
|
||||
|
||||
for p in self.order.positions.all():
|
||||
cp = CartPosition(
|
||||
event=self.event,
|
||||
item=p.item,
|
||||
variation=p.variation,
|
||||
attendee_name_parts=p.attendee_name_parts,
|
||||
@@ -2691,16 +2692,23 @@ class OrderChangeManager:
|
||||
positions_to_fake_cart[op.position].seat = op.seat
|
||||
elif isinstance(op, self.MembershipOperation):
|
||||
positions_to_fake_cart[op.position].used_membership = op.membership
|
||||
elif isinstance(op, self.ChangeValidFromOperation):
|
||||
positions_to_fake_cart[op.position].override_valid_from = op.valid_from
|
||||
elif isinstance(op, self.ChangeValidUntilOperation):
|
||||
positions_to_fake_cart[op.position].override_valid_until = op.valid_until
|
||||
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
|
||||
fake_cart.remove(positions_to_fake_cart[op.position])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
cp = CartPosition(
|
||||
event=self.event,
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
used_membership=op.membership,
|
||||
subevent=op.subevent,
|
||||
seat=op.seat,
|
||||
)
|
||||
cp.override_valid_from = op.valid_from
|
||||
cp.override_valid_until = op.valid_until
|
||||
fake_cart.append(cp)
|
||||
try:
|
||||
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
|
||||
|
||||
Reference in New Issue
Block a user