Allow to keep cancellation fees (#1130)

* Allow to keep cancellation fees

* Add tests and clarifications

* Add API
This commit is contained in:
Raphael Michel
2019-01-11 15:42:33 +01:00
committed by GitHub
parent 0b8798a65c
commit 60c1ea8aad
10 changed files with 302 additions and 95 deletions

View File

@@ -126,6 +126,11 @@ last_modified datetime Last modificati
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4:
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. _order-position-resource:
Order position resource
@@ -775,7 +780,10 @@ Order state operations
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_canceled/
Marks a pending order as canceled.
Cancels an order. For a pending order, this will set the order to status ``c``. For a paid order, this will set
the order to status ``c`` if no ``cancellation_fee`` is passed. If you do pass a ``cancellation_fee``, the order
will instead stay paid, but all positions will be removed (or marked as canceled) and replaced by the cancellation
fee as the only component of the order.
**Example request**:
@@ -787,7 +795,8 @@ Order state operations
Content-Type: text/json
{
"send_email": true
"send_email": true,
"cancellation_fee": null
}
**Example response**:
@@ -848,44 +857,6 @@ 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)/mark_refunded/
Marks a paid order as refunded.
.. warning:: In the current implementation, this will **bypass** the payment provider, i.e. the money will **not** be
transferred back to the user automatically, the order will only be *marked* as refunded within pretix.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_expired/ 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": "c",
...
}
: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 marked as expired since the current order status does not allow it.
: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_expired/
Marks a unpaid order as expired.
@@ -1502,7 +1473,7 @@ Order payment endpoints
{
"amount": "23.00",
"mark_refunded": false
"mark_canceled": false
}
@@ -1649,7 +1620,7 @@ Order refund endpoints
"payment": 1,
"execution_date": null,
"provider": "manual",
"mark_refunded": false
"mark_canceled": false
}
**Example response**:
@@ -1719,7 +1690,7 @@ Order refund endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/process/
Acts on an external refund, either marks the order as refunded or pending. Only allowed in state ``external``.
Acts on an external refund, either marks the order as canceled or pending. Only allowed in state ``external``.
**Example request**:
@@ -1730,7 +1701,7 @@ Order refund endpoints
Accept: application/json, text/javascript
Content-Type: application/json
{"mark_refunded": false}
{"mark_canceled": false}
**Example response**:

View File

@@ -1,4 +1,5 @@
import datetime
from decimal import Decimal
import django_filters
import pytz
@@ -186,6 +187,12 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
@detail_route(methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
cancellation_fee = request.data.get('cancellation_fee', None)
if cancellation_fee:
try:
cancellation_fee = float(Decimal(cancellation_fee))
except:
cancellation_fee = None
order = self.get_object()
if not order.cancel_allowed():
@@ -194,14 +201,21 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
device=request.auth if isinstance(request.auth, Device) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail
)
try:
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
device=request.auth if isinstance(request.auth, Device) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail,
cancellation_fee=cancellation_fee
)
except OrderError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@@ -515,7 +529,10 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('amount', str(payment.amount))
)
mark_refunded = request.data.get('mark_refunded', False)
if 'mark_refunded' in request.data:
mark_refunded = request.data.get('mark_refunded', False)
else:
mark_refunded = request.data.get('mark_canceled', False)
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
@@ -624,7 +641,11 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
if request.data.get('mark_refunded', False):
if 'mark_refunded' in request.data:
mark_refunded = request.data.get('mark_refunded', False)
else:
mark_refunded = request.data.get('mark_canceled', False)
if mark_refunded:
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth)
elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0):
@@ -653,7 +674,10 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return ctx
def create(self, request, *args, **kwargs):
mark_refunded = request.data.pop('mark_refunded', False)
if 'mark_refunded' in request.data:
mark_refunded = request.data.pop('mark_refunded', False)
else:
mark_refunded = request.data.pop('mark_canceled', False)
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():

View File

