Add order lifecycle signals

This commit is contained in:
Raphael Michel
2019-04-06 15:05:16 +02:00
parent c372bffc57
commit b686978074
6 changed files with 251 additions and 171 deletions

View File

@@ -20,7 +20,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle: There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download :members: validate_cart, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
Frontend Frontend
-------- --------

View File

@@ -42,7 +42,9 @@ from pretix.base.services.orders import (
extend_order, mark_order_expired, mark_order_refunded, extend_order, mark_order_expired, mark_order_refunded,
) )
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
from pretix.base.signals import order_placed, register_ticket_outputs from pretix.base.signals import (
order_modified, order_placed, register_ticket_outputs,
)
class OrderFilter(FilterSet): class OrderFilter(FilterSet):
@@ -451,61 +453,64 @@ class OrderViewSet(viewsets.ModelViewSet):
) )
return super().update(request, *args, **kwargs) return super().update(request, *args, **kwargs)
@transaction.atomic
def perform_update(self, serializer): def perform_update(self, serializer):
if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'): with transaction.atomic():
serializer.instance.log_action( if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'):
'pretix.event.order.comment', serializer.instance.log_action(
user=self.request.user, 'pretix.event.order.comment',
auth=self.request.auth, user=self.request.user,
data={ auth=self.request.auth,
'new_comment': self.request.data.get('comment') data={
} 'new_comment': self.request.data.get('comment')
) }
)
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'): if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
serializer.instance.log_action( serializer.instance.log_action(
'pretix.event.order.checkin_attention', 'pretix.event.order.checkin_attention',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={
'new_value': self.request.data.get('checkin_attention') 'new_value': self.request.data.get('checkin_attention')
} }
) )
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'): if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
serializer.instance.log_action( serializer.instance.log_action(
'pretix.event.order.contact.changed', 'pretix.event.order.contact.changed',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={
'old_email': serializer.instance.email, 'old_email': serializer.instance.email,
'new_email': self.request.data.get('email'), 'new_email': self.request.data.get('email'),
} }
) )
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'): if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
serializer.instance.log_action( serializer.instance.log_action(
'pretix.event.order.locale.changed', 'pretix.event.order.locale.changed',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={
'old_locale': serializer.instance.locale, 'old_locale': serializer.instance.locale,
'new_locale': self.request.data.get('locale'), 'new_locale': self.request.data.get('locale'),
} }
) )
if 'invoice_address' in self.request.data:
serializer.instance.log_action(
'pretix.event.order.modified',
user=self.request.user,
auth=self.request.auth,
data={
'invoice_data': self.request.data.get('invoice_address'),
}
)
serializer.save()
if 'invoice_address' in self.request.data: if 'invoice_address' in self.request.data:
serializer.instance.log_action( order_modified.send(sender=serializer.instance.event, order=serializer.instance)
'pretix.event.order.modified',
user=self.request.user,
auth=self.request.auth,
data={
'invoice_data': self.request.data.get('invoice_address'),
}
)
serializer.save()
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save() serializer.save()

View File

