forked from CGM_Public/pretix_original
Allow to cancel subevents by date range
This commit is contained in:
@@ -65,7 +65,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
|||||||
|
|
||||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||||
real_subject = str(subject).format_map(TolerantDict(email_context))
|
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||||
email_context = get_email_context(event_or_subevent=subevent or order.event,
|
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||||
event=order.event,
|
event=order.event,
|
||||||
refund_amount=refund_amount,
|
refund_amount=refund_amount,
|
||||||
position_or_address=p,
|
position_or_address=p,
|
||||||
@@ -82,11 +82,12 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||||
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
keep_fee_fixed: str, keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
||||||
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||||
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
||||||
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None):
|
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None,
|
||||||
|
subevents_from: str=None, subevents_to: str=None):
|
||||||
send_subject = LazyI18nString(send_subject)
|
send_subject = LazyI18nString(send_subject)
|
||||||
send_message = LazyI18nString(send_message)
|
send_message = LazyI18nString(send_message)
|
||||||
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||||
@@ -102,14 +103,20 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
pcnt__gt=0
|
pcnt__gt=0
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if subevent:
|
if subevent or subevents_from:
|
||||||
subevent = event.subevents.get(pk=subevent)
|
if subevent:
|
||||||
|
subevents = event.subevents.filter(pk=subevent)
|
||||||
|
subevent = subevents.first()
|
||||||
|
subevent_ids = {subevent.pk}
|
||||||
|
else:
|
||||||
|
subevents = event.subevents.filter(date_from__gte=subevents_from, date_from__lt=subevents_to)
|
||||||
|
subevent_ids = set(subevents.values_list('id', flat=True))
|
||||||
|
|
||||||
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
||||||
subevent=subevent
|
subevent__in=subevents
|
||||||
)
|
)
|
||||||
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||||
subevent=subevent
|
subevent__in=subevents
|
||||||
)
|
)
|
||||||
orders_to_change = orders_to_cancel.annotate(
|
orders_to_change = orders_to_cancel.annotate(
|
||||||
has_subevent=Exists(has_subevent),
|
has_subevent=Exists(has_subevent),
|
||||||
@@ -124,15 +131,18 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
has_subevent=True, has_other_subevent=False
|
has_subevent=True, has_other_subevent=False
|
||||||
)
|
)
|
||||||
|
|
||||||
subevent.log_action(
|
for se in subevents:
|
||||||
'pretix.subevent.canceled', user=user,
|
se.log_action(
|
||||||
)
|
'pretix.subevent.canceled', user=user,
|
||||||
subevent.active = False
|
)
|
||||||
subevent.save(update_fields=['active'])
|
se.active = False
|
||||||
subevent.log_action(
|
se.save(update_fields=['active'])
|
||||||
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
se.log_action(
|
||||||
)
|
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
subevents = None
|
||||||
|
subevent_ids = set()
|
||||||
orders_to_change = event.orders.none()
|
orders_to_change = event.orders.none()
|
||||||
event.log_action(
|
event.log_action(
|
||||||
'pretix.event.canceled', user=user,
|
'pretix.event.canceled', user=user,
|
||||||
@@ -146,7 +156,9 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
)
|
)
|
||||||
failed = 0
|
failed = 0
|
||||||
total = orders_to_cancel.count() + orders_to_change.count()
|
total = orders_to_cancel.count() + orders_to_change.count()
|
||||||
qs_wl = event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True)
|
qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent')
|
||||||
|
if subevents:
|
||||||
|
qs_wl = qs_wl.filter(subevent__in=subevents)
|
||||||
if send_waitinglist:
|
if send_waitinglist:
|
||||||
total += qs_wl.count()
|
total += qs_wl.count()
|
||||||
counter = 0
|
counter = 0
|
||||||
@@ -205,7 +217,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
|
|
||||||
ocm = OrderChangeManager(o, user=user, notify=False)
|
ocm = OrderChangeManager(o, user=user, notify=False)
|
||||||
for p in o.positions.all():
|
for p in o.positions.all():
|
||||||
if p.subevent == subevent:
|
if p.subevent_id in subevent_ids:
|
||||||
total += p.price
|
total += p.price
|
||||||
ocm.cancel(p)
|
ocm.cancel(p)
|
||||||
positions.append(p)
|
positions.append(p)
|
||||||
@@ -246,7 +258,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
|||||||
|
|
||||||
if send_waitinglist:
|
if send_waitinglist:
|
||||||
for wle in qs_wl:
|
for wle in qs_wl:
|
||||||
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
|
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent)
|
||||||
|
|
||||||
counter += 1
|
counter += 1
|
||||||
if not self.request.called_directly and counter % max(10, total // 100) == 0:
|
if not self.request.called_directly and counter % max(10, total // 100) == 0:
|
||||||
|
|||||||
@@ -586,7 +586,21 @@ class EventCancelForm(forms.Form):
|
|||||||
all_subevents = forms.BooleanField(
|
all_subevents = forms.BooleanField(
|
||||||
label=_('Cancel all dates'),
|
label=_('Cancel all dates'),
|
||||||
initial=False,
|
initial=False,
|
||||||
required=False
|
required=False,
|
||||||
|
)
|
||||||
|
subevents_from = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
'data-inverse-dependency': '#id_all_subevents',
|
||||||
|
}),
|
||||||
|
label=pgettext_lazy('subevent', 'All dates starting at or after'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
subevents_to = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
'data-inverse-dependency': '#id_all_subevents',
|
||||||
|
}),
|
||||||
|
label=pgettext_lazy('subevent', 'All dates starting before'),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
auto_refund = forms.BooleanField(
|
auto_refund = forms.BooleanField(
|
||||||
label=_('Automatically refund money if possible'),
|
label=_('Automatically refund money if possible'),
|
||||||
@@ -731,6 +745,7 @@ class EventCancelForm(forms.Form):
|
|||||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||||
self.fields['subevent'].widget = Select2(
|
self.fields['subevent'].widget = Select2(
|
||||||
attrs={
|
attrs={
|
||||||
|
'data-inverse-dependency': '#id_all_subevents',
|
||||||
'data-model-select2': 'event',
|
'data-model-select2': 'event',
|
||||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
'event': self.event.slug,
|
'event': self.event.slug,
|
||||||
@@ -747,6 +762,12 @@ class EventCancelForm(forms.Form):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
if self.event.has_subevents and not d['subevent'] and not d['all_subevents']:
|
if d.get('subevent') and d.get('subevents_from'):
|
||||||
|
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
|
||||||
|
if d.get('all_subevents') and d.get('subevent_from'):
|
||||||
|
raise ValidationError(pgettext_lazy('subevent', 'Please either select all subevents or a date range, not both.'))
|
||||||
|
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
|
||||||
|
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
||||||
|
if self.event.has_subevents and not d['subevent'] and not d['all_subevents'] and not d.get('subevents_from'):
|
||||||
raise ValidationError(_('Please confirm that you want to cancel ALL dates in this event series.'))
|
raise ValidationError(_('Please confirm that you want to cancel ALL dates in this event series.'))
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Select date" context "subevents" %}</legend>
|
<legend>{% trans "Select date" context "subevents" %}</legend>
|
||||||
{% bootstrap_field form.subevent layout="control" %}
|
|
||||||
{% bootstrap_field form.all_subevents layout="control" %}
|
{% bootstrap_field form.all_subevents layout="control" %}
|
||||||
|
{% bootstrap_field form.subevent layout="control" %}
|
||||||
|
{% bootstrap_field form.subevents_from layout="control" %}
|
||||||
|
{% bootstrap_field form.subevents_to layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -2074,6 +2074,8 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
|
|||||||
return self.do(
|
return self.do(
|
||||||
self.request.event.pk,
|
self.request.event.pk,
|
||||||
subevent=form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
subevent=form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
||||||
|
subevents_from=form.cleaned_data.get('subevents_from'),
|
||||||
|
subevents_to=form.cleaned_data.get('subevents_to'),
|
||||||
auto_refund=form.cleaned_data.get('auto_refund'),
|
auto_refund=form.cleaned_data.get('auto_refund'),
|
||||||
manual_refund=form.cleaned_data.get('manual_refund'),
|
manual_refund=form.cleaned_data.get('manual_refund'),
|
||||||
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
|
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ class SubEventCancelTests(TestCase):
|
|||||||
with scope(organizer=self.o):
|
with scope(organizer=self.o):
|
||||||
self.event = Event.objects.create(organizer=self.o, name='Dummy', slug='dummy', date_from=now(),
|
self.event = Event.objects.create(organizer=self.o, name='Dummy', slug='dummy', date_from=now(),
|
||||||
plugins='tests.testdummy', has_subevents=True)
|
plugins='tests.testdummy', has_subevents=True)
|
||||||
self.se1 = self.event.subevents.create(name='One', date_from=now())
|
self.se1 = self.event.subevents.create(name='One', date_from=now() - timedelta(days=30))
|
||||||
self.se2 = self.event.subevents.create(name='Two', date_from=now())
|
self.se2 = self.event.subevents.create(name='Two', date_from=now())
|
||||||
self.order = Order.objects.create(
|
self.order = Order.objects.create(
|
||||||
code='FOO', event=self.event, email='dummy@dummy.test',
|
code='FOO', event=self.event, email='dummy@dummy.test',
|
||||||
@@ -360,6 +360,27 @@ class SubEventCancelTests(TestCase):
|
|||||||
assert self.order.status == Order.STATUS_PENDING
|
assert self.order.status == Order.STATUS_PENDING
|
||||||
assert self.order.positions.count() == 1
|
assert self.order.positions.count() == 1
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_cancel_subevent_range(self):
|
||||||
|
self.op2.subevent = self.se1
|
||||||
|
self.op2.save()
|
||||||
|
cancel_event(
|
||||||
|
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||||
|
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||||
|
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||||
|
user=None
|
||||||
|
)
|
||||||
|
self.order.refresh_from_db()
|
||||||
|
assert self.order.status == Order.STATUS_PENDING
|
||||||
|
cancel_event(
|
||||||
|
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||||
|
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||||
|
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||||
|
user=None
|
||||||
|
)
|
||||||
|
self.order.refresh_from_db()
|
||||||
|
assert self.order.status == Order.STATUS_CANCELED
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_cancel_simple_order(self):
|
def test_cancel_simple_order(self):
|
||||||
self.op2.subevent = self.se1
|
self.op2.subevent = self.se1
|
||||||
@@ -405,6 +426,27 @@ class SubEventCancelTests(TestCase):
|
|||||||
assert self.order.status == Order.STATUS_PAID
|
assert self.order.status == Order.STATUS_PAID
|
||||||
assert '23.00' in djmail.outbox[0].body
|
assert '23.00' in djmail.outbox[0].body
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_cancel_mixed_order_range(self):
|
||||||
|
cancel_event(
|
||||||
|
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||||
|
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||||
|
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||||
|
user=None
|
||||||
|
)
|
||||||
|
self.order.refresh_from_db()
|
||||||
|
assert self.order.status == Order.STATUS_PENDING
|
||||||
|
assert self.order.positions.count() == 2
|
||||||
|
cancel_event(
|
||||||
|
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||||
|
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
|
||||||
|
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||||
|
user=None
|
||||||
|
)
|
||||||
|
self.order.refresh_from_db()
|
||||||
|
assert self.order.status == Order.STATUS_PENDING
|
||||||
|
assert self.order.positions.filter(subevent=self.se1, canceled=False).count() == 0
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_cancel_partially_keep_fees(self):
|
def test_cancel_partially_keep_fees(self):
|
||||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||||
|
|||||||
Reference in New Issue
Block a user