diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 922818d29..990cacf85 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, - cancellation_fee=None, keep_fees=None): + cancellation_fee=None, keep_fees=None, cancel_invoice=True): """ Mark this order as canceled :param order: The order to change @@ -351,9 +351,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device if not order.cancel_allowed(): raise OrderError(_('You cannot cancel this order.')) invoices = [] - i = order.invoices.filter(is_cancellation=False).last() - if i and not i.refered.exists(): - invoices.append(generate_cancellation(i)) + if cancel_invoice: + i = order.invoices.filter(is_cancellation=False).last() + if i and not i.refered.exists(): + invoices.append(generate_cancellation(i)) for position in order.positions.all(): for gc in position.issued_gift_cards.all(): @@ -403,7 +404,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device order.cancellation_date = now() order.save(update_fields=['status', 'cancellation_date', 'total']) - if i: + if cancel_invoice and i: invoices.append(generate_invoice(order)) else: with order.event.lock(): @@ -2152,11 +2153,12 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @scopes_disabled() def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, - device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None): + device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None, + cancel_invoice=True): try: try: ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application, - cancellation_fee) + cancellation_fee, cancel_invoice=cancel_invoice) if try_auto_refund: _try_auto_refund(order, refund_as_giftcard=refund_as_giftcard, comment=comment) diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 19db3f309..a2f01da8f 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -5,7 +5,7 @@ from urllib.parse import urlencode from django import forms from django.apps import apps from django.conf import settings -from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet +from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet, Count from django.db.models.functions import Coalesce, ExtractWeekDay from django.urls import reverse, reverse_lazy from django.utils.formats import date_format, localize @@ -153,6 +153,7 @@ class OrderFilterForm(FilterForm): (Order.STATUS_CANCELED, _('Canceled (fully)')), ('cp', _('Canceled (fully or with paid fee)')), ('rc', _('Cancellation requested')), + ('cni', _('Fully canceled but invoice not canceled')), )), (_('Payment process'), ( (Order.STATUS_EXPIRED, _('Expired')), @@ -264,6 +265,18 @@ class OrderFilterForm(FilterForm): status=Order.STATUS_PAID, pending_sum_t__gt=0 ) + elif s == 'cni': + i = Invoice.objects.filter( + order=OuterRef('pk'), + is_cancellation=False, + refered__isnull=True, + ).order_by().values('order').annotate(k=Count('id')).values('k') + qs = qs.annotate( + icnt=i + ).filter( + icnt__gt=0, + status=Order.STATUS_CANCELED, + ) elif s == 'pa': qs = qs.filter( status=Order.STATUS_PENDING, diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index e79931245..1d3694471 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -117,6 +117,11 @@ class CancelForm(ConfirmPaymentForm): 'in your cancellation fee if you want to keep them. Please always enter a gross value, ' 'tax will be calculated automatically.'), ) + cancel_invoice = forms.BooleanField( + label=_('Generate cancellation for invoice'), + initial=True, + required=False + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -130,6 +135,8 @@ class CancelForm(ConfirmPaymentForm): self.fields['cancellation_fee'].max_value = prs else: del self.fields['cancellation_fee'] + if not self.instance.invoices.exists(): + del self.fields['cancel_invoice'] def clean_cancellation_fee(self): val = self.cleaned_data['cancellation_fee'] or Decimal('0.00') diff --git a/src/pretix/control/templates/pretixcontrol/order/cancel.html b/src/pretix/control/templates/pretixcontrol/order/cancel.html index 72823c5e3..9d4264cea 100644 --- a/src/pretix/control/templates/pretixcontrol/order/cancel.html +++ b/src/pretix/control/templates/pretixcontrol/order/cancel.html @@ -23,13 +23,16 @@ {% bootstrap_form_errors form %} {% bootstrap_field form.send_email layout='' %} + {% if form.cancel_invoice %} + {% bootstrap_field form.cancel_invoice layout='' %} + {% endif %} {% if form.cancellation_fee %} {% bootstrap_field form.cancellation_fee layout='' %} {% endif %}
+ href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> {% trans "No, take me back" %}
diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html index 4c6e08833..5a271e8b2 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/index.html +++ b/src/pretix/control/templates/pretixcontrol/orders/index.html @@ -170,6 +170,10 @@ {% endif %} {{ o.total|money:request.event.currency }} + {% if o.status == "c" and o.icnt %} +
+ {% trans "INVOICE NOT CANCELED" %} + {% endif %} {{ o.pcnt|default_if_none:"0" }} {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index e10b2e3e4..625d872d6 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -151,6 +151,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, s = OrderPosition.objects.filter( order=OuterRef('pk') ).order_by().values('order').annotate(k=Count('id')).values('k') + i = Invoice.objects.filter( + order=OuterRef('pk'), + is_cancellation=False, + refered__isnull=True, + ).order_by().values('order').annotate(k=Count('id')).values('k') annotated = { o['pk']: o for o in @@ -158,10 +163,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, pk__in=[o.pk for o in ctx['orders']] ).annotate( pcnt=Subquery(s, output_field=IntegerField()), + icnt=Subquery(i, output_field=IntegerField()), has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk'))) ).values( 'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund', - 'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum' + 'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum', 'icnt' ) } @@ -177,6 +183,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, o.has_pending_refund = annotated.get(o.pk)['has_pending_refund'] o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request'] o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum'] + o.icnt = annotated.get(o.pk)['icnt'] o.sales_channel_obj = scs[o.sales_channel] if ctx['page_obj'].paginator.count < 1000: @@ -1134,6 +1141,7 @@ class OrderTransition(OrderView): try: cancel_order(self.order.pk, user=self.request.user, send_mail=self.mark_canceled_form.cleaned_data['send_email'], + cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True), cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee')) except OrderError as e: messages.error(self.request, str(e))