@@ -42,7 +42,9 @@ from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import ( from pretix.base.signals import (
allow_ticket_download, order_fee_calculation, order_placed, periodic_task, allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_placed,
periodic_task,
) )
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
@@ -134,55 +136,58 @@ def mark_order_refunded(order, user=None, auth=None, api_token=None):
) )
@transaction.atomic
def mark_order_expired(order, user=None, auth=None): def mark_order_expired(order, user=None, auth=None):
""" """
Mark this order as expired. This sets the payment status and returns the order object. Mark this order as expired. This sets the payment status and returns the order object.
:param order: The order to change :param order: The order to change
:param user: The user that performed the change :param user: The user that performed the change
""" """
if isinstance(order, int): with transaction.atomic():
order = Order.objects.get(pk=order) if isinstance(order, int):
if isinstance(user, int): order = Order.objects.get(pk=order)
user = User.objects.get(pk=user) if isinstance(user, int):
with order.event.lock(): user = User.objects.get(pk=user)
order.status = Order.STATUS_EXPIRED with order.event.lock():
order.save(update_fields=['status']) order.status = Order.STATUS_EXPIRED
order.save(update_fields=['status'])
order.log_action('pretix.event.order.expired', user=user, auth=auth) order.log_action('pretix.event.order.expired', user=user, auth=auth)
i = order.invoices.filter(is_cancellation=False).last() i = order.invoices.filter(is_cancellation=False).last()
if i: if i:
generate_cancellation(i) generate_cancellation(i)
order_expired.send(order.event, order=order)
return order return order
@transaction.atomic
def approve_order(order, user=None, send_mail: bool=True, auth=None): def approve_order(order, user=None, send_mail: bool=True, auth=None):
""" """
Mark this order as approved Mark this order as approved
:param order: The order to change :param order: The order to change
:param user: The user that performed the change :param user: The user that performed the change
""" """
if not order.require_approval or not order.status == Order.STATUS_PENDING: with transaction.atomic():
raise OrderError(_('This order is not pending approval.')) if not order.require_approval or not order.status == Order.STATUS_PENDING:
raise OrderError(_('This order is not pending approval.'))
order.require_approval = False order.require_approval = False
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()])) order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
order.save(update_fields=['require_approval', 'expires']) order.save(update_fields=['require_approval', 'expires'])
order.log_action('pretix.event.order.approved', user=user, auth=auth) order.log_action('pretix.event.order.approved', user=user, auth=auth)
if order.total == Decimal('0.00'): if order.total == Decimal('0.00'):
p = order.payments.create( p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED, state=OrderPayment.PAYMENT_STATE_CREATED,
provider='free', provider='free',
amount=0, amount=0,
fee=None fee=None
) )
try: try:
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth) p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
raise OrderError(error_messages['unavailable']) raise OrderError(error_messages['unavailable'])
order_approved.send(order.event, order=order)
invoice = order.invoices.last() # Might be generated by plugin already invoice = order.invoices.last() # Might be generated by plugin already
if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -234,30 +239,32 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None):
return order.pk return order.pk
@transaction.atomic
def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
""" """
Mark this order as canceled Mark this order as canceled
:param order: The order to change :param order: The order to change
:param user: The user that performed the change :param user: The user that performed the change
""" """
if not order.require_approval or not order.status == Order.STATUS_PENDING: with transaction.atomic():
raise OrderError(_('This order is not pending approval.')) if not order.require_approval or not order.status == Order.STATUS_PENDING:
raise OrderError(_('This order is not pending approval.'))
with order.event.lock(): with order.event.lock():
order.status = Order.STATUS_CANCELED order.status = Order.STATUS_CANCELED
order.save(update_fields=['status']) order.save(update_fields=['status'])
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={ order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
'comment': comment 'comment': comment
}) })
i = order.invoices.filter(is_cancellation=False).last() i = order.invoices.filter(is_cancellation=False).last()
if i: if i:
generate_cancellation(i) generate_cancellation(i)
for position in order.positions.all(): for position in order.positions.all():
if position.voucher: if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
order_denied.send(order.event, order=order)
if send_mail: if send_mail:
try: try:
@@ -294,7 +301,6 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
return order.pk return order.pk
@transaction.atomic
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None): cancellation_fee=None):
""" """
@@ -302,85 +308,87 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
:param order: The order to change :param order: The order to change
:param user: The user that performed the change :param user: The user that performed the change
""" """
if isinstance(order, int): with transaction.atomic():
order = Order.objects.get(pk=order) if isinstance(order, int):
if isinstance(user, int): order = Order.objects.get(pk=order)
user = User.objects.get(pk=user) if isinstance(user, int):
if isinstance(api_token, int): user = User.objects.get(pk=user)
api_token = TeamAPIToken.objects.get(pk=api_token) if isinstance(api_token, int):
if isinstance(device, int): api_token = TeamAPIToken.objects.get(pk=api_token)
device = Device.objects.get(pk=device) if isinstance(device, int):
if isinstance(oauth_application, int): device = Device.objects.get(pk=device)
oauth_application = OAuthApplication.objects.get(pk=oauth_application) if isinstance(oauth_application, int):
if isinstance(cancellation_fee, str): oauth_application = OAuthApplication.objects.get(pk=oauth_application)
cancellation_fee = Decimal(cancellation_fee) if isinstance(cancellation_fee, str):
cancellation_fee = Decimal(cancellation_fee)
if not order.cancel_allowed(): if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.')) raise OrderError(_('You cannot cancel this order.'))
i = order.invoices.filter(is_cancellation=False).last() i = order.invoices.filter(is_cancellation=False).last()
if i: if i:
generate_cancellation(i) generate_cancellation(i)
if cancellation_fee:
with order.event.lock():
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
position.save(update_fields=['canceled'])
for fee in order.fees.all():
fee.canceled = True
fee.save(update_fields=['canceled'])
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=cancellation_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
)
f._calculate_tax()
f.save()
if order.payment_refund_sum < cancellation_fee:
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
order.status = Order.STATUS_PAID
order.total = f.value
order.save(update_fields=['status', 'total'])
if i:
generate_invoice(order)
else:
with order.event.lock():
order.status = Order.STATUS_CANCELED
order.save(update_fields=['status'])
if cancellation_fee:
with order.event.lock():
for position in order.positions.all(): for position in order.positions.all():
if position.voucher: if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
position.save(update_fields=['canceled'])
for fee in order.fees.all():
fee.canceled = True
fee.save(update_fields=['canceled'])
f = OrderFee( order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
fee_type=OrderFee.FEE_TYPE_CANCELLATION, data={'cancellation_fee': cancellation_fee})
value=cancellation_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
)
f._calculate_tax()
f.save()
if order.payment_refund_sum < cancellation_fee: if send_mail:
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.')) email_template = order.event.settings.mail_text_order_canceled
order.status = Order.STATUS_PAID email_context = {
order.total = f.value 'event': order.event.name,
order.save(update_fields=['status', 'total']) 'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
if i: 'order': order.code,
generate_invoice(order) 'secret': order.secret
else: })
with order.event.lock(): }
order.status = Order.STATUS_CANCELED with language(order.locale):
order.save(update_fields=['status']) email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
for position in order.positions.all(): order.send_mail(
if position.voucher: email_subject, email_template, email_context,
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) 'pretix.event.order.email.order_canceled', user
)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device, except SendMailException:
data={'cancellation_fee': cancellation_fee}) logger.exception('Order canceled email could not be sent')
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
email_context = {
'event': order.event.name,
'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
})
}
with language(order.locale):
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_canceled', user
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
order_canceled.send(order.event, order=order)
return order.pk return order.pk
@@ -1377,6 +1385,8 @@ class OrderChangeManager:
if self.split_order: if self.split_order:
self._notify_user(self.split_order) self._notify_user(self.split_order)
order_changed.send(self.order.event, order=self.order)
def _clear_tickets_cache(self): def _clear_tickets_cache(self):
CachedTicket.objects.filter(order_position__order=self.order).delete() CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete() CachedCombinedTicket.objects.filter(order=self.order).delete()

