New check-in features (#3022)

This commit is contained in:
Raphael Michel
2023-02-09 09:46:46 +01:00
committed by GitHub
parent 7b0d07065f
commit 6902725f3c
69 changed files with 1606 additions and 183 deletions

View File

@@ -1052,7 +1052,7 @@ class MailSettingsForm(SettingsForm):
"value is 0, the mail will never be sent.")
)
mail_text_order_expire_warning = I18nFormField(
label=_("Text"),
label=_("Text (if order will expire automatically)"),
required=False,
widget=I18nTextarea,
)
@@ -1061,6 +1061,11 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextInput,
)
mail_text_order_pending_warning = I18nFormField(
label=_("Text (if order will not expire automatically)"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_pending_warning = I18nFormField(
label=_("Subject (if order will not expire automatically)"),
required=False,

View File

@@ -210,6 +210,7 @@ class OrderFilterForm(FilterForm):
('', _('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')),
)),
@@ -296,6 +297,8 @@ class OrderFilterForm(FilterForm):
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':

View File

@@ -387,7 +387,6 @@ class ItemCreateForm(I18nModelForm):
'show_quota_left',
'hidden_if_available',
'require_bundling',
'checkin_attention',
'require_membership',
'grant_membership_type',
'grant_membership_duration_like_event',
@@ -800,6 +799,7 @@ class ItemVariationForm(I18nModelForm):
'require_membership',
'require_membership_hidden',
'require_membership_types',
'checkin_attention',
'available_from',
'available_until',
'sales_channels',

View File

@@ -68,12 +68,6 @@ from pretix.helpers.money import change_decimal_field
class ExtendForm(I18nModelForm):
quota_ignore = forms.BooleanField(
label=_('Overbook quota'),
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
'and you having sold more tickets than you planned!'),
required=False
)
expires = forms.DateField(
label=_("Expiration date"),
widget=forms.DateInput(attrs={
@@ -81,16 +75,35 @@ class ExtendForm(I18nModelForm):
'data-is-payment-date': 'true'
}),
)
valid_if_pending = forms.BooleanField(
label=_('Confirm order regardless of payment'),
help_text=_('If you check this box, this order will behave like a paid order for most purposes, even though it '
'is not yet paid. This means that the customer can already download and use tickets regardless '
'of your event settings, and the order might be treated as paid by some plugins. If you check '
'this, this order will not be marked as "expired" automatically if the payment deadline arrives, '
'since we expect that you want to collect the amount somehow and not auto-cancel the order.'),
required=False
)
quota_ignore = forms.BooleanField(
label=_('Overbook quota'),
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
'and you having sold more tickets than you planned!'),
required=False
)
class Meta:
model = Order
fields = []
def __init__(self, *args, **kwargs):
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault('valid_if_pending', kwargs['instance'].valid_if_pending)
kwargs['initial'].setdefault('expires', kwargs['instance'].expires)
super().__init__(*args, **kwargs)
if self.instance.status == Order.STATUS_PENDING or self.instance._is_still_available(now(),
count_waitinglist=False)\
is True:
if (
self.instance.status == Order.STATUS_PENDING or
self.instance._is_still_available(now(), count_waitinglist=False) is True
):
del self.fields['quota_ignore']
def clean(self):
@@ -435,6 +448,20 @@ class OrderPositionChangeForm(forms.Form):
localize=True,
label=_('New price (gross)')
)
blocked = forms.BooleanField(
required=False,
label=_('Ticket is blocked')
)
valid_from = SplitDateTimeField(
required=False,
widget=SplitDateTimePickerWidget,
label=_('Validity start')
)
valid_until = SplitDateTimeField(
required=False,
widget=SplitDateTimePickerWidget,
label=_('Validity end')
)
used_membership = forms.ChoiceField(
required=False,
)
@@ -466,6 +493,9 @@ class OrderPositionChangeForm(forms.Form):
initial = kwargs.get('initial', {})
initial['price'] = instance.price
initial['blocked'] = instance.blocked and "admin" in instance.blocked
initial['valid_from'] = instance.valid_from
initial['valid_until'] = instance.valid_until
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
@@ -756,7 +786,8 @@ class EventCancelForm(forms.Form):
label=_('Automatically refund money if possible'),
initial=True,
required=False,
help_text=_('Only available for payment method that support automatic refunds.')
help_text=_('Only available for payment method that support automatic refunds. Tickets that have been blocked '
'(manually or by a plugin) are not auto-canceled and you will need to deal with them manually.')
)
manual_refund = forms.BooleanField(
label=_('Create refund in the manual refund to-do list'),

View File

@@ -52,7 +52,7 @@ from pretix.base.models import (
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
TaxRule,
)
from pretix.base.signals import logentry_display
from pretix.base.signals import logentry_display, orderposition_blocked_display
from pretix.base.templatetags.money import money_filter
OVERVIEW_BANLIST = [
@@ -162,6 +162,25 @@ def _display_order_changed(event: Event, logentry: LogEntry):
return text + ' ' + _('A new secret has been generated for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.valid_from':
return text + ' ' + _('The validity start date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get(
'new_value') else ''
)
elif logentry.action_type == 'pretix.event.order.changed.valid_until':
return text + ' ' + _('The validity end date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get('new_value') else ''
)
elif logentry.action_type == 'pretix.event.order.changed.add_block':
return text + ' ' + _('A block has been added for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.remove_block':
return text + ' ' + _('A block has been removed for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.split':
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
@@ -351,6 +370,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
'pretix.event.order.valid_if_pending.set': _('The order has been set to be usable before it is paid.'),
'pretix.event.order.valid_if_pending.unset': _('The order has been set to require payment before use.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
@@ -376,6 +397,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'),
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
'toggled.'),
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
'unpaid has been toggled.'),
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
@@ -647,3 +670,11 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.control.auth.user.impersonate_stopped':
return str(_('You stopped impersonating {}.')).format(data['other_email'])
@receiver(signal=orderposition_blocked_display, dispatch_uid="pretixcontrol_orderposition_blocked_display")
def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, block_name, **kwargs):
if block_name == 'admin':
return _('Blocked manually')
elif block_name.startswith('api:'):
return _('Blocked because of an API integration')

View File

@@ -103,7 +103,7 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_subject_order_changed,mail_text_order_changed" %}
{% blocktrans asvar title_payment_reminder %}Payment reminder{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_subject_order_expire_warning,mail_subject_order_pending_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_subject_order_expire_warning,mail_text_order_expire_warning,mail_subject_order_pending_warning,mail_text_order_pending_warning" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_subject_waiting_list,mail_text_waiting_list" %}

View File

@@ -107,6 +107,7 @@
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
{% bootstrap_field form.checkin_attention layout="control" %}
</div>
</details>
{% endfor %}
@@ -204,6 +205,7 @@
{% bootstrap_field formset.empty_form.require_membership_hidden layout="control" %}
</div>
{% endif %}
{% bootstrap_field formset.empty_form.checkin_attention layout="control" %}
</div>
</details>
{% endescapescript %}

View File

@@ -18,6 +18,7 @@
<select name="status" class="form-control">
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>

View File

@@ -189,6 +189,55 @@
</div>
</div>
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Ticket block" %}</strong>
</div>
<div class="col-sm-5">
{% if "admin" in position.blocked %}
{% trans "Blocked" %}
{% elif position.blocked %}
{% trans "Blocked due to external constraints" %}
{% else %}
{% trans "Not blocked" %}
{% endif %}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.blocked layout='inline' %}
</div>
</div>
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Validity time" %}</strong>
</div>
<div class="col-sm-5">
{% if position.valid_from %}
{% blocktrans trimmed with datetime=position.valid_from|date:"SHORT_DATETIME_FORMAT" %}
Valid from {{ datetime }}
{% endblocktrans %}
{% endif %}
{% if position.valid_until %}
{% if position.valid_from %}
<br />
{% endif %}
{% blocktrans trimmed with datetime=position.valid_from|date:"SHORT_DATETIME_FORMAT" %}
Valid until {{ datetime }}
{% endblocktrans %}
{% endif %}
{% if not position.valid_from and not position.valid_until %}
{% trans "Unconstrained" %}
{% endif %}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.valid_from layout='inline' %}
<div class="text-center">
{% trans "" %}
</div>
{% bootstrap_field position.form.valid_until layout='inline' %}
</div>
</div>
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Ticket secret" %}</strong>

