diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 44c5652c16..181839c1ac 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -151,6 +151,10 @@ last_modified datetime Last modificati The ``order.fees.canceled`` attribute has been added. +.. versionchanged:: 3.8 + + The ``reactivate`` operation has been added. + .. _order-position-resource: @@ -1057,6 +1061,42 @@ Order state operations :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 404: The requested order does not exist. +.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/ + + Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are + still available. + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "code": "ABC12", + "status": "n", + ... + } + + :param organizer: The ``slug`` field of the organizer to modify + :param event: The ``slug`` field of the event to modify + :param code: The ``code`` field of the order to modify + :statuscode 200: no error + :statuscode 400: The order cannot be reactivated + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. + :statuscode 404: The requested order does not exist. + .. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/ Marks a paid order as unpaid. diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 587718a801..9dd1603985 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -20,7 +20,7 @@ Order events There are multiple signals that will be sent out in the ordering cycle: .. automodule:: pretix.base.signals - :members: validate_cart, validate_cart_addons, validate_order, 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, order_split, order_gracefully_delete, invoice_line_text + :members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text Check-ins """"""""" diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index e905ef10b2..4d6aa505be 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( OrderChangeManager, OrderError, _order_placed_email, _order_placed_email_attendee, approve_order, cancel_order, deny_order, - extend_order, mark_order_expired, mark_order_refunded, + extend_order, mark_order_expired, mark_order_refunded, reactivate_order, ) from pretix.base.services.pricing import get_price from pretix.base.services.tickets import generate @@ -261,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet): ) return self.retrieve(request, [], **kwargs) + @action(detail=True, methods=['POST']) + def reactivate(self, request, **kwargs): + + order = self.get_object() + if order.status != Order.STATUS_CANCELED: + return Response( + {'detail': 'The order is not allowed to be reactivated.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + reactivate_order( + order, + user=request.user if request.user.is_authenticated else None, + auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, + ) + except OrderError as e: + return Response( + {'detail': str(e)}, + status=status.HTTP_400_BAD_REQUEST + ) + return self.retrieve(request, [], **kwargs) + @action(detail=True, methods=['POST']) def approve(self, request, **kwargs): send_mail = request.data.get('send_email', True) diff --git a/src/pretix/api/webhooks.py b/src/pretix/api/webhooks.py index b9ae9ca4e0..71a0182332 100644 --- a/src/pretix/api/webhooks.py +++ b/src/pretix/api/webhooks.py @@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs): 'pretix.event.order.canceled', _('Order canceled'), ), + ParametrizedOrderWebhookEvent( + 'pretix.event.order.reactivated', + _('Order reactivated'), + ), ParametrizedOrderWebhookEvent( 'pretix.event.order.expired', _('Order expired'), diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index f04202dc00..b8363fe389 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -4,6 +4,7 @@ import json import logging import os import string +from collections import Counter from datetime import datetime, time, timedelta from decimal import Decimal from typing import Any, Dict, List, Union @@ -694,16 +695,19 @@ class Order(LockModel, LoggedModel): return self._is_still_available(count_waitinglist=count_waitinglist, force=force) - def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]: + def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False, + check_voucher_usage=False) -> Union[bool, str]: error_messages = { 'unavailable': _('The ordered product "{item}" is no longer available.'), 'seat_unavailable': _('The seat "{seat}" is no longer available.'), 'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'), + 'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'), } now_dt = now_dt or now() positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher') quota_cache = {} v_budget = {} + v_usage = Counter() try: for i, op in enumerate(positions): if op.seat: @@ -722,6 +726,13 @@ class Order(LockModel, LoggedModel): )) v_budget[op.voucher] -= disc + if op.voucher and check_voucher_usage: + v_usage[op.voucher.pk] += 1 + if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages: + raise Quota.QuotaExceededException(error_messages['voucher_usages'].format( + voucher=op.voucher.code + )) + quotas = list(op.quotas) if len(quotas) == 0: raise Quota.QuotaExceededException(error_messages['unavailable'].format( diff --git a/src/pretix/base/notifications.py b/src/pretix/base/notifications.py index 141ed6ac7d..707ca25c3d 100644 --- a/src/pretix/base/notifications.py +++ b/src/pretix/base/notifications.py @@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs): _('Order canceled'), _('Order {order.code} has been canceled.') ), + ParametrizedOrderNotificationType( + sender, + 'pretix.event.order.reactivated', + _('Order reactivated'), + _('Order {order.code} has been reactivated.') + ), ParametrizedOrderNotificationType( sender, 'pretix.event.order.expired', diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index d66a00d5ef..0496209598 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -94,6 +94,53 @@ def mark_order_paid(*args, **kwargs): raise NotImplementedError("This method is no longer supported since pretix 1.17.") +def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None): + """ + Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not + enough quota. + """ + if order.status != Order.STATUS_CANCELED: + raise OrderError('The order was not canceled.') + + with order.event.lock() as now_dt: + is_available = force or order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True) + if is_available is True: + if order.payment_refund_sum >= order.total: + order.status = Order.STATUS_PAID + else: + order.status = Order.STATUS_PENDING + order.set_expires(now(), + order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()])) + with transaction.atomic(): + order.save(update_fields=['expires', 'status']) + order.log_action( + 'pretix.event.order.reactivated', + user=user, + auth=auth, + data={ + 'expires': order.expires, + } + ) + for position in order.positions.all(): + if position.voucher: + Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1)) + + for gc in position.issued_gift_cards.all(): + gc = GiftCard.objects.select_for_update().get(pk=gc.pk) + gc.transactions.create(value=position.price, order=order) + break + else: + raise OrderError(is_available) + + order_approved.send(order.event, order=order) + if order.status == Order.STATUS_PAID: + order_paid.send(order.event, order=order) + + num_invoices = order.invoices.filter(is_cancellation=False).count() + if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order): + generate_invoice(order) + + def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None): """ Extends the deadline of an order. If the order is already expired, the quota will be checked to @@ -117,9 +164,10 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User 'state_change': was_expired } ) + if was_expired: num_invoices = order.invoices.filter(is_cancellation=False).count() - if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices: + if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order): generate_invoice(order) if order.status == Order.STATUS_PENDING: @@ -277,6 +325,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device :param order: The order to change :param user: The user that performed the change """ + # If new actions are added to this function, make sure to add the reverse operation to reactivate_order() with transaction.atomic(): if isinstance(order, int): order = Order.objects.select_for_update().get(pk=order) diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index a2fab17a1c..59f17d734d 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -341,6 +341,16 @@ as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ +order_reactivated = EventPluginSignal( + providing_args=["order"] +) +""" +This signal is sent out every time a canceled order is reactivated. 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"] ) diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index c68ec4aee3..517fcfb1b8 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -191,6 +191,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.order.paid': _('The order has been marked as paid.'), 'pretix.event.order.refunded': _('The order has been refunded.'), 'pretix.event.order.canceled': _('The order has been canceled.'), + 'pretix.event.order.reactivated': _('The order has been reactivated.'), 'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'), 'pretix.event.order.placed': _('The order has been created.'), 'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'), diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index f68dd56d37..d51bb00aa1 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -59,6 +59,10 @@ {% trans "Cancel order" %} + {% elif order.status == 'c' %} + + {% trans "Reactivate order" %} + {% endif %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/order/reactivate.html b/src/pretix/control/templates/pretixcontrol/order/reactivate.html new file mode 100644 index 0000000000..bfedc21679 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/order/reactivate.html @@ -0,0 +1,38 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %} + {% trans "Reactivate order" %} +{% endblock %} +{% block content %} +
+ {% blocktrans trimmed %} + By reactivating the order, you reverse its cancellation and transform this back into a pending or paid order. + This is only possible as long as all products in the order are still available. + If the order is pending payment, the expiry date will be reset. + {% endblocktrans %} +
+ + +{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index f22c8348b3..c6743915cd 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -222,6 +222,8 @@ urlpatterns = [ name='event.order.checkvatid'), url(r'^orders/(?P[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
name='event.order.extend'),
+ url(r'^orders/(?P[0-9A-Z]+)/reactivate$', orders.OrderReactivate.as_view(),
+ name='event.order.reactivate'),
url(r'^orders/(?P[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),
name='event.order.contact'),
url(r'^orders/(?P[0-9A-Z]+)/locale', orders.OrderLocaleChange.as_view(),
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py
index c0d1d9c2dc..bcc2a84e0b 100644
--- a/src/pretix/control/views/orders.py
+++ b/src/pretix/control/views/orders.py
@@ -57,7 +57,7 @@ from pretix.base.services.mail import SendMailException, render_mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
- notify_user_changed_order,
+ notify_user_changed_order, reactivate_order,
)
from pretix.base.services.stats import order_overview
from pretix.base.services.tickets import generate
@@ -1261,6 +1261,42 @@ class OrderExtend(OrderView):
data=self.request.POST if self.request.method == "POST" else None)
+class OrderReactivate(OrderView):
+ permission = 'can_change_orders'
+
+ def post(self, *args, **kwargs):
+ try:
+ reactivate_order(
+ self.order,
+ user=self.request.user
+ )
+ messages.success(self.request, _('The order has been reactivated.'))
+ except OrderError as e:
+ messages.error(self.request, str(e))
+ return self._redirect_here()
+ except LockTimeoutException:
+ messages.error(self.request, _('We were not able to process the request completely as the '
+ 'server was too busy.'))
+ return self._redirect_back()
+
+ def dispatch(self, request, *args, **kwargs):
+ if self.order.status != Order.STATUS_CANCELED:
+ messages.error(self.request, _('This action is only allowed for canceled orders.'))
+ return self._redirect_back()
+ return super().dispatch(request, *kwargs, **kwargs)
+
+ def _redirect_here(self):
+ return redirect('control:event.order.reactivate',
+ event=self.request.event.slug,
+ organizer=self.request.event.organizer.slug,
+ code=self.order.code)
+
+ def get(self, *args, **kwargs):
+ return render(self.request, 'pretixcontrol/order/reactivate.html', {
+ 'order': self.order,
+ })
+
+
class OrderChange(OrderView):
permission = 'can_change_orders'
template_name = 'pretixcontrol/order/change.html'
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index cf39b65322..9b8d5ac04a 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -1097,6 +1097,29 @@ def test_order_mark_paid_locked(token_client, organizer, event, order):
assert order.status == Order.STATUS_EXPIRED
+@pytest.mark.django_db
+def test_order_reactivate(token_client, organizer, event, order, quota):
+ order.status = Order.STATUS_CANCELED
+ order.save()
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/{}/reactivate/'.format(
+ organizer.slug, event.slug, order.code
+ )
+ )
+ assert resp.status_code == 200
+ assert resp.data['status'] == Order.STATUS_PENDING
+
+
+@pytest.mark.django_db
+def test_order_reactivate_invalid(token_client, organizer, event, order):
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/{}/reactivate/'.format(
+ organizer.slug, event.slug, order.code
+ )
+ )
+ assert resp.status_code == 400
+
+
@pytest.mark.django_db
def test_order_mark_canceled_pending(token_client, organizer, event, order):
djmail.outbox = []
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index 59e0e70406..b2897a3189 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -23,7 +23,8 @@ from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _create_order, approve_order, cancel_order,
- deny_order, expire_orders, send_download_reminders, send_expiry_warnings,
+ deny_order, expire_orders, reactivate_order, send_download_reminders,
+ send_expiry_warnings,
)
from pretix.base.signals import register_sales_channels
from pretix.plugins.banktransfer.payment import BankTransfer
@@ -1457,7 +1458,7 @@ class OrderChangeManagerTests(TestCase):
self.ocm.commit()
ops = list(self.order.positions.all())
for op in ops:
- assert op.price == Decimal('23.01') # sic. we can't really avoid it.
+ assert op.price == Decimal('23.01') # sic. we can't really avoid it.
assert op.tax_value == Decimal('1.51')
assert op.tax_rate == Decimal('7.00')
@@ -2169,7 +2170,8 @@ class OrderChangeManagerTests(TestCase):
def test_clear_out_order(self):
self.order.status = Order.STATUS_PAID
self.order.save()
- self.order.payments.create(amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED, provider='manual')
+ self.order.payments.create(amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED,
+ provider='manual')
cancel_order(self.order, cancellation_fee=Decimal('5.00'))
self.order.refresh_from_db()
assert self.order.total == Decimal('5.00')
@@ -2187,7 +2189,8 @@ class OrderChangeManagerTests(TestCase):
self.order.total = Decimal('51.1')
self.order.save()
- self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee, provider="banktransfer")
+ self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee,
+ provider="banktransfer")
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('10.00'))
prov.settings.set('_fee_reverse_calc', False)
@@ -2205,7 +2208,8 @@ class OrderChangeManagerTests(TestCase):
self.order.total = Decimal('50.60')
self.order.save()
- self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee, provider="banktransfer")
+ self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee,
+ provider="banktransfer")
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('10.00'))
prov.settings.set('_fee_reverse_calc', False)
@@ -2224,7 +2228,8 @@ class OrderChangeManagerTests(TestCase):
self.order.total = Decimal('50.60')
self.order.save()
- self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee, provider="banktransfer")
+ self.order.payments.create(state=OrderPayment.PAYMENT_STATE_PENDING, amount=Decimal('48.5'), fee=fee,
+ provider="banktransfer")
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('10.00'))
prov.settings.set('_fee_reverse_calc', False)
@@ -2463,3 +2468,118 @@ def test_issue_when_paid_and_changed(event):
op2 = order.positions.last()
gc2 = op2.issued_gift_cards.get()
assert gc2.value == op2.price
+
+
+class OrderReactivateTest(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.o = Organizer.objects.create(name='Dummy', slug='dummy')
+ with scope(organizer=self.o):
+ self.event = Event.objects.create(organizer=self.o, name='Dummy', slug='dummy', date_from=now(),
+ plugins='tests.testdummy')
+ self.order = Order.objects.create(
+ code='FOO', event=self.event, email='dummy@dummy.test',
+ status=Order.STATUS_CANCELED, locale='en',
+ datetime=now(), expires=now() + timedelta(days=1),
+ total=Decimal('46.00'),
+ )
+ self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
+ default_price=Decimal('23.00'), admission=True)
+ self.op1 = OrderPosition.objects.create(
+ order=self.order, item=self.ticket, variation=None,
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
+ )
+ self.op2 = OrderPosition.objects.create(
+ order=self.order, item=self.ticket, variation=None,
+ price=Decimal("23.00"), attendee_name_parts={'full_name': "Dieter"}, positionid=2
+ )
+ self.stalls = Item.objects.create(event=self.event, name='Stalls',
+ default_price=Decimal('23.00'), admission=True)
+ self.plan = SeatingPlan.objects.create(
+ name="Plan", organizer=self.o, layout="{}"
+ )
+ self.event.seat_category_mappings.create(
+ layout_category='Stalls', product=self.stalls
+ )
+ self.quota = self.event.quotas.create(name='Test', size=None)
+ self.quota.items.add(self.stalls)
+ self.quota.items.add(self.ticket)
+ self.seat_a1 = self.event.seats.create(name="A1", product=self.stalls, seat_guid="A1")
+ generate_invoice(self.order)
+ djmail.outbox = []
+
+ @classscope(attr='o')
+ def test_paid(self):
+ self.order.status = Order.STATUS_PAID
+ self.order.save()
+ with pytest.raises(OrderError):
+ reactivate_order(self.order)
+
+ @classscope(attr='o')
+ def test_reactivate_unpaid(self):
+ e = self.order.expires
+ reactivate_order(self.order)
+ self.order.refresh_from_db()
+ assert self.order.status == Order.STATUS_PENDING
+ assert self.order.all_logentries().last().action_type == 'pretix.event.order.reactivated'
+ assert self.order.invoices.count() == 3
+ assert self.order.expires > e > now()
+
+ @classscope(attr='o')
+ def test_reactivate_paid(self):
+ self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
+ reactivate_order(self.order)
+ self.order.refresh_from_db()
+ assert self.order.status == Order.STATUS_PAID
+ assert self.order.all_logentries().last().action_type == 'pretix.event.order.reactivated'
+ assert self.order.invoices.count() == 3
+
+ @classscope(attr='o')
+ def test_reactivate_sold_out(self):
+ self.quota.size = 0
+ self.quota.save()
+ with pytest.raises(OrderError):
+ reactivate_order(self.order)
+
+ @classscope(attr='o')
+ def test_reactivate_seat_taken(self):
+ self.op1.item = self.stalls
+ self.op1.seat = self.seat_a1
+ self.op1.save()
+ self.seat_a1.blocked = True
+ self.seat_a1.save()
+ with pytest.raises(OrderError):
+ reactivate_order(self.order)
+
+ @classscope(attr='o')
+ def test_reactivate_voucher_ok(self):
+ self.op1.voucher = self.event.vouchers.create(code="FOO", item=self.ticket, redeemed=0, max_usages=1)
+ self.op1.save()
+ reactivate_order(self.order)
+ v = self.op1.voucher
+ v.refresh_from_db()
+ assert v.redeemed == 1
+
+ @classscope(attr='o')
+ def test_reactivate_voucher_budget(self):
+ self.op1.voucher = self.event.vouchers.create(code="FOO", item=self.ticket, budget=Decimal('0.00'))
+ self.op1.price_before_voucher = self.op1.price * 2
+ self.op1.save()
+ with pytest.raises(OrderError):
+ reactivate_order(self.order)
+
+ @classscope(attr='o')
+ def test_reactivate_voucher_used(self):
+ self.op1.voucher = self.event.vouchers.create(code="FOO", item=self.ticket, redeemed=1, max_usages=1)
+ self.op1.save()
+ with pytest.raises(OrderError):
+ reactivate_order(self.order)
+ v = self.op1.voucher
+ v.refresh_from_db()
+ assert v.redeemed == 1
+
+ @classscope(attr='o')
+ def test_reactivate_gift_card(self):
+ gc = self.o.issued_gift_cards.create(currency="EUR", issued_in=self.op1)
+ reactivate_order(self.order)
+ assert gc.value == 23
diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py
index 83569ce348..0f276e799d 100644
--- a/src/tests/control/test_orders.py
+++ b/src/tests/control/test_orders.py
@@ -574,6 +574,37 @@ def test_order_resend_link(client, env):
assert 'FOO' in mail.outbox[0].body
+@pytest.mark.django_db
+def test_order_reactivate_not_canceled(client, env):
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ o.status = Order.STATUS_PAID
+ o.save()
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ response = client.get('/control/event/dummy/dummy/orders/FOO/reactivate', follow=True)
+ assert 'alert-danger' in response.content.decode()
+ response = client.post('/control/event/dummy/dummy/orders/FOO/reactivate', follow=True)
+ assert 'alert-danger' in response.content.decode()
+
+
+@pytest.mark.django_db
+def test_order_reactivate(client, env):
+ with scopes_disabled():
+ q = Quota.objects.create(event=env[0], size=3)
+ q.items.add(env[3])
+ o = Order.objects.get(id=env[2].id)
+ o.status = Order.STATUS_CANCELED
+ o.save()
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ response = client.post('/control/event/dummy/dummy/orders/FOO/reactivate', {
+ }, follow=True)
+ print(response.content.decode())
+ assert 'alert-success' in response.content.decode()
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ assert o.status == Order.STATUS_PENDING
+
+
@pytest.mark.django_db
def test_order_extend_not_pending(client, env):
with scopes_disabled():
diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py
index 655d46adaa..2b8c357824 100644
--- a/src/tests/control/test_permissions.py
+++ b/src/tests/control/test_permissions.py
@@ -102,6 +102,7 @@ event_urls = [
"orders/ABC/resend",
"orders/ABC/invoice",
"orders/ABC/extend",
+ "orders/ABC/reactivate",
"orders/ABC/change",
"orders/ABC/contact",
"orders/ABC/comment",
@@ -274,6 +275,7 @@ event_permission_urls = [
("can_view_orders", "orders/", 200),
("can_view_orders", "orders/FOO/", 200),
("can_change_orders", "orders/FOO/extend", 200),
+ ("can_change_orders", "orders/FOO/reactivate", 302),
("can_change_orders", "orders/FOO/contact", 200),
("can_change_orders", "orders/FOO/transition", 405),
("can_change_orders", "orders/FOO/checkvatid", 405),