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

@@ -23,7 +23,7 @@ import logging
from decimal import Decimal
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
from django.utils.translation import gettext
from i18nfield.strings import LazyI18nString
@@ -122,9 +122,13 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
orders_to_cancel = event.orders.annotate(pcnt=Subquery(s, output_field=IntegerField())).filter(
has_blocked = OrderPosition.objects.filter(order_id=OuterRef('pk'), blocked__isnull=False)
orders_to_cancel = event.orders.annotate(
pcnt=Subquery(s, output_field=IntegerField()),
has_blocked=Exists(has_blocked),
).filter(
status__in=[Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED],
pcnt__gt=0
pcnt__gt=0,
).all()
if subevent or subevents_from:
@@ -146,13 +150,14 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
has_subevent=Exists(has_subevent),
has_other_subevent=Exists(has_other_subevent),
).filter(
has_subevent=True, has_other_subevent=True
Q(has_subevent=True, has_other_subevent=True) |
Q(has_subevent=True, has_blocked=True)
)
orders_to_cancel = orders_to_cancel.annotate(
has_subevent=Exists(has_subevent),
has_other_subevent=Exists(has_other_subevent),
).filter(
has_subevent=True, has_other_subevent=False
has_subevent=True, has_other_subevent=False, has_blocked=False
)
for se in subevents:
@@ -167,7 +172,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
else:
subevents = None
subevent_ids = set()
orders_to_change = event.orders.none()
orders_to_change = orders_to_cancel.filter(has_blocked=True)
orders_to_cancel = orders_to_cancel.filter(has_blocked=False)
event.log_action(
'pretix.event.canceled', user=user,
)
@@ -247,7 +253,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
ocm = OrderChangeManager(o, user=user, notify=False)
for p in o.positions.all():
if p.subevent_id in subevent_ids:
if (not event.has_subevents or p.subevent_id in subevent_ids) and not p.blocked:
total += p.price
ocm.cancel(p)
positions.append(p)

View File

