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 %} +

+ {% trans "Reactivate order" %} + + {% blocktrans trimmed with order=order.code %} + Back to order {{ order }} + {% endblocktrans %} + +

+

+ {% 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 %} +

+ +
+ {% csrf_token %} +
+ + {% trans "Cancel" %} + + +
+
+
+{% 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),