@@ -327,9 +327,8 @@ class Item(LoggedModel):
allow_cancel = models.BooleanField(
verbose_name=_('Allow product to be canceled'),
default=True,
help_text=_('If this is active and the general event settings allow it, orders containing this product can be '
'canceled by the user until the order is paid for. Users cannot cancel paid orders on their own '
'and you can cancel orders at all times, regardless of this setting')
help_text=_('If this is checked, the usual cancellation settings of this event apply. If this is unchecked, '
'orders containing this product can not be canceled by users but only by you.')
)
min_per_order = models.IntegerField(
verbose_name=_('Minimum amount per order'),

View File

@@ -293,7 +293,8 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
@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):
"""
Mark this order as canceled
:param order: The order to change
@@ -309,20 +310,52 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
device = Device.objects.get(pk=device)
if isinstance(oauth_application, int):
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
with order.event.lock():
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
order.status = Order.STATUS_CANCELED
order.save(update_fields=['status'])
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device)
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
i = order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
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=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'])
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
@@ -1331,10 +1364,11 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None):
device=None, cancellation_fee=None):
try:
try:
return _cancel_order(order, user, send_mail, api_token, device, oauth_application)
return _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -77,6 +77,40 @@ class ConfirmPaymentForm(forms.Form):
del self.fields['force']
class CancelForm(ConfirmPaymentForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify user by e-mail'),
initial=True
)
cancellation_fee = forms.DecimalField(
required=False,
max_digits=10, decimal_places=2,
localize=True,
label=_('Keep a cancellation fee of'),
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
'to a paid cancellation fee. Payment and shipping fees will be canceled as well, so include them '
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
prs = self.instance.payment_refund_sum
if prs > 0:
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
self.fields['cancellation_fee'].initial = Decimal('0.00')
self.fields['cancellation_fee'].max_value = prs
else:
del self.fields['cancellation_fee']
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee']
if val > self.instance.payment_refund_sum:
raise ValidationError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
return val
class MarkPaidForm(ConfirmPaymentForm):
amount = forms.DecimalField(
required=True,

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% trans "Cancel order" %}
{% endblock %}
@@ -17,15 +18,14 @@
{% endblocktrans %}</p>
{% endif %}
<form method="post" href="">
<form method="post" href="" class="">
{% csrf_token %}
<input type="hidden" name="status" value="c"/>
<div class="checkbox">
<label>
<input type="checkbox" name="send_email" value="on" checked="checked">
{% trans "Notify user by e-mail" %}
</label>
</div>
{% bootstrap_form_errors form %}
{% bootstrap_field form.send_email layout='' %}
{% if form.cancellation_fee %}
{% bootstrap_field form.cancellation_fee layout='' %}
{% endif %}
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"

View File

@@ -62,9 +62,10 @@ from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.filter import EventOrderFilterForm, RefundFilterForm
from pretix.control.forms.orders import (
CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm, MarkPaidForm,
OrderContactForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
OrderPositionChangeForm, OrderRefundForm, OtherOperationsForm,
CancelForm, CommentForm, ConfirmPaymentForm, ExporterForm, ExtendForm,
MarkPaidForm, OrderContactForm, OrderLocaleForm, OrderMailForm,
OrderPositionAddForm, OrderPositionChangeForm, OrderRefundForm,
OtherOperationsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin
@@ -772,7 +773,10 @@ class OrderRefundView(OrderView):
'payments': payments,
'remainder': to_refund,
'order': self.order,
'partial_amount': self.request.POST.get('start-partial_amount'),
'partial_amount': (
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
else self.request.GET.get('start-partial_amount')
),
'start_form': self.start_form
})
@@ -800,6 +804,13 @@ class OrderTransition(OrderView):
data=self.request.POST if self.request.method == "POST" else None,
)
@cached_property
def mark_canceled_form(self):
return CancelForm(
instance=self.order,
data=self.request.POST if self.request.method == "POST" else None,
)
def post(self, *args, **kwargs):
to = self.request.POST.get('status', '')
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
@@ -847,8 +858,10 @@ class OrderTransition(OrderView):
'confirmation mail.'))
else:
messages.success(self.request, _('The payment has been created successfully.'))
elif self.order.cancel_allowed() and to == 'c':
cancel_order(self.order, user=self.request.user, send_mail=self.request.POST.get("send_email") == "on")
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
cancel_order(self.order, user=self.request.user,
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
self.order.refresh_from_db()
if self.order.pending_sum < 0:
@@ -877,6 +890,7 @@ class OrderTransition(OrderView):
})
elif self.order.cancel_allowed() and to == 'c':
return render(self.request, 'pretixcontrol/order/cancel.html', {
'form': self.mark_canceled_form,
'order': self.order,
})
else:

View File

@@ -363,7 +363,7 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch)
organizer.slug, event.slug, order.code
), format='json', data={
'amount': '25.00',
'mark_refunded': False
'mark_canceled': False
})
assert resp.status_code == 400
assert resp.data == {'amount': ['Invalid refund amount, only 23.00 are available to refund.']}
@@ -372,7 +372,7 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch)
organizer.slug, event.slug, order.code
), format='json', data={
'amount': '20.00',
'mark_refunded': False
'mark_canceled': False
})
assert resp.status_code == 400
assert resp.data == {'amount': ['Partial refund not available for this payment method.']}
@@ -380,7 +380,7 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch)
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/2/refund/'.format(
organizer.slug, event.slug, order.code
), format='json', data={
'mark_refunded': False
'mark_canceled': False
})
assert resp.status_code == 400
assert resp.data == {'amount': ['Full refund not available for this payment method.']}
@@ -389,7 +389,7 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch)
organizer.slug, event.slug, order.code
), format='json', data={
'amount': '23.00',
'mark_refunded': False
'mark_canceled': False
})
assert resp.status_code == 400
assert resp.data == {'amount': ['Full refund not available for this payment method.']}
@@ -398,7 +398,7 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch)
organizer.slug, event.slug, order.code
), format='json', data={
'amount': '23.00',
'mark_refunded': False
'mark_canceled': False
})
assert resp.status_code == 400
assert resp.data == {'detail': 'Invalid state of payment.'}
@@ -431,7 +431,7 @@ def test_payment_refund_success(token_client, organizer, event, order, monkeypat
organizer.slug, event.slug, order.code, p1.local_id
), format='json', data={
'amount': '23.00',
'mark_refunded': False,
'mark_canceled': False,
})
assert resp.status_code == 200
r = order.refunds.get(local_id=resp.data['local_id'])
@@ -464,7 +464,7 @@ def test_payment_refund_unavailable(token_client, organizer, event, order, monke
organizer.slug, event.slug, order.code, p1.local_id
), format='json', data={
'amount': '23.00',
'mark_refunded': False,
'mark_canceled': False,
})
assert resp.status_code == 400
assert resp.data == {'detail': 'External error: We had trouble communicating with Stripe. Please try again and contact support if the problem persists.'}
@@ -514,7 +514,7 @@ def test_refund_process_mark_refunded(token_client, organizer, event, order):
p.create_external_refund()
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/refunds/2/process/'.format(
organizer.slug, event.slug, order.code
), format='json', data={'mark_refunded': True})
), format='json', data={'mark_canceled': True})
r = order.refunds.get(local_id=1)
assert resp.status_code == 200
assert r.state == OrderRefund.REFUND_STATE_DONE
@@ -523,7 +523,7 @@ def test_refund_process_mark_refunded(token_client, organizer, event, order):
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/refunds/2/process/'.format(
organizer.slug, event.slug, order.code
), format='json', data={'mark_refunded': True})
), format='json', data={'mark_canceled': True})
assert resp.status_code == 400
@@ -533,7 +533,7 @@ def test_refund_process_mark_pending(token_client, organizer, event, order):
p.create_external_refund()
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/refunds/2/process/'.format(
organizer.slug, event.slug, order.code
), format='json', data={'mark_refunded': False})
), format='json', data={'mark_canceled': False})
r = order.refunds.get(local_id=1)
assert resp.status_code == 200
assert r.state == OrderRefund.REFUND_STATE_DONE
@@ -955,6 +955,20 @@ def test_order_mark_canceled_pending(token_client, organizer, event, order):
assert len(djmail.outbox) == 1
@pytest.mark.django_db
def test_order_mark_canceled_pending_fee_not_allowed(token_client, organizer, event, order):
djmail.outbox = []
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/{}/mark_canceled/'.format(
organizer.slug, event.slug, order.code
), data={
'cancellation_fee': '7.00'
}
)
assert resp.status_code == 400
assert resp.data == {'detail': 'The cancellation fee cannot be higher than the payment credit of this order.'}
@pytest.mark.django_db
def test_order_mark_canceled_pending_no_email(token_client, organizer, event, order):
djmail.outbox = []
@@ -984,6 +998,25 @@ def test_order_mark_canceled_expired(token_client, organizer, event, order):
assert order.status == Order.STATUS_EXPIRED
@pytest.mark.django_db
def test_order_mark_paid_canceled_keep_fee(token_client, organizer, event, order):
order.status = Order.STATUS_PAID
order.save()
order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=order.total)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/{}/mark_canceled/'.format(
organizer.slug, event.slug, order.code
), data={
'cancellation_fee': '6.00'
}
)
assert resp.status_code == 200
assert resp.data['status'] == Order.STATUS_PAID
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
assert order.total == Decimal('6.00')
@pytest.mark.django_db
def test_order_mark_paid_refunded(token_client, organizer, event, order):
order.status = Order.STATUS_PAID
@@ -2415,7 +2448,7 @@ def test_refund_create(token_client, organizer, event, order):
@pytest.mark.django_db
def test_refund_create_mark_refunded(token_client, organizer, event, order):
res = copy.deepcopy(REFUND_CREATE_PAYLOAD)
res['mark_refunded'] = True
res['mark_canceled'] = True
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/{}/refunds/'.format(
organizer.slug, event.slug, order.code

View File

@@ -127,6 +127,19 @@ class QuotaTestCase(BaseQuotaTestCase):
self.assertEqual(quota2.availability(), (Quota.AVAILABILITY_OK, 1))
def test_position_canceled(self):
self.quota.items.add(self.item1)
self.quota.size = 3
self.quota.save()
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
op = OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
op.canceled = True
op.save()
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 3))
def test_reserved(self):
self.quota.items.add(self.item1)
self.quota.size = 3

View File

@@ -11,7 +11,7 @@ from tests.base import SoupTest
from tests.plugins.stripe.test_provider import MockedCharge
from pretix.base.models import (
Event, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
Event, InvoiceAddress, Item, Order, OrderFee, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, Team, User,
)
from pretix.base.payment import PaymentException
@@ -307,6 +307,91 @@ def test_order_cancel_free(client, env):
assert o.status == Order.STATUS_CANCELED
@pytest.mark.django_db
def test_order_cancel_paid_keep_fee(client, env):
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'))
o.event.settings.tax_rate_default = tr7
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.get()
assert f.fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f.value == Decimal('6.00')
assert f.tax_value == Decimal('0.39')
assert f.tax_rate == Decimal('7')
assert f.tax_rule == tr7
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_pending_keep_fee(client, env):
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=Decimal('8.00'))
o.status = Order.STATUS_PENDING
o.save()
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.get()
assert f.fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f.value == Decimal('6.00')
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-2.00')
@pytest.mark.django_db
def test_order_cancel_pending_fee_too_high(client, env):
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=Decimal('4.00'))
o.status = Order.STATUS_PENDING
o.save()
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
o = Order.objects.get(id=env[2].id)
assert o.positions.exists()
assert not o.fees.exists()
assert o.status == Order.STATUS_PENDING
assert o.total == Decimal('14.00')
@pytest.mark.django_db
def test_order_cancel_unpaid_no_fees_allowed(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
o = Order.objects.get(id=env[2].id)
assert o.positions.exists()
assert not o.fees.exists()
assert o.status == Order.STATUS_CANCELED
assert o.total == Decimal('14.00')
@pytest.mark.django_db
def test_order_invoice_create_forbidden(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')