@@ -721,6 +721,34 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'canceled' if canceled_supported else 'unpaid'
)
if op.blocked:
raise CheckInError(
_('This ticket has been blocked.'), # todo provide reason
'blocked'
)
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
raise CheckInError(
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
),
)
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
raise CheckInError(
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
),
)
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
checkin_questions = list(
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
@@ -751,8 +779,13 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
_('This order position has an invalid date for this check-in list.'),
'product'
)
elif op.order.status != Order.STATUS_PAID and not force and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
elif op.order.status != Order.STATUS_PAID and not force and op.order.require_approval:
raise CheckInError(
_('This order is not yet approved.'),
'unpaid'
)
elif op.order.status != Order.STATUS_PAID and not force and not op.order.valid_if_pending and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
raise CheckInError(
_('This order is not marked as paid.'),

View File

@@ -68,7 +68,8 @@ from pretix.base.models import (
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
@@ -250,7 +251,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
generate_invoice(order)
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_pending: bool=None, user: User=None, auth=None):
"""
Extends the deadline of an order. If the order is already expired, the quota will be checked to
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
@@ -261,19 +262,35 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
@transaction.atomic
def change(was_expired=True):
old_date = order.expires
order.expires = new_date
if was_expired:
order.status = Order.STATUS_PENDING
order.save(update_fields=['expires'] + (['status'] if was_expired else []))
order.log_action(
'pretix.event.order.expirychanged',
user=user,
auth=auth,
data={
'expires': order.expires,
'state_change': was_expired
}
)
if valid_if_pending is not None and valid_if_pending != order.valid_if_pending:
order.valid_if_pending = valid_if_pending
if valid_if_pending:
order.log_action(
'pretix.event.order.valid_if_pending.set',
user=user,
auth=auth,
)
else:
order.log_action(
'pretix.event.order.valid_if_pending.unset',
user=user,
auth=auth,
)
order.save(update_fields=['valid_if_pending', 'expires'] + (['status'] if was_expired else []))
if old_date != new_date:
order.log_action(
'pretix.event.order.expirychanged',
user=user,
auth=auth,
data={
'expires': order.expires,
'state_change': was_expired
}
)
if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count()
@@ -1203,6 +1220,7 @@ def expire_orders(sender, **kwargs):
qs = Order.objects.filter(
expires__lt=now(),
status=Order.STATUS_PENDING,
valid_if_pending=False,
require_approval=False
).exclude(
Exists(
@@ -1247,11 +1265,17 @@ def send_expiry_warnings(sender, **kwargs):
with language(o.locale, settings.region):
o.expiry_reminder_sent = True
o.save(update_fields=['expiry_reminder_sent'])
email_template = settings.mail_text_order_expire_warning
email_context = get_email_context(event=o.event, order=o)
if settings.payment_term_expire_automatically:
can_autoexpire = (
settings.payment_term_expire_automatically and
not o.valid_if_pending and
not o.fees.filter(fee_type=OrderFee.FEE_TYPE_CANCELLATION).exists()
)
if can_autoexpire:
email_template = settings.mail_text_order_expire_warning
email_subject = settings.mail_subject_order_expire_warning
else:
email_template = settings.mail_text_order_pending_warning
email_subject = settings.mail_subject_order_pending_warning
try:
@@ -1310,8 +1334,7 @@ def send_download_reminders(sender, **kwargs):
positions = o.positions.select_related('item')
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or not \
o.event.settings.ticket_download_pending:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
continue
send = False
for p in positions:
@@ -1405,12 +1428,17 @@ class OrderChangeManager:
TaxRuleOperation = namedtuple('TaxRuleOperation', ('position', 'tax_rule'))
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
'valid_from', 'valid_until'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
ChangeValidFromOperation = namedtuple('ChangeValidFromOperation', ('position', 'valid_from'))
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name'))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
self.order = order
@@ -1514,6 +1542,18 @@ class OrderChangeManager:
def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position))
def change_valid_from(self, position: OrderPosition, new_value: datetime):
self._operations.append(self.ChangeValidFromOperation(position, new_value))
def change_valid_until(self, position: OrderPosition, new_value: datetime):
self._operations.append(self.ChangeValidUntilOperation(position, new_value))
def add_block(self, position: OrderPosition, block_name: str):
self._operations.append(self.AddBlockOperation(position, block_name))
def remove_block(self, position: OrderPosition, block_name: str):
self._operations.append(self.RemoveBlockOperation(position, block_name))
def change_price(self, position: OrderPosition, price: Decimal):
tax_rule = self._current_tax_rules().get(position.pk, position.tax_rule) or TaxRule.zero()
price = tax_rule.tax(price, base_price_is='gross')
@@ -1595,7 +1635,8 @@ class OrderChangeManager:
self._invoice_dirty = True
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None):
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
valid_from: datetime = None, valid_until: datetime = None):
if isinstance(seat, str):
if not seat:
seat = None
@@ -1649,7 +1690,8 @@ class OrderChangeManager:
self._quotadiff.update(new_quotas)
if seat:
self._seatdiff.update([seat])
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership))
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
valid_from, valid_until))
def split(self, position: OrderPosition):
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
@@ -1961,6 +2003,7 @@ class OrderChangeManager:
def _perform_operations(self):
nextposid = self.order.all_positions.aggregate(m=Max('positionid'))['m'] + 1
split_positions = []
secret_dirty = set()
for op in self._operations:
if isinstance(op, self.ItemOperation):
@@ -1986,9 +2029,7 @@ class OrderChangeManager:
else:
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate=False, save=False
)
secret_dirty.add(op.position)
op.position.save()
elif isinstance(op, self.MembershipOperation):
self.order.log_action('pretix.event.order.changed.membership', user=self.user, auth=self.auth, data={
@@ -2009,9 +2050,7 @@ class OrderChangeManager:
'new_seat_id': op.seat.pk if op.seat else None,
})
op.position.seat = op.seat
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate=False, save=False
)
secret_dirty.add(op.position)
op.position.save()
elif isinstance(op, self.SubeventOperation):
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
@@ -2023,9 +2062,7 @@ class OrderChangeManager:
'new_price': op.position.price
})
op.position.subevent = op.subevent
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate=False, save=False
)
secret_dirty.add(op.position)
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
@@ -2131,8 +2168,10 @@ class OrderChangeManager:
opa.canceled = True
if opa.voucher:
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
if opa in secret_dirty:
secret_dirty.remove(opa)
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
event=self.event, position=opa, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
)
opa.save(update_fields=['canceled', 'secret'])
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
@@ -2149,6 +2188,8 @@ class OrderChangeManager:
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
)
if op.position in secret_dirty:
secret_dirty.remove(op.position)
op.position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
@@ -2156,7 +2197,7 @@ class OrderChangeManager:
price=op.price.gross, order=self.order, tax_rate=op.price.rate,
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
@@ -2169,6 +2210,8 @@ class OrderChangeManager:
'membership': pos.used_membership_id,
'subevent': op.subevent.pk if op.subevent else None,
'seat': op.seat.pk if op.seat else None,
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
})
elif isinstance(op, self.SplitOperation):
split_positions.append(op.position)
@@ -2176,12 +2219,79 @@ class OrderChangeManager:
assign_ticket_secret(
event=self.event, position=op.position, force_invalidate=True, save=True
)
if op.position in secret_dirty:
secret_dirty.remove(op.position)
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.order.pk})
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
})
elif isinstance(op, self.ChangeValidFromOperation):
self.order.log_action('pretix.event.order.changed.valid_from', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'new_value': op.valid_from.isoformat() if op.valid_from else None,
'old_value': op.position.valid_from.isoformat() if op.position.valid_from else None,
})
op.position.valid_from = op.valid_from
op.position.save(update_fields=['valid_from'])
secret_dirty.add(op.position)
elif isinstance(op, self.ChangeValidUntilOperation):
self.order.log_action('pretix.event.order.changed.valid_until', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'new_value': op.valid_until.isoformat() if op.valid_until else None,
'old_value': op.position.valid_until.isoformat() if op.position.valid_until else None,
})
op.position.valid_until = op.valid_until
op.position.save(update_fields=['valid_until'])
secret_dirty.add(op.position)
elif isinstance(op, self.AddBlockOperation):
self.order.log_action('pretix.event.order.changed.add_block', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'block_name': op.block_name,
})
if op.position.blocked:
if op.block_name not in op.position.blocked:
op.position.blocked = op.position.blocked + [op.block_name]
else:
op.position.blocked = [op.block_name]
op.position.save(update_fields=['blocked'])
if op.position.blocked:
op.position.blocked_secrets.update_or_create(
event=self.event,
secret=op.position.secret,
defaults={
'blocked': True,
'updated': now(),
}
)
elif isinstance(op, self.RemoveBlockOperation):
self.order.log_action('pretix.event.order.changed.remove_block', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'block_name': op.block_name,
})
if op.position.blocked and op.block_name in op.position.blocked:
op.position.blocked = [b for b in op.position.blocked if b != op.block_name]
if not op.position.blocked:
op.position.blocked = None
op.position.save(update_fields=['blocked'])
if not op.position.blocked:
try:
bs = op.position.blocked_secrets.get(secret=op.position.secret)
bs.blocked = False
bs.save()
except BlockedTicketSecret.DoesNotExist:
pass
# todo: revoke list handling
for p in secret_dirty:
assign_ticket_secret(
event=self.event, position=p, force_invalidate=False, save=True
)
if split_positions:
self.split_order = self._create_split_order(split_positions)