View File

@@ -275,6 +275,66 @@ because an already-paid order has been split.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event. As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
""" """
order_canceled = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order is canceled. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_expired = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order is marked as expired. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_modified = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order's information is modified. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_changed = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order's content is changed. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_approved = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order is being approved. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_denied = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time an order is being denied. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
logentry_display = EventPluginSignal( logentry_display = EventPluginSignal(
providing_args=["logentry"] providing_args=["logentry"]
) )

View File

@@ -57,7 +57,7 @@ from pretix.base.services.orders import (
from pretix.base.services.stats import order_overview from pretix.base.services.stats import order_overview
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
from pretix.base.signals import ( from pretix.base.signals import (
register_data_exporters, register_ticket_outputs, order_modified, register_data_exporters, register_ticket_outputs,
) )
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
@@ -1321,6 +1321,8 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
CachedTicket.objects.filter(order_position__order=self.order).delete() CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete() CachedCombinedTicket.objects.filter(order=self.order).delete()
order_modified.send(sender=self.request.event, order=self.order)
return redirect(self.get_order_url()) return redirect(self.get_order_url())

View File

@@ -30,7 +30,9 @@ from pretix.base.services.invoices import (
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import cancel_order, change_payment_provider from pretix.base.services.orders import cancel_order, change_payment_provider
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
from pretix.base.signals import allow_ticket_download, register_ticket_outputs from pretix.base.signals import (
allow_ticket_download, order_modified, register_ticket_outputs,
)
from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
@@ -540,6 +542,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
for k in f.changed_data for k in f.changed_data
} for f in self.forms] } for f in self.forms]
}) })
order_modified.send(sender=self.request.event, order=self.order)
if self.invoice_form.has_changed(): if self.invoice_form.has_changed():
success_message = ('Your invoice address has been updated. Please contact us if you need us ' success_message = ('Your invoice address has been updated. Please contact us if you need us '
'to regenerate your invoice.') 'to regenerate your invoice.')