From 4ff4402a5f8d416230ba0aec10c13450cac68f32 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 29 Oct 2020 10:08:37 +0100 Subject: [PATCH] Allow to cancel subevents by date range --- src/pretix/base/services/cancelevent.py | 50 ++++++++++++------- src/pretix/control/forms/orders.py | 25 +++++++++- .../pretixcontrol/orders/cancel.html | 4 +- src/pretix/control/views/orders.py | 2 + src/tests/base/test_cancelevent.py | 44 +++++++++++++++- 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index e12d8cc8cb..71c35d5158 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -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: 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, refund_amount=refund_amount, 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,)) -def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str, - keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False, +def cancel_event(self, event: Event, subevent: int, auto_refund: bool, + 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_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_message = LazyI18nString(send_message) 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 ).all() - if subevent: - subevent = event.subevents.get(pk=subevent) + if subevent or subevents_from: + 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( - subevent=subevent + subevent__in=subevents ) has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude( - subevent=subevent + subevent__in=subevents ) orders_to_change = orders_to_cancel.annotate( 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 ) - subevent.log_action( - 'pretix.subevent.canceled', user=user, - ) - subevent.active = False - subevent.save(update_fields=['active']) - subevent.log_action( - 'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'} - ) + for se in subevents: + se.log_action( + 'pretix.subevent.canceled', user=user, + ) + se.active = False + se.save(update_fields=['active']) + se.log_action( + 'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'} + ) else: + subevents = None + subevent_ids = set() orders_to_change = event.orders.none() event.log_action( 'pretix.event.canceled', user=user, @@ -146,7 +156,9 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_ ) failed = 0 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: total += qs_wl.count() 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) for p in o.positions.all(): - if p.subevent == subevent: + if p.subevent_id in subevent_ids: total += p.price ocm.cancel(p) positions.append(p) @@ -246,7 +258,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_ if send_waitinglist: 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 if not self.request.called_directly and counter % max(10, total // 100) == 0: diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index bf06e96b21..30e2aa4db3 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -586,7 +586,21 @@ class EventCancelForm(forms.Form): all_subevents = forms.BooleanField( label=_('Cancel all dates'), 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( 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'].widget = Select2( attrs={ + 'data-inverse-dependency': '#id_all_subevents', 'data-model-select2': 'event', 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ 'event': self.event.slug, @@ -747,6 +762,12 @@ class EventCancelForm(forms.Form): def clean(self): 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.')) return d diff --git a/src/pretix/control/templates/pretixcontrol/orders/cancel.html b/src/pretix/control/templates/pretixcontrol/orders/cancel.html index f424f5d02f..51df2e6c0f 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/cancel.html +++ b/src/pretix/control/templates/pretixcontrol/orders/cancel.html @@ -27,8 +27,10 @@ {% if request.event.has_subevents %}
{% trans "Select date" context "subevents" %} - {% bootstrap_field form.subevent 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" %}
{% endif %}
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index a1195867c6..8ee20c7979 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -2074,6 +2074,8 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView): return self.do( self.request.event.pk, 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'), manual_refund=form.cleaned_data.get('manual_refund'), refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'), diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index c417911fe5..d89222264e 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -322,7 +322,7 @@ class SubEventCancelTests(TestCase): with scope(organizer=self.o): self.event = Event.objects.create(organizer=self.o, name='Dummy', slug='dummy', date_from=now(), 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.order = Order.objects.create( 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.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') def test_cancel_simple_order(self): self.op2.subevent = self.se1 @@ -405,6 +426,27 @@ class SubEventCancelTests(TestCase): assert self.order.status == Order.STATUS_PAID 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') def test_cancel_partially_keep_fees(self): gc = self.o.issued_gift_cards.create(currency="EUR")