View File

@@ -194,7 +194,10 @@
<dt>{% trans "Expiry date" %}</dt>
<dd>
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
{% if has_cancellation_fee and request.event.settings.payment_term_expire_automatically %}
{% if order.valid_if_pending %}
<span class="fa fa-warning text-danger" data-toggle="tooltip"
title="{% trans "This order will not expire automatically since it is already confirmed and can be used." %}"></span>
{% elif has_cancellation_fee and request.event.settings.payment_term_expire_automatically %}
<span class="fa fa-warning text-danger" data-toggle="tooltip"
title="{% trans "This order will not expire automatically as it has an open cancellation fee." %}"></span>
{% endif %}
@@ -390,6 +393,15 @@
{% endif %}
{% endfor %}
{% endif %}
{% if line.blocked %}
<br />
<strong class="text-danger">
<span class="fa fa-ban fa-fw text-danger"></span>
{% for k, block_reason in line.blocked_reasons.items %}
{{ block_reason }}
{% endfor %}
</strong>
{% endif %}
{% if line.seat %}
<br />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 4.7624999 3.7041668" class="svg-icon">
@@ -429,6 +441,26 @@
</a>
</span>
{% endif %}
{% if line.valid_from or line.valid_until %}
<div class="cart-icon-details">
<dd>
<span class="fa fa-clock-o fa-fw" aria-hidden="true"></span>
{% if line.valid_from and line.valid_until %}
{% blocktrans trimmed with datetime_from=line.valid_from|date:"SHORT_DATETIME_FORMAT" datetime_until=line.valid_until|date:"SHORT_DATETIME_FORMAT" %}
Valid from {{ datetime_from }} until {{ datetime_until }}
{% endblocktrans %}
{% elif line.valid_from %}
{% blocktrans trimmed with datetime=line.valid_from|date:"SHORT_DATETIME_FORMAT" %}
Valid from {{ datetime }}
{% endblocktrans %}
{% elif line.valid_until %}
{% blocktrans trimmed with datetime=line.valid_until|date:"SHORT_DATETIME_FORMAT" %}
Valid until {{ datetime }}
{% endblocktrans %}
{% endif %}
</dd>
</div>
{% endif %}
{% if not line.canceled %}
<div class="position-buttons">
{% if line.generate_ticket %}

