mirror of
https://github.com/pretix/pretix.git
synced 2026-05-12 16:24:00 +00:00
Centralise order filter logic, properly handle cancelled orders in sendmail (Z#23230149)
This commit is contained in:
@@ -55,7 +55,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.db.models.signals import post_delete
|
from django.db.models.signals import post_delete
|
||||||
@@ -134,6 +134,124 @@ class OrderQuerySet(models.QuerySet):
|
|||||||
raise Order.DoesNotExist
|
raise Order.DoesNotExist
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
def filter_by_status(self, qs, status: list[str]):
|
||||||
|
from .invoices import Invoice
|
||||||
|
|
||||||
|
if not isinstance(status, list):
|
||||||
|
raise ValueError("`status` needs to be a list of strings")
|
||||||
|
|
||||||
|
filter = Q()
|
||||||
|
|
||||||
|
if any(s in status for s in ['overpaid', 'pendingpaid', 'partially_paid', 'underpaid']):
|
||||||
|
results = any(s in status for s in ['underpaid', 'overpaid'])
|
||||||
|
sums = any(s in status for s in ['pendingpaid', 'partially_paid'])
|
||||||
|
qs = Order.annotate_overpayments(qs, refunds=False, results=results, sums=sums)
|
||||||
|
|
||||||
|
if 'o' in status:
|
||||||
|
filter |= Q(status=Order.STATUS_PENDING, expires__lt=now().replace(hour=0, minute=0, second=0))
|
||||||
|
if 'np' in status:
|
||||||
|
filter |= Q(status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||||
|
if 'ne' in status:
|
||||||
|
filter |= Q(status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
||||||
|
if 'pv' in status:
|
||||||
|
filter |= Q(status=Order.STATUS_PAID) | Q(status=Order.STATUS_PENDING, valid_if_pending=True)
|
||||||
|
for s in ('p', 'n', 'e', 'c', 'r'):
|
||||||
|
if s in status:
|
||||||
|
filter |= Q(status=s)
|
||||||
|
if 'overpaid' in status:
|
||||||
|
qs = Order.annotate_overpayments(qs, refunds=False, results=True, sums=False)
|
||||||
|
filter |= Q(is_overpaid=True)
|
||||||
|
if 'rc' in status:
|
||||||
|
filter |= Q(cancellation_requests__isnull=False)
|
||||||
|
qs = qs.annotate(
|
||||||
|
cancellation_request_time=Max('cancellation_requests__created')
|
||||||
|
).order_by(
|
||||||
|
'-cancellation_request_time'
|
||||||
|
)
|
||||||
|
if 'pendingpaid' in status:
|
||||||
|
filter |= (
|
||||||
|
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING),
|
||||||
|
pending_sum_t__lte=0,
|
||||||
|
require_approval=False)
|
||||||
|
)
|
||||||
|
if 'pendingnopayment' in status:
|
||||||
|
filter |= Q(
|
||||||
|
~Exists(
|
||||||
|
OrderPayment.objects.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=False,
|
||||||
|
)
|
||||||
|
if 'partially_paid' in status:
|
||||||
|
filter |= (
|
||||||
|
Q(
|
||||||
|
computed_payment_refund_sum__lt=F('total'),
|
||||||
|
computed_payment_refund_sum__gt=Decimal('0.00')
|
||||||
|
) & ~Q(
|
||||||
|
status=Order.STATUS_CANCELED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if 'underpaid' in status:
|
||||||
|
filter |= Q(is_underpaid=True)
|
||||||
|
if 'cni' in status:
|
||||||
|
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 |= Q(
|
||||||
|
icnt__gt=0,
|
||||||
|
status=Order.STATUS_CANCELED,
|
||||||
|
)
|
||||||
|
if 'pa' in status:
|
||||||
|
filter |= Q(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=True
|
||||||
|
)
|
||||||
|
if 'na' in status:
|
||||||
|
filter |= Q(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=False
|
||||||
|
)
|
||||||
|
if 'valid_if_confirmed' in status:
|
||||||
|
filter |= Q(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=False,
|
||||||
|
valid_if_pending=True
|
||||||
|
)
|
||||||
|
if 'custom_followup_at' in status:
|
||||||
|
filter |= Q(
|
||||||
|
custom_followup_at__isnull=False
|
||||||
|
)
|
||||||
|
if 'custom_followup_due' in status:
|
||||||
|
filter |= Q(
|
||||||
|
custom_followup_at__lte=now().astimezone(get_current_timezone()).date()
|
||||||
|
)
|
||||||
|
if 'testmode' in status:
|
||||||
|
filter |= Q(testmode=True)
|
||||||
|
if 'cp' in status:
|
||||||
|
has_pc = OrderPosition.objects.filter(
|
||||||
|
order=OuterRef('pk')
|
||||||
|
)
|
||||||
|
filter |= (
|
||||||
|
Q(~Exists(has_pc), status=Order.STATUS_PAID)
|
||||||
|
| Q(status=Order.STATUS_CANCELED)
|
||||||
|
)
|
||||||
|
if 'cany' in status:
|
||||||
|
has_pc_c = OrderPosition.all.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
canceled=True,
|
||||||
|
)
|
||||||
|
filter |= Q(Exists(has_pc_c)) | Q(status=Order.STATUS_CANCELED)
|
||||||
|
|
||||||
|
return qs.filter(filter)
|
||||||
|
|
||||||
|
|
||||||
class Order(LockModel, LoggedModel):
|
class Order(LockModel, LoggedModel):
|
||||||
"""
|
"""
|
||||||
@@ -204,6 +322,69 @@ class Order(LockModel, LoggedModel):
|
|||||||
(STATUS_CANCELED, _("canceled")),
|
(STATUS_CANCELED, _("canceled")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STATUS_FILTERS = (
|
||||||
|
('', _('All orders')),
|
||||||
|
(_('Valid orders'), (
|
||||||
|
(STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||||
|
(STATUS_PAID + 'v', _('Paid or confirmed')),
|
||||||
|
(STATUS_PENDING, _('Pending')),
|
||||||
|
(STATUS_PENDING + STATUS_PAID, _('Pending or paid')),
|
||||||
|
('valid_if_confirmed', _('Pending but already confirmed')),
|
||||||
|
)),
|
||||||
|
(_('Cancellations'), (
|
||||||
|
(STATUS_CANCELED, _('Canceled (fully)')),
|
||||||
|
('cp', _('Canceled (fully or with paid fee)')),
|
||||||
|
('cany', _('Canceled (at least one position)')),
|
||||||
|
('rc', _('Cancellation requested')),
|
||||||
|
('cni', _('Fully canceled but invoice not canceled')),
|
||||||
|
)),
|
||||||
|
(_('Payment process'), (
|
||||||
|
(STATUS_EXPIRED, _('Expired')),
|
||||||
|
(STATUS_PENDING + STATUS_EXPIRED, _('Pending or expired')),
|
||||||
|
('o', _('Pending (overdue)')),
|
||||||
|
('overpaid', _('Overpaid')),
|
||||||
|
('partially_paid', _('Partially paid')),
|
||||||
|
('underpaid', _('Underpaid (but confirmed)')),
|
||||||
|
('pendingpaid', _('Pending (but fully paid)')),
|
||||||
|
('pendingnopayment', _('Pending (but no current payment)')),
|
||||||
|
)),
|
||||||
|
(_('Approval process'), (
|
||||||
|
('na', _('Approved, payment pending')),
|
||||||
|
('pa', _('Approval pending')),
|
||||||
|
)),
|
||||||
|
(_('Follow-up date'), (
|
||||||
|
('custom_followup_at', _('Follow-up configured')),
|
||||||
|
('custom_followup_due', _('Follow-up due')),
|
||||||
|
)),
|
||||||
|
('testmode', _('Test mode')),
|
||||||
|
)
|
||||||
|
|
||||||
|
STATUS_FILTER_OPTIONS = (
|
||||||
|
(STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||||
|
(STATUS_PAID + 'v', _('Paid or confirmed')),
|
||||||
|
('valid_if_confirmed', _('Pending but already confirmed')),
|
||||||
|
(STATUS_PENDING, _('Pending')),
|
||||||
|
(STATUS_PENDING + STATUS_PAID, _('Pending or paid')),
|
||||||
|
(STATUS_CANCELED, _('Canceled (fully)')),
|
||||||
|
('cp', _('Canceled (fully or with paid fee)')),
|
||||||
|
('cany', _('Canceled (at least one position)')),
|
||||||
|
('rc', _('Cancellation requested')),
|
||||||
|
('cni', _('Fully canceled but invoice not canceled')),
|
||||||
|
(STATUS_EXPIRED, _('Expired')),
|
||||||
|
(STATUS_PENDING + STATUS_EXPIRED, _('Pending or expired')),
|
||||||
|
('o', _('Pending (overdue)')),
|
||||||
|
('overpaid', _('Overpaid')),
|
||||||
|
('partially_paid', _('Partially paid')),
|
||||||
|
('underpaid', _('Underpaid (but confirmed)')),
|
||||||
|
('pendingpaid', _('Pending (but fully paid)')),
|
||||||
|
('pendingnopayment', _('Pending (but no current payment)')),
|
||||||
|
('na', _('Approved, payment pending')),
|
||||||
|
('pa', _('Approval pending')),
|
||||||
|
('custom_followup_at', _('Follow-up configured')),
|
||||||
|
('custom_followup_due', _('Follow-up due')),
|
||||||
|
('testmode', _('Test mode')),
|
||||||
|
)
|
||||||
|
|
||||||
code = models.CharField(
|
code = models.CharField(
|
||||||
max_length=16,
|
max_length=16,
|
||||||
verbose_name=_("Order code"),
|
verbose_name=_("Order code"),
|
||||||
@@ -2282,7 +2463,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
|||||||
:type value: Decimal
|
:type value: Decimal
|
||||||
:param order: Order this fee is charged with
|
:param order: Order this fee is charged with
|
||||||
:type order: Order
|
:type order: Order
|
||||||
:param fee_type: The type of the fee, currently ``payment``, ``shipping``, ``service``, ``giftcard``, or ``other``.
|
:param fee_type: The type of the fee, currently ``payment``, ``shipping``, ``service``, ``cancellation``, ``insurance``, ``late``,
|
||||||
|
``giftcard``, or ``other``.
|
||||||
:type fee_type: str
|
:type fee_type: str
|
||||||
:param description: A human-readable description of the fee
|
:param description: A human-readable description of the fee
|
||||||
:type description: str
|
:type description: str
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from django import forms
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
|
Count, Exists, F, Model, OrderBy, OuterRef, Q, QuerySet,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce, ExtractWeekDay, Upper
|
from django.db.models.functions import Coalesce, ExtractWeekDay, Upper
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
@@ -219,41 +219,7 @@ class OrderFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
label=_('Order status'),
|
label=_('Order status'),
|
||||||
choices=(
|
choices=Order.STATUS_FILTERS,
|
||||||
('', _('All orders')),
|
|
||||||
(_('Valid orders'), (
|
|
||||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
|
||||||
(Order.STATUS_PAID + 'v', _('Paid or confirmed')),
|
|
||||||
(Order.STATUS_PENDING, _('Pending')),
|
|
||||||
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
|
|
||||||
)),
|
|
||||||
(_('Cancellations'), (
|
|
||||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
|
||||||
('cp', _('Canceled (fully or with paid fee)')),
|
|
||||||
('cany', _('Canceled (at least one position)')),
|
|
||||||
('rc', _('Cancellation requested')),
|
|
||||||
('cni', _('Fully canceled but invoice not canceled')),
|
|
||||||
)),
|
|
||||||
(_('Payment process'), (
|
|
||||||
(Order.STATUS_EXPIRED, _('Expired')),
|
|
||||||
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
|
|
||||||
('o', _('Pending (overdue)')),
|
|
||||||
('overpaid', _('Overpaid')),
|
|
||||||
('partially_paid', _('Partially paid')),
|
|
||||||
('underpaid', _('Underpaid (but confirmed)')),
|
|
||||||
('pendingpaid', _('Pending (but fully paid)')),
|
|
||||||
('pendingnopayment', _('Pending (but no current payment)')),
|
|
||||||
)),
|
|
||||||
(_('Approval process'), (
|
|
||||||
('na', _('Approved, payment pending')),
|
|
||||||
('pa', _('Approval pending')),
|
|
||||||
)),
|
|
||||||
(_('Follow-up date'), (
|
|
||||||
('custom_followup_at', _('Follow-up configured')),
|
|
||||||
('custom_followup_due', _('Follow-up due')),
|
|
||||||
)),
|
|
||||||
('testmode', _('Test mode')),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -309,114 +275,8 @@ class OrderFilterForm(FilterForm):
|
|||||||
mainq
|
mainq
|
||||||
)
|
)
|
||||||
|
|
||||||
if fdata.get('status'):
|
if s := fdata.get('status'):
|
||||||
s = fdata.get('status')
|
qs = qs.filter_by_status(qs, [s])
|
||||||
if s == 'o':
|
|
||||||
qs = qs.filter(status=Order.STATUS_PENDING, expires__lt=now().replace(hour=0, minute=0, second=0))
|
|
||||||
elif s == 'np':
|
|
||||||
qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
|
||||||
elif s == 'ne':
|
|
||||||
qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
|
||||||
elif s == 'pv':
|
|
||||||
qs = qs.filter(Q(status=Order.STATUS_PAID) | Q(status=Order.STATUS_PENDING, valid_if_pending=True))
|
|
||||||
elif s in ('p', 'n', 'e', 'c', 'r'):
|
|
||||||
qs = qs.filter(status=s)
|
|
||||||
elif s == 'overpaid':
|
|
||||||
qs = Order.annotate_overpayments(qs, refunds=False, results=True, sums=False)
|
|
||||||
qs = qs.filter(is_overpaid=True)
|
|
||||||
elif s == 'rc':
|
|
||||||
qs = qs.filter(
|
|
||||||
cancellation_requests__isnull=False
|
|
||||||
).annotate(
|
|
||||||
cancellation_request_time=Max('cancellation_requests__created')
|
|
||||||
).order_by(
|
|
||||||
'-cancellation_request_time'
|
|
||||||
)
|
|
||||||
elif s == '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 s == 'pendingnopayment':
|
|
||||||
qs = qs.exclude(
|
|
||||||
Exists(
|
|
||||||
OrderPayment.objects.filter(
|
|
||||||
order=OuterRef('pk'),
|
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).filter(
|
|
||||||
status=Order.STATUS_PENDING,
|
|
||||||
require_approval=False,
|
|
||||||
)
|
|
||||||
elif s == 'partially_paid':
|
|
||||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
|
||||||
qs = qs.filter(
|
|
||||||
computed_payment_refund_sum__lt=F('total'),
|
|
||||||
computed_payment_refund_sum__gt=Decimal('0.00')
|
|
||||||
).exclude(
|
|
||||||
status=Order.STATUS_CANCELED
|
|
||||||
)
|
|
||||||
elif s == 'underpaid':
|
|
||||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
|
||||||
qs = qs.filter(
|
|
||||||
Q(status=Order.STATUS_PAID, pending_sum_t__gt=0) |
|
|
||||||
Q(status=Order.STATUS_CANCELED, pending_sum_rc__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,
|
|
||||||
require_approval=True
|
|
||||||
)
|
|
||||||
elif s == 'na':
|
|
||||||
qs = qs.filter(
|
|
||||||
status=Order.STATUS_PENDING,
|
|
||||||
require_approval=False
|
|
||||||
)
|
|
||||||
elif s == 'custom_followup_at':
|
|
||||||
qs = qs.filter(
|
|
||||||
custom_followup_at__isnull=False
|
|
||||||
)
|
|
||||||
elif s == 'custom_followup_due':
|
|
||||||
qs = qs.filter(
|
|
||||||
custom_followup_at__lte=now().astimezone(get_current_timezone()).date()
|
|
||||||
)
|
|
||||||
elif s == 'testmode':
|
|
||||||
qs = qs.filter(
|
|
||||||
testmode=True
|
|
||||||
)
|
|
||||||
elif s == '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)
|
|
||||||
)
|
|
||||||
elif s == 'cany':
|
|
||||||
s = OrderPosition.all.filter(
|
|
||||||
order=OuterRef('pk'),
|
|
||||||
canceled=True,
|
|
||||||
)
|
|
||||||
qs = qs.annotate(
|
|
||||||
has_pc_c=Exists(s)
|
|
||||||
).filter(
|
|
||||||
Q(has_pc_c=True) | Q(status=Order.STATUS_CANCELED)
|
|
||||||
)
|
|
||||||
|
|
||||||
if fdata.get('ordering'):
|
if fdata.get('ordering'):
|
||||||
qs = qs.order_by(*get_deterministic_ordering(Order, self.get_order_by()))
|
qs = qs.order_by(*get_deterministic_ordering(Order, self.get_order_by()))
|
||||||
|
|||||||
@@ -151,7 +151,13 @@ class OrderMailForm(BaseMailForm):
|
|||||||
initial='orders',
|
initial='orders',
|
||||||
choices=[]
|
choices=[]
|
||||||
)
|
)
|
||||||
sendto = forms.MultipleChoiceField() # overridden later
|
sendto = forms.MultipleChoiceField(
|
||||||
|
label=pgettext_lazy('sendmail_form', 'Restrict to orders with status'),
|
||||||
|
widget=forms.CheckboxSelectMultiple(
|
||||||
|
attrs={'class': 'scrolling-multiple-choice no-search'}
|
||||||
|
),
|
||||||
|
choices=Order.STATUS_FILTER_OPTIONS
|
||||||
|
)
|
||||||
items = forms.ModelMultipleChoiceField(
|
items = forms.ModelMultipleChoiceField(
|
||||||
widget=forms.CheckboxSelectMultiple(
|
widget=forms.CheckboxSelectMultiple(
|
||||||
attrs={'class': 'scrolling-multiple-choice'}
|
attrs={'class': 'scrolling-multiple-choice'}
|
||||||
@@ -230,27 +236,15 @@ class OrderMailForm(BaseMailForm):
|
|||||||
self.fields['attach_tickets'].help_text = _("Attachment of tickets is disabled in this event's email "
|
self.fields['attach_tickets'].help_text = _("Attachment of tickets is disabled in this event's email "
|
||||||
"settings.")
|
"settings.")
|
||||||
|
|
||||||
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
|
||||||
choices.insert(0, ('valid_if_pending', _('payment pending but already confirmed')))
|
|
||||||
choices.insert(0, ('na', _('payment pending (except unapproved or already confirmed)')))
|
|
||||||
choices.insert(0, ('pa', _('approval pending')))
|
|
||||||
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
|
||||||
choices.append(
|
|
||||||
('overdue', _('pending with payment overdue'))
|
|
||||||
)
|
|
||||||
self.fields['sendto'] = forms.MultipleChoiceField(
|
|
||||||
label=pgettext_lazy('sendmail_form', 'Restrict to orders with status'),
|
|
||||||
widget=forms.CheckboxSelectMultiple(
|
|
||||||
attrs={'class': 'scrolling-multiple-choice no-search'}
|
|
||||||
),
|
|
||||||
choices=choices
|
|
||||||
)
|
|
||||||
if not self.initial.get('sendto'):
|
if not self.initial.get('sendto'):
|
||||||
self.initial['sendto'] = ['p', 'valid_if_pending']
|
self.initial['sendto'] = ['p', 'valid_if_pending']
|
||||||
elif 'n' in self.initial['sendto']:
|
else:
|
||||||
|
if 'n' in self.initial['sendto']:
|
||||||
self.initial['sendto'].append('pa')
|
self.initial['sendto'].append('pa')
|
||||||
self.initial['sendto'].append('na')
|
self.initial['sendto'].append('na')
|
||||||
self.initial['sendto'].append('valid_if_pending')
|
self.initial['sendto'].append('valid_if_pending')
|
||||||
|
if 'overdue' in self.initial['sendto']:
|
||||||
|
self.initial['sendto'].append('o')
|
||||||
|
|
||||||
self.fields['items'].queryset = event.items.all()
|
self.fields['items'].queryset = event.items.all()
|
||||||
if not self.initial.get('items'):
|
if not self.initial.get('items'):
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ from django.shortcuts import get_object_or_404, redirect
|
|||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _, ngettext
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
from django.views.generic import DeleteView, FormView, ListView, TemplateView
|
from django.views.generic import DeleteView, FormView, ListView, TemplateView
|
||||||
|
|
||||||
@@ -262,11 +261,8 @@ class OrderSendView(BaseSenderView):
|
|||||||
i.pk: str(i) for i in logentry.event.checkin_lists.all()
|
i.pk: str(i) for i in logentry.event.checkin_lists.all()
|
||||||
}
|
}
|
||||||
if 'status' not in _cache_store:
|
if 'status' not in _cache_store:
|
||||||
status = dict(Order.STATUS_CHOICE)
|
status = dict(Order.STATUS_FILTER_OPTIONS)
|
||||||
status['overdue'] = _('pending with payment overdue')
|
status['overdue'] = status['o']
|
||||||
status['valid_if_pending'] = _('payment pending but already confirmed')
|
|
||||||
status['na'] = _('payment pending (except unapproved or already confirmed)')
|
|
||||||
status['pa'] = _('approval pending')
|
|
||||||
status['r'] = status['c']
|
status['r'] = status['c']
|
||||||
_cache_store['status'] = status
|
_cache_store['status'] = status
|
||||||
|
|
||||||
@@ -343,16 +339,7 @@ class OrderSendView(BaseSenderView):
|
|||||||
|
|
||||||
def get_object_queryset(self, form):
|
def get_object_queryset(self, form):
|
||||||
qs = Order.objects.filter(event=self.request.event)
|
qs = Order.objects.filter(event=self.request.event)
|
||||||
statusq = Q(status__in=form.cleaned_data['sendto'])
|
orders = qs.filter_by_status(qs, form.cleaned_data['sendto'])
|
||||||
if 'overdue' in form.cleaned_data['sendto']:
|
|
||||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False, expires__lt=now())
|
|
||||||
if 'pa' in form.cleaned_data['sendto']:
|
|
||||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=True)
|
|
||||||
if 'na' in form.cleaned_data['sendto']:
|
|
||||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False)
|
|
||||||
if 'valid_if_pending' in form.cleaned_data['sendto']:
|
|
||||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
|
|
||||||
orders = qs.filter(statusq)
|
|
||||||
|
|
||||||
opq = OrderPosition.objects.filter(
|
opq = OrderPosition.objects.filter(
|
||||||
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
|
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
|
||||||
@@ -408,9 +395,10 @@ class OrderSendView(BaseSenderView):
|
|||||||
if form.cleaned_data.get('created_to'):
|
if form.cleaned_data.get('created_to'):
|
||||||
opq = opq.filter(order__datetime__lt=form.cleaned_data.get('created_to'))
|
opq = opq.filter(order__datetime__lt=form.cleaned_data.get('created_to'))
|
||||||
|
|
||||||
|
orders_without_positions = Order.objects.filter(~Exists(OrderPosition.objects.filter(canceled=False, order_id=OuterRef('pk'))))
|
||||||
# pk__in turns out to be faster than Exists(subquery) in many cases since we often filter on a large subset
|
# pk__in turns out to be faster than Exists(subquery) in many cases since we often filter on a large subset
|
||||||
# of orderpositions
|
# of orderpositions
|
||||||
return orders.filter(pk__in=opq.values_list('order_id'))
|
return orders.filter(Q(pk__in=opq.values_list('order_id')) | Q(pk__in=orders_without_positions))
|
||||||
|
|
||||||
def describe_match_size(self, cnt):
|
def describe_match_size(self, cnt):
|
||||||
return ngettext(
|
return ngettext(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from django.utils.timezone import now
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import Checkin, Item, Order, OrderPosition, Team, User
|
from pretix.base.models import Checkin, Item, Order, OrderPosition, Team, User
|
||||||
|
from pretix.base.models.orders import OrderFee
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -615,3 +616,123 @@ def test_waitinglist_sendmail_simple_case(logged_in_client, sendmail_url, event,
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert 'Test subject' in response.rendered_content
|
assert 'Test subject' in response.rendered_content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_sendmail_canceled(logged_in_client, sendmail_url, event, item):
|
||||||
|
event.settings.attendee_emails_asked = True
|
||||||
|
with scopes_disabled():
|
||||||
|
o1 = Order.objects.create(event=event, status=Order.STATUS_CANCELED,
|
||||||
|
expires=now() + datetime.timedelta(hours=1),
|
||||||
|
total=13, code='DUMMY1', email='order1@dummy.test',
|
||||||
|
datetime=now(),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
locale='de')
|
||||||
|
o1_p1 = OrderPosition.objects.create(order=o1, item=item, price=13, attendee_email='attendee1@dummy.test')
|
||||||
|
o2 = Order.objects.create(event=event, status=Order.STATUS_PAID,
|
||||||
|
expires=now() + datetime.timedelta(hours=1),
|
||||||
|
total=2, code='DUMMY2', email='order2@dummy.test',
|
||||||
|
datetime=now(),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
locale='de')
|
||||||
|
OrderPosition.objects.create(order=o2, item=item, price=13, canceled=True, attendee_email='attendee2@dummy.test')
|
||||||
|
OrderFee.objects.create(order=o2, fee_type=OrderFee.FEE_TYPE_CANCELLATION, value=2)
|
||||||
|
o3 = Order.objects.create(event=event, status=Order.STATUS_PAID,
|
||||||
|
expires=now() + datetime.timedelta(hours=1),
|
||||||
|
total=13, code='DUMMY3', email='order3@dummy.test',
|
||||||
|
datetime=now(),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
locale='de')
|
||||||
|
o3_p1 = OrderPosition.objects.create(order=o3, item=item, price=13, attendee_email='attendee3@dummy.test')
|
||||||
|
OrderPosition.objects.create(order=o3, item=item, price=13, canceled=True, attendee_email='attendee4@dummy.test')
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'c',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'orders',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
assert len(djmail.outbox) == 1
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1.email,), }
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'cp',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'orders',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
assert len(djmail.outbox) == 2
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1.email,), (o2.email,)}
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'cany',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'orders',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
assert len(djmail.outbox) == 3
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1.email,), (o2.email,), (o3.email,)}
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'c',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'attendees',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
assert len(djmail.outbox) == 1
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1_p1.attendee_email,), }
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'cp',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'attendees',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
# o2 has no active attendees
|
||||||
|
assert len(djmail.outbox) == 1
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1_p1.attendee_email,), }
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||||
|
{'sendto': 'cany',
|
||||||
|
'action': 'send',
|
||||||
|
'recipients': 'attendees',
|
||||||
|
'items': item.pk,
|
||||||
|
'subject_0': 'Test subject',
|
||||||
|
'message_0': 'This is a test file for sending mails.',
|
||||||
|
},
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
assert 'alert-success' in response.rendered_content
|
||||||
|
assert len(djmail.outbox) == 2
|
||||||
|
assert set(tuple(x.to) for x in djmail.outbox) == {(o1_p1.attendee_email,), (o3_p1.attendee_email,)}
|
||||||
|
|||||||
Reference in New Issue
Block a user