diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 158fbafc9..0b226e856 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -363,6 +363,28 @@ class OrderSearchFilterForm(OrderFilterForm): ) ) + status = forms.ChoiceField( + label=_('Order status'), + choices=( + ('', _('All orders')), + (Order.STATUS_PAID, _('Paid (or canceled with paid fee)')), + (Order.STATUS_PENDING, _('Pending')), + ('o', _('Pending (overdue)')), + (Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')), + (Order.STATUS_EXPIRED, _('Expired')), + (Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')), + (Order.STATUS_CANCELED, _('Canceled')), + ('cp', _('Canceled (or with paid fee)')), + ('pa', _('Approval pending')), + ('overpaid', _('Overpaid')), + ('underpaid', _('Underpaid')), + ('pendingpaid', _('Pending (but fully paid)')), + ('testmode', _('Test mode')), + ('rc', _('Cancellation requested')), + ), + required=False, + ) + def __init__(self, *args, **kwargs): request = kwargs.pop('request') super().__init__(*args, **kwargs) @@ -381,6 +403,47 @@ class OrderSearchFilterForm(OrderFilterForm): if fdata.get('organizer'): qs = qs.filter(event__organizer=fdata.get('organizer')) + if fdata.get('status') == 'overpaid': + qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True) + qs = qs.filter( + Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0)) + | Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0)) + ) + elif fdata.get('status') == 'rc': + qs = qs.filter( + cancellation_requests__isnull=False + ) + elif fdata.get('status') == 'pendingpaid': + qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True) + qs = qs.filter( + Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) + & Q(require_approval=False) + ) + elif fdata.get('status') == 'underpaid': + qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True) + qs = qs.filter( + status=Order.STATUS_PAID, + pending_sum_t__gt=0 + ) + elif fdata.get('status') == 'pa': + qs = qs.filter( + status=Order.STATUS_PENDING, + require_approval=True + ) + elif fdata.get('status') == 'testmode': + qs = qs.filter( + testmode=True + ) + elif fdata.get('status') == 'cp': + s = OrderPosition.objects.filter( + order=OuterRef('pk') + ) + qs = qs.annotate( + has_pc=Exists(s) + ).filter( + Q(status=Order.STATUS_PAID, has_pc=False) | Q(status=Order.STATUS_CANCELED) + ) + return qs diff --git a/src/pretix/control/templates/pretixcontrol/search/orders.html b/src/pretix/control/templates/pretixcontrol/search/orders.html index 8af809814..52e0a14f1 100644 --- a/src/pretix/control/templates/pretixcontrol/search/orders.html +++ b/src/pretix/control/templates/pretixcontrol/search/orders.html @@ -73,7 +73,24 @@ {% endif %} {{ o.datetime|date:"SHORT_DATETIME_FORMAT" }} - {{ o.total|money:o.event.currency }} + + {% if o.has_cancellation_request %} + {% trans "CANCELLATION REQUESTED" %} + {% endif %} + {% if o.has_external_refund or o.has_pending_refund %} + {% trans "REFUND PENDING" %} + {% elif o.has_pending_refund %} + {% trans "REFUND PENDING" %} + {% endif %} + {% if o.is_overpaid %} + {% trans "OVERPAID" %} + {% elif o.is_underpaid %} + {% trans "UNDERPAID" %} + {% elif o.is_pending_with_full_payment %} + {% trans "FULLY PAID" %} + {% endif %} + {{ o.total|money:o.event.currency }} + {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %} {% empty %} diff --git a/src/pretix/control/views/search.py b/src/pretix/control/views/search.py index 1f9ae3529..8e89ce5c3 100644 --- a/src/pretix/control/views/search.py +++ b/src/pretix/control/views/search.py @@ -1,9 +1,10 @@ from django.conf import settings -from django.db.models import Count, IntegerField, OuterRef, Q, Subquery +from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery from django.utils.functional import cached_property from django.views.generic import ListView from pretix.base.models import Order, OrderPosition +from pretix.base.models.orders import CancellationRequest from pretix.control.forms.filter import OrderSearchFilterForm from pretix.control.views import LargeResultSetPaginator, PaginationMixin @@ -29,12 +30,14 @@ class OrderSearch(PaginationMixin, ListView): annotated = { o['pk']: o for o in - Order.objects.using(settings.DATABASE_REPLICA).filter( + Order.annotate_overpayments(Order.objects).using(settings.DATABASE_REPLICA).filter( pk__in=[o.pk for o in ctx['orders']] ).annotate( - pcnt=Subquery(s, output_field=IntegerField()) + pcnt=Subquery(s, output_field=IntegerField()), + has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk'))) ).values( - 'pk', 'pcnt', + 'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund', + 'has_pending_refund', 'has_cancellation_request' ) } @@ -42,6 +45,12 @@ class OrderSearch(PaginationMixin, ListView): if o.pk not in annotated: continue o.pcnt = annotated.get(o.pk)['pcnt'] + o.is_overpaid = annotated.get(o.pk)['is_overpaid'] + o.is_underpaid = annotated.get(o.pk)['is_underpaid'] + o.is_pending_with_full_payment = annotated.get(o.pk)['is_pending_with_full_payment'] + o.has_external_refund = annotated.get(o.pk)['has_external_refund'] + o.has_pending_refund = annotated.get(o.pk)['has_pending_refund'] + o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request'] return ctx