forked from CGM_Public/pretix_original
Check-in: New flags for check-in lists (#3577)
This commit is contained in:
@@ -37,12 +37,18 @@ allow_entry_after_exit boolean If ``true``, su
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
|
||||
ignore_in_statistics boolean If ``true``, check-ins on this list will be ignored in most reporting features.
|
||||
consider_tickets_used boolean If ``true`` (default), tickets checked in on this list will be considered "used" by other functionality, i.e. when checking if they can still be canceled.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2023.9
|
||||
|
||||
The ``ignore_in_statistics`` and ``consider_tickets_used`` attributes have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -767,4 +773,4 @@ Order position endpoints
|
||||
:statuscode 404: The requested order position or check-in list does not exist.
|
||||
|
||||
|
||||
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
|
||||
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
|
||||
|
||||
@@ -38,7 +38,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.4 on 2023-09-06 11:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0246_bigint"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="checkinlist",
|
||||
name="consider_tickets_used",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="checkinlist",
|
||||
name="ignore_in_statistics",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -62,6 +62,16 @@ class CheckinList(LoggedModel):
|
||||
'and valid for check-in regardless of which date they are purchased for. '
|
||||
'You can limit their validity through the advanced check-in rules, '
|
||||
'though.'))
|
||||
ignore_in_statistics = models.BooleanField(
|
||||
verbose_name=pgettext_lazy('checkin', 'Ignore check-ins on this list in statistics'),
|
||||
default=False
|
||||
)
|
||||
consider_tickets_used = models.BooleanField(
|
||||
verbose_name=pgettext_lazy('checkin', 'Tickets with a check-in on this list should be considered "used"'),
|
||||
help_text=_('This is relevant in various situations, e.g. for deciding if a ticket can still be canceled by '
|
||||
'the customer.'),
|
||||
default=True
|
||||
)
|
||||
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
|
||||
@@ -633,7 +633,7 @@ class Order(LockModel, LoggedModel):
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('issued_gift_cards')
|
||||
)
|
||||
if self.event.settings.change_allow_user_if_checked_in:
|
||||
@@ -665,7 +665,7 @@ class Order(LockModel, LoggedModel):
|
||||
return False
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('issued_gift_cards')
|
||||
)
|
||||
cancelable = all([op.item.allow_cancel and not op.has_checkin and not op.blocked for op in positions])
|
||||
@@ -820,7 +820,7 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
|
||||
@@ -1996,7 +1996,7 @@ class OrderChangeManager:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
if a.checkins.exists():
|
||||
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
||||
raise OrderError(
|
||||
error_messages['addon_already_checked_in'] % {
|
||||
'addon': str(a.item.name),
|
||||
|
||||
@@ -2072,7 +2072,8 @@ class VoucherFilterForm(FilterForm):
|
||||
qs = qs.filter(Q(valid_until__isnull=False) & Q(valid_until__lt=now())).filter(redeemed=0)
|
||||
elif s == 'c':
|
||||
checkins = Checkin.objects.filter(
|
||||
position__voucher=OuterRef('pk')
|
||||
position__voucher=OuterRef('pk'),
|
||||
list__consider_tickets_used=True,
|
||||
)
|
||||
qs = qs.annotate(has_checkin=Exists(checkins)).filter(
|
||||
redeemed__gt=0, has_checkin=True
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-check {% if c.list.consider_tickets_used %}text-success{% else %}text-muted{% endif %}" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -133,10 +133,10 @@ class ScheduledMail(models.Model):
|
||||
|
||||
if self.rule.checked_in_status == "no_checkin":
|
||||
filter_orders_by_op = True
|
||||
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
|
||||
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))
|
||||
elif self.rule.checked_in_status == "checked_in":
|
||||
filter_orders_by_op = True
|
||||
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
|
||||
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))
|
||||
|
||||
status_q = Q(status__in=self.rule.restrict_to_status)
|
||||
if 'n__pending_approval' in self.rule.restrict_to_status:
|
||||
|
||||
@@ -67,6 +67,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
any_checkins=Exists(
|
||||
Checkin.objects.filter(
|
||||
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
|
||||
list__consider_tickets_used=True,
|
||||
)
|
||||
),
|
||||
matching_checkins=Exists(
|
||||
|
||||
@@ -373,7 +373,8 @@ class OrderSendView(BaseSenderView):
|
||||
any_checkins=Exists(
|
||||
Checkin.all.filter(
|
||||
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
|
||||
successful=True
|
||||
successful=True,
|
||||
list__consider_tickets_used=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -246,7 +246,10 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
|
||||
qs = qs.annotate(
|
||||
checkin_count=Subquery(
|
||||
Checkin.objects.filter(
|
||||
successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk')
|
||||
successful=True,
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
position_id=OuterRef('pk'),
|
||||
list__consider_tickets_used=True,
|
||||
).order_by().values('position').annotate(c=Count('*')).values('c')
|
||||
)
|
||||
)
|
||||
@@ -358,7 +361,10 @@ class OrderPositionDetails(EventViewMixin, OrderPositionDetailMixin, CartMixin,
|
||||
qs = qs.annotate(
|
||||
checkin_count=Subquery(
|
||||
Checkin.objects.filter(
|
||||
successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk')
|
||||
successful=True,
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
position_id=OuterRef('pk'),
|
||||
list__consider_tickets_used=True,
|
||||
).order_by().values('position').annotate(c=Count('*')).values('c')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -218,6 +218,8 @@ TEST_LIST_RES = {
|
||||
"subevent": None,
|
||||
"exit_all_at": None,
|
||||
"addon_match": False,
|
||||
"ignore_in_statistics": False,
|
||||
"consider_tickets_used": True,
|
||||
"rules": {}
|
||||
}
|
||||
|
||||
|
||||
@@ -776,6 +776,26 @@ class OrdersTest(BaseOrdersTest):
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
|
||||
def test_orders_cancel_paid_checkin_list(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
with scopes_disabled():
|
||||
cl = self.event.checkin_lists.create(name="Foo")
|
||||
self.order.positions.first().checkins.create(list=cl)
|
||||
self.event.settings.cancel_allow_user_paid = True
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
cl.consider_tickets_used = False
|
||||
cl.save()
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_orders_cancel_forbidden(self):
|
||||
self.event.settings.set('cancel_allow_user', False)
|
||||
self.client.post(
|
||||
|
||||
Reference in New Issue
Block a user