View File

@@ -6,6 +6,11 @@
<span class="fa fa-question-circle"></span>
{% trans "Approval pending" %}
</span>
{% elif order.valid_if_pending %}
<span data-toggle="tooltip" class="label label-info {{ class }}">
<span class="fa fa-money"></span>
{% trans "Pending (confirmed)" context "order state" %}
</span>
{% else %}
<span data-toggle="tooltip" title="{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}"
class="label label-warning {{ class }}">

View File

@@ -36,7 +36,7 @@ import dateutil.parser
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
@@ -85,9 +85,16 @@ class CheckInListQueryMixin:
m=Max('datetime')
).values('m')
if self.list.include_pending:
status_q = Q(order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING])
else:
status_q = Q(
Q(order__status=Order.STATUS_PAID) |
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
)
qs = OrderPosition.objects.filter(
status_q,
order__event=self.request.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.list.include_pending else [Order.STATUS_PAID],
).annotate(
last_entry=Subquery(cqs),
last_exit=Subquery(cqs_exit),
@@ -199,7 +206,9 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', request=request):
raise PermissionDenied()
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
if op.order.status == Order.STATUS_PAID or (
(self.list.include_pending or op.order.valid_if_pending) and op.order.status == Order.STATUS_PENDING
):
Checkin.objects.filter(position=op, list=self.list).delete()
op.order.log_action('pretix.event.checkin.reverted', data={
'position': op.id,
@@ -213,7 +222,9 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
else:
t = Checkin.TYPE_EXIT if request.POST.get('checkout') == 'true' else Checkin.TYPE_ENTRY
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
if op.order.status == Order.STATUS_PAID or (
(self.list.include_pending or op.order.valid_if_pending) and op.order.status == Order.STATUS_PENDING
):
lci = op.checkins.filter(list=self.list).first()
if self.list.allow_multiple_entries or t != Checkin.TYPE_ENTRY or (lci and lci.type != Checkin.TYPE_ENTRY):
ci = Checkin.objects.create(position=op, list=self.list, datetime=now(), type=t)

View File

@@ -629,6 +629,11 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
orderposition__order__expires__lt=now().replace(hour=0, minute=0, second=0))
elif s == 'np':
qs = qs.filter(orderposition__order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
elif s == 'pv':
qs = qs.filter(
Q(orderposition__order__status=Order.STATUS_PAID) |
Q(orderposition__order__status=Order.STATUS_PENDING, orderposition__order__valid_if_pending=True)
)
elif s == 'ne':
qs = qs.filter(orderposition__order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
else:

View File

@@ -1520,6 +1520,7 @@ class OrderExtend(OrderView):
self.order,
new_date=self.form.cleaned_data.get('expires'),
force=self.form.cleaned_data.get('quota_ignore', False),
valid_if_pending=self.form.cleaned_data.get('valid_if_pending', False),
user=self.request.user
)
messages.success(self.request, _('The payment term has been changed.'))
@@ -1773,6 +1774,17 @@ class OrderChange(OrderView):
if p.form.cleaned_data['tax_rule'] and p.form.cleaned_data['tax_rule'] != p.tax_rule:
ocm.change_tax_rule(p, p.form.cleaned_data['tax_rule'])
if p.form.cleaned_data["blocked"] and "admin" not in (p.blocked or []):
ocm.add_block(p, "admin")
elif not p.form.cleaned_data["blocked"] and "admin" in (p.blocked or []):
ocm.remove_block(p, "admin")
if p.form.cleaned_data['valid_from'] != p.valid_from:
ocm.change_valid_from(p, p.form.cleaned_data['valid_from'])
if p.form.cleaned_data['valid_until'] != p.valid_until:
ocm.change_valid_until(p, p.form.cleaned_data['valid_until'])
if p.form.cleaned_data.get('operation_split'):
ocm.split(p)