Allow customers to change to a different product variation (#1719)

This commit is contained in:
Raphael Michel
2020-07-20 16:36:24 +02:00
committed by GitHub
parent c8ef825de5
commit e7b9c49620
17 changed files with 952 additions and 132 deletions

View File

@@ -315,6 +315,51 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,

View File

@@ -378,9 +378,9 @@ class Item(LoggedModel):
'but only for fixed bundles!')
)
allow_cancel = models.BooleanField(
verbose_name=_('Allow product to be canceled'),
verbose_name=_('Allow product to be canceled or changed'),
default=True,
help_text=_('If this is checked, the usual cancellation settings of this event apply. If this is unchecked, '
help_text=_('If this is checked, the usual cancellation and order change settings of this event apply. If this is unchecked, '
'orders containing this product can not be canceled by users but only by you.')
)
min_per_order = models.IntegerField(

View File

@@ -434,6 +434,19 @@ class Order(LockModel, LoggedModel):
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED) and self.count_positions
)
@cached_property
def user_change_deadline(self):
until = self.event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
if until:
if self.event.has_subevents:
terms = [
until.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
]
return min(terms) if terms else None
else:
return until.datetime(self.event)
@cached_property
def user_cancel_deadline(self):
if self.status == Order.STATUS_PAID and self.total != Decimal('0.00'):
@@ -466,6 +479,36 @@ class Order(LockModel, LoggedModel):
fee += self.event.settings.cancel_allow_user_paid_keep
return round_decimal(fee, self.event.currency)
@property
@scopes_disabled()
def user_change_allowed(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
from .checkin import Checkin
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
return False
if self.cancellation_requests.exists():
return False
positions = list(
self.positions.all().annotate(
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
if op.issued_gift_cards.all():
return False
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
@property
@scopes_disabled()
def user_cancel_allowed(self) -> bool:
@@ -474,7 +517,7 @@ class Order(LockModel, LoggedModel):
"""
from .checkin import Checkin
if self.cancellation_requests.exists():
if self.cancellation_requests.exists() or not self.cancel_allowed():
return False
positions = list(
self.positions.all().annotate(

View File

@@ -1131,7 +1131,7 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices,
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
)
except SendMailException:
logger.exception('Order changed email could not be sent')
@@ -1869,9 +1869,10 @@ class OrderChangeManager:
def _reissue_invoice(self):
i = self.order.invoices.filter(is_cancellation=False).last()
if self.reissue_invoice and i and self._invoice_dirty:
self._invoices.append(generate_cancellation(i))
if invoice_qualified(self.order):
if self.reissue_invoice and self._invoice_dirty:
if i:
self._invoices.append(generate_cancellation(i))
if (i or self.event.settings.invoice_generate == 'True') and invoice_qualified(self.order):
self._invoices.append(generate_invoice(self.order))
def _check_complete_cancel(self):

View File

@@ -89,7 +89,7 @@ class QuotaAvailability:
def compute(self, now_dt=None):
now_dt = now_dt or now()
quotas = list(self._queue)
quotas = list(set(self._queue))
quotas_original = list(self._queue)
self._queue.clear()
if not quotas:

View File

@@ -925,6 +925,46 @@ DEFAULTS = {
"multiple event dates, the earliest date will be used."),
)
},
'change_allow_user_variation': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Customers can change the variation of the products they purchased"),
)
},
'change_allow_user_price': {
'default': 'gt',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('gt', _('Only allow changes if the resulting price is higher or equal than the previous price.')),
('eq', _('Only allow changes if the resulting price is equal to the previous price.')),
('any', _('Allow changes regardless of price, even if this results in a refund.')),
)
),
'form_kwargs': dict(
label=_("Requirement for changed prices"),
choices=(
('gt', _('Only allow changes if the resulting price is higher or equal than the previous price.')),
('eq', _('Only allow changes if the resulting price is equal to the previous price.')),
('any', _('Allow changes regardless of price, even if this results in a refund.')),
),
widget=forms.RadioSelect,
),
},
'change_allow_user_until': {
'default': None,
'type': RelativeDateWrapper,
'form_class': RelativeDateTimeField,
'serializer_class': SerializerRelativeDateTimeField,
'form_kwargs': dict(
label=_("Do not allow changes after"),
)
},
'cancel_allow_user': {
'default': 'True',
'type': bool,

View File

@@ -37,7 +37,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
arg,
floatformat(value, 2)
)
return format_currency(value, arg, locale=translation.get_language())
return format_currency(value, arg, locale=translation.get_language()[:2])
except:
return '{} {}'.format(
arg,