diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index c686678acb..89d0d82b6f 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -24,6 +24,7 @@ from decimal import Decimal from django.db import transaction from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery +from django.utils.crypto import get_random_string from django.utils.translation import gettext from i18nfield.strings import LazyI18nString @@ -41,6 +42,7 @@ from pretix.base.services.orders import ( ) from pretix.base.services.tasks import ProfiledEventTask from pretix.base.services.tax import split_fee_for_taxes +from pretix.base.templatetags.money import money_filter from pretix.celery_app import app from pretix.helpers import OF_SELF from pretix.helpers.format import format_map @@ -112,7 +114,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, 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, - subevents_from: str=None, subevents_to: str=None): + subevents_from: str=None, subevents_to: str=None, dry_run=False): send_subject = LazyI18nString(send_subject) send_message = LazyI18nString(send_message) send_waitinglist_subject = LazyI18nString(send_waitinglist_subject) @@ -161,32 +163,72 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, has_subevent=True, has_other_subevent=False, has_blocked=False ) - 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'} - ) + if not dry_run: + for se in subevents: + se.log_action( + 'pretix.subevent.canceled', user=user, + data={ + "auto_refund": auto_refund, + "keep_fee_fixed": keep_fee_fixed, + "keep_fee_per_ticket": keep_fee_per_ticket, + "keep_fee_percentage": keep_fee_percentage, + "keep_fees": keep_fees, + "manual_refund": manual_refund, + "send": send, + "send_subject": send_subject, + "send_message": send_message, + "send_waitinglist": send_waitinglist, + "send_waitinglist_subject": send_waitinglist_subject, + "send_waitinglist_message": send_waitinglist_message, + "refund_as_giftcard": refund_as_giftcard, + "giftcard_expires": str(giftcard_expires), + "giftcard_conditions": giftcard_conditions, + } + ) + 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 = orders_to_cancel.filter(has_blocked=True) orders_to_cancel = orders_to_cancel.filter(has_blocked=False) - event.log_action( - 'pretix.event.canceled', user=user, - ) - for i in event.items.filter(active=True): - i.active = False - i.save(update_fields=['active']) - i.log_action( - 'pretix.event.item.changed', user=user, data={'active': False, '_source': 'cancel_event'} + if not dry_run: + event.log_action( + 'pretix.event.canceled', user=user, + data={ + "auto_refund": auto_refund, + "keep_fee_fixed": keep_fee_fixed, + "keep_fee_per_ticket": keep_fee_per_ticket, + "keep_fee_percentage": keep_fee_percentage, + "keep_fees": keep_fees, + "manual_refund": manual_refund, + "send": send, + "send_subject": send_subject, + "send_message": send_message, + "send_waitinglist": send_waitinglist, + "send_waitinglist_subject": send_waitinglist_subject, + "send_waitinglist_message": send_waitinglist_message, + "refund_as_giftcard": refund_as_giftcard, + "giftcard_expires": str(giftcard_expires), + "giftcard_conditions": giftcard_conditions, + } ) + + for i in event.items.filter(active=True): + i.active = False + i.save(update_fields=['active']) + i.log_action( + 'pretix.event.item.changed', user=user, data={'active': False, '_source': 'cancel_event'} + ) failed = 0 - total = orders_to_cancel.count() + orders_to_change.count() + refund_total = Decimal("0.00") + cancel_full_total = orders_to_cancel.count() + cancel_partial_total = orders_to_change.count() + total = cancel_full_total + cancel_partial_total qs_wl = event.waitinglistentries.filter(voucher__isnull=True).select_related('subevent') if subevents: qs_wl = qs_wl.filter(subevent__in=subevents) @@ -199,6 +241,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, ) for o in orders_to_cancel.only('id', 'total').iterator(): + payment_refund_sum = o.payment_refund_sum # cache to avoid multiple computations try: fee = Decimal('0.00') fee_sum = Decimal('0.00') @@ -217,20 +260,24 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, for p in o.positions.all(): if p.addon_to_id is None: fee += min(p.price, Decimal(keep_fee_per_ticket)) - fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) + fee = round_decimal(min(fee, payment_refund_sum), event.currency) - _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) - refund_amount = o.payment_refund_sum + if dry_run: + refund_total += max(Decimal("0.00"), min(payment_refund_sum, o.total - fee)) + else: + _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) + refund_amount = payment_refund_sum + refund_amount += refund_total - try: - if auto_refund or manual_refund: - _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, - source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, - giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, - comment=gettext('Event canceled')) - finally: - if send: - _send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all()) + try: + if auto_refund or manual_refund: + _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, + source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, + giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, + comment=gettext('Event canceled')) + finally: + if send: + _send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all()) counter += 1 if not self.request.called_directly and counter % max(10, total // 100) == 0: @@ -247,12 +294,16 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, for o in orders_to_change.values_list('id', flat=True).iterator(): with transaction.atomic(): - o = event.orders.select_for_update(of=OF_SELF).get(pk=o) + if dry_run: + o = event.orders.get(pk=o) + else: + o = event.orders.select_for_update(of=OF_SELF).get(pk=o) total = Decimal('0.00') fee = Decimal('0.00') positions = [] ocm = OrderChangeManager(o, user=user, notify=False) + payment_refund_sum = o.payment_refund_sum # cache to avoid multiple computations for p in o.positions.all(): if (not event.has_subevents or p.subevent_id in subevent_ids) and not p.blocked: total += p.price @@ -267,7 +318,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, fee += Decimal(keep_fee_fixed) if keep_fee_percentage: fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total - fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) + fee = round_decimal(min(fee, payment_refund_sum), event.currency) if fee: tax_rule_zero = TaxRule.zero() if event.settings.tax_rule_cancellation == "default": @@ -298,17 +349,21 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, ) ocm.add_fee(f) - ocm.commit() - refund_amount = o.payment_refund_sum - o.total + if dry_run: + refund_total += max(payment_refund_sum - (o.total + ocm._totaldiff), Decimal("0.00")) + else: + ocm.commit() + refund_amount = payment_refund_sum - o.total + refund_total += refund_amount - if auto_refund or manual_refund: - _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, - source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, - giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, - comment=gettext('Event canceled')) + if auto_refund or manual_refund: + _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, + source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, + giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, + comment=gettext('Event canceled')) - if send: - _send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions) + if send: + _send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions) counter += 1 if not self.request.called_directly and counter % max(10, total // 100) == 0: @@ -319,7 +374,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, if send_waitinglist: for wle in qs_wl: - _send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, wle.subevent) + if not dry_run: + _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: @@ -327,4 +383,30 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, state='PROGRESS', meta={'value': round(counter / total * 100 if total else 0, 2)} ) - return failed + + confirmation_code = None + if dry_run and user and refund_total > Decimal('100.00'): + confirmation_code = get_random_string(8, allowed_chars="01234567890") + mail( + user.email, + subject=gettext('Bulk-refund confirmation'), + template='pretixbase/email/cancel_confirm.txt', + context={ + "event": str(event), + "amount": money_filter(refund_total, event.currency), + "confirmation_code": confirmation_code, + }, + locale=user.locale, + ) + + return { + "dry_run": dry_run, + "id": self.request.id, + "failed": failed, + "refund_total": refund_total, + "cancel_full_total": cancel_full_total, + "cancel_partial_total": cancel_partial_total, + "confirmation_code": confirmation_code, + "args": self.request.args, + "kwargs": self.request.kwargs, + } diff --git a/src/pretix/base/templates/pretixbase/email/cancel_confirm.txt b/src/pretix/base/templates/pretixbase/email/cancel_confirm.txt new file mode 100644 index 0000000000..5e3043156a --- /dev/null +++ b/src/pretix/base/templates/pretixbase/email/cancel_confirm.txt @@ -0,0 +1,10 @@ +{% load i18n %} +{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %} + +{% trans "Event" %}: {{ event }} + +{% trans "Estimated refund amount" %}: **{{ amount }}** + +{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %} + +**{{ confirmation_code }}** diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index bdcbafd14f..9d2dc82d67 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -1030,3 +1030,27 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form): 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 + + +class EventCancelConfirmForm(forms.Form): + confirm = forms.BooleanField( + label=_("I understand that this is not reversible and want to continue"), + required=True, + ) + confirmation_code = forms.CharField( + label=_("Confirmation code"), + help_text=_("We have just emailed you a confirmation code to enter to confirm this action"), + required=True, + ) + + def __init__(self, *args, **kwargs): + self.code = kwargs.pop("confirmation_code") + super().__init__(*args, **kwargs) + if not self.code: + del self.fields["confirmation_code"] + + def clean_confirmation_code(self): + val = self.cleaned_data['confirmation_code'] + if val != self.code: + raise ValidationError(_('The confirmation code is incorrect.')) + return val diff --git a/src/pretix/control/templates/pretixcontrol/orders/cancel.html b/src/pretix/control/templates/pretixcontrol/orders/cancel.html index 47c91ecb9e..2e8c9ce71b 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/cancel.html +++ b/src/pretix/control/templates/pretixcontrol/orders/cancel.html @@ -79,9 +79,15 @@ {% bootstrap_field form.send_waitinglist_message layout="horizontal" %}
- + {% if dry_run_supported %} + + {% else %} + + {% endif %}
{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/orders/cancel_confirm.html b/src/pretix/control/templates/pretixcontrol/orders/cancel_confirm.html new file mode 100644 index 0000000000..b7e6208618 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/orders/cancel_confirm.html @@ -0,0 +1,85 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load eventsignal %} +{% load bootstrap3 %} +{% load money %} +{% block title %}{% trans "Cancel event" %}{% endblock %} +{% block content %} +

{% trans "Cancel event" %}

+
+ {% csrf_token %} + {% bootstrap_form_errors form %} +

+ {% blocktrans trimmed %} + If you proceed, the system will do the following: + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + These numbers are estimates and may change if the data in your event recently changed. + {% endblocktrans %} +

+ {% bootstrap_form_errors form %} + {% if form.confirmation_code %} + {% bootstrap_field form.confirm layout="control" %} + {% bootstrap_field form.confirmation_code layout="control" %} + {% else %} + {% bootstrap_field form.confirm layout="inline" form_group_class="" %} + {% endif %} +
+ +
+
+{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 130d4e78c4..feb3b64014 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -460,6 +460,7 @@ urlpatterns = [ re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'), re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'), re_path(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'), + re_path(r'^cancel/(?P[^/]+)/$', orders.EventCancelConfirm.as_view(), name='event.cancel.confirm'), re_path(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'), re_path(r'^shredder/export$', shredder.ShredExportView.as_view(), name='event.shredder.export'), re_path(r'^shredder/download/(?P[^/]+)/$', shredder.ShredDownloadView.as_view(), name='event.shredder.download'), diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 9e5d8382c5..c403cec839 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -42,6 +42,7 @@ from datetime import datetime, time, timedelta from decimal import Decimal, DecimalException from urllib.parse import quote, urlencode +from celery.result import AsyncResult from django import forms from django.conf import settings from django.contrib import messages @@ -122,8 +123,8 @@ from pretix.control.forms.filter import ( RefundFilterForm, ) from pretix.control.forms.orders import ( - CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm, - ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm, + CancelForm, CommentForm, DenyForm, EventCancelConfirmForm, EventCancelForm, + ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm, OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm, OrderRefundForm, OtherOperationsForm, @@ -2975,10 +2976,99 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView): send_waitinglist_subject=form.cleaned_data.get('send_waitinglist_subject').data, send_waitinglist_message=form.cleaned_data.get('send_waitinglist_message').data, user=self.request.user.pk, + dry_run=settings.HAS_CELERY, + ) + + def get_context_data(self, **kwargs): + return super().get_context_data( + dry_run_supported=settings.HAS_CELERY, ) def get_success_message(self, value): - if value == 0: + if value["dry_run"]: + return None + elif value["failed"] == 0: + return _('All orders have been canceled.') + else: + return _('The orders have been canceled. An error occurred with {count} orders, please ' + 'check all uncanceled orders.').format(count=value) + + def get_success_url(self, value): + if settings.HAS_CELERY: + return reverse('control:event.cancel.confirm', kwargs={ + 'organizer': self.request.organizer.slug, + 'event': self.request.event.slug, + 'task': value["id"], + }) + else: + return reverse('control:event.cancel', kwargs={ + 'organizer': self.request.organizer.slug, + 'event': self.request.event.slug, + }) + + def get_error_url(self): + return reverse('control:event.cancel', kwargs={ + 'organizer': self.request.organizer.slug, + 'event': self.request.event.slug, + }) + + def get_error_message(self, exception): + if isinstance(exception, str): + return exception + return super().get_error_message(exception) + + def form_invalid(self, form): + messages.error(self.request, _('Your input was not valid.')) + return super().form_invalid(form) + + +class EventCancelConfirm(EventPermissionRequiredMixin, AsyncAction, FormView): + template_name = 'pretixcontrol/orders/cancel_confirm.html' + permission = 'can_change_orders' + form_class = EventCancelConfirmForm + task = cancel_event + known_errortypes = ['OrderError'] + + @cached_property + def dryrun_result(self): + res = AsyncResult(self.kwargs.get("task")) + if not res.ready(): + raise Http404() + if not res.successful(): + raise Http404() + data = res.info + if not data.get("dry_run"): + raise Http404() + if data.get("args")[0] != self.request.event.pk: + raise Http404() + return data + + def get(self, request, *args, **kwargs): + if 'async_id' in request.GET and settings.HAS_CELERY: + return self.get_result(request) + return FormView.get(self, request, *args, **kwargs) + + def get_form_kwargs(self): + k = super().get_form_kwargs() + k['confirmation_code'] = self.dryrun_result["confirmation_code"] + return k + + def form_valid(self, form): + return self.do( + *self.dryrun_result["args"], + **{ + **self.dryrun_result["kwargs"], + "dry_run": False, + }, + ) + + def get_context_data(self, **kwargs): + return super().get_context_data( + dryrun_result=self.dryrun_result, + ) + + def get_success_message(self, value): + if value["failed"] == 0: return _('All orders have been canceled.') else: return _('The orders have been canceled. An error occurred with {count} orders, please ' diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index a78a9acfe5..a1928bbcaf 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -348,7 +348,7 @@ var form_handlers = function (el) { dependency.on("change", update); }); - el.find("div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency]").each(function () { + el.find("div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency], button[data-display-dependency]").each(function () { var dependent = $(this), dependency = findDependency($(this).attr("data-display-dependency"), this), update = function (ev) { @@ -373,10 +373,11 @@ var form_handlers = function (el) { enabled = !enabled; } var $toggling = dependent; - if (dependent.attr("data-disable-dependent")) { + if (dependent.is("[data-disable-dependent]")) { $toggling.attr('disabled', !enabled).trigger("change"); } - if (dependent.get(0).tagName.toLowerCase() !== "div") { + const tagName = dependent.get(0).tagName.toLowerCase() + if (tagName !== "div" && tagName !== "button") { $toggling = dependent.closest('.form-group'); } if (ev) { diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index 79509aef0f..dbb97c1530 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -63,6 +63,15 @@ class EventCancelTests(TestCase): generate_invoice(self.order) djmail.outbox = [] + def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs): + dry_run = cancel_event( + *args, **kwargs, dry_run=True + ) + assert dry_run["refund_total"] == expected_refunds + cancel_event( + *args, **kwargs, + ) + @classscope(attr='o') def test_cancel_send_mail(self): gc = self.o.issued_gift_cards.create(currency="EUR") @@ -74,11 +83,11 @@ class EventCancelTests(TestCase): ) self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert len(djmail.outbox) == 1 self.order.refresh_from_db() @@ -114,11 +123,11 @@ class EventCancelTests(TestCase): self.op1.blocked = ["admin"] self.op1.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("23.00") ) self.op1.refresh_from_db() @@ -147,11 +156,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) r = self.order.refunds.get() @@ -175,11 +184,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) self.order.refresh_from_db() @@ -198,11 +207,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("42.00") ) r = self.order.refunds.get() @@ -226,11 +235,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("44.00") ) r = self.order.refunds.get() @@ -252,11 +261,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("44.00") ) r = self.order.refunds.get() @@ -276,11 +285,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("31.40") ) r = self.order.refunds.get() @@ -304,11 +313,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PENDING self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("12.00") ) assert not self.order.refunds.exists() @@ -335,10 +344,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", - send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None, + expected_refunds=Decimal("36.90") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE @@ -371,11 +381,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("39.40") ) r = self.order.refunds.get() assert r.amount == Decimal('39.40') @@ -400,11 +410,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=True, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 2 @@ -436,11 +446,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=False, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 1 @@ -467,11 +477,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=True, auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 1 @@ -511,17 +521,26 @@ class SubEventCancelTests(TestCase): generate_invoice(self.order) djmail.outbox = [] + def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs): + dry_run = cancel_event( + *args, **kwargs, dry_run=True + ) + assert dry_run["refund_total"] == expected_refunds + cancel_event( + *args, **kwargs, + ) + @classscope(attr='o') def test_cancel_partially_send_mail_attendees(self): self.op1.attendee_email = 'foo@example.com' self.op1.save() self.op2.attendee_email = 'foo@example.org' self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) assert len(djmail.outbox) == 2 self.order.refresh_from_db() @@ -532,19 +551,19 @@ class SubEventCancelTests(TestCase): def test_cancel_subevent_range(self): self.op2.subevent = self.se1 self.op2.save() - cancel_event( + self._cancel_with_dryrun( 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", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING - cancel_event( + self._cancel_with_dryrun( 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", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -553,11 +572,11 @@ class SubEventCancelTests(TestCase): def test_cancel_simple_order(self): self.op2.subevent = self.se1 self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -567,11 +586,11 @@ class SubEventCancelTests(TestCase): self.op2.subevent = self.se1 self.op2.blocked = ["admin"] self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING @@ -582,11 +601,11 @@ class SubEventCancelTests(TestCase): @classscope(attr='o') def test_cancel_all_subevents(self): - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -602,11 +621,12 @@ class SubEventCancelTests(TestCase): ) self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self.order.refresh_from_db() + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("23.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PAID @@ -614,20 +634,20 @@ class SubEventCancelTests(TestCase): @classscope(attr='o') def test_cancel_mixed_order_range(self): - cancel_event( + self._cancel_with_dryrun( 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", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING assert self.order.positions.count() == 2 - cancel_event( + self._cancel_with_dryrun( 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", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING @@ -651,11 +671,11 @@ class SubEventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("16.20") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE @@ -682,11 +702,11 @@ class SubEventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("21.00") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE