Refs #340 -- Allow order changes for paid orders if they don't change the total

This commit is contained in:
Raphael Michel
2017-02-15 18:42:46 +01:00
parent 0db927407d
commit c4bf73c8d6
7 changed files with 49 additions and 10 deletions

View File

@@ -309,6 +309,10 @@ class ItemVariation(models.Model):
def __str__(self):
return str(self.value)
@property
def price(self):
return self.default_price if self.default_price is not None else self.item.default_price
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.item:

View File

@@ -190,6 +190,10 @@ class Order(LoggedModel):
"""
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
@property
def changable(self):
return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
def save(self, *args, **kwargs):
if not self.code:
self.assign_code()

View File

@@ -480,9 +480,11 @@ class OrderChangeManager:
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'),
'product_invalid': _('The selected product is not active or has no price set.'),
'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'),
'not_pending': _('Only pending orders can be changed.'),
'not_pending_or_paid': _('Only pending or paid orders can be changed.'),
'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however '
'no quota is available.'),
'paid_price_change': _('Currently, paid orders can only be changed in a way that does not change the total '
'price of the order as partial payments or refunds are not yet supported.')
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
@@ -498,8 +500,7 @@ class OrderChangeManager:
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = item.default_price if variation is None else (
variation.default_price if variation.default_price is not None else item.default_price)
price = item.default_price if variation is None else variation.price
if not price:
raise OrderError(self.error_messages['product_invalid'])
self._totaldiff = price - position.price
@@ -528,6 +529,10 @@ class OrderChangeManager:
if self.order.total == Decimal('0.00') and self._totaldiff > 0:
raise OrderError(self.error_messages['free_to_paid'])
def _check_paid_price_change(self):
if self.order.status == Order.STATUS_PAID and self._totaldiff != 0:
raise OrderError(self.error_messages['paid_price_change'])
def _check_paid_to_free(self):
if self.order.total == 0:
try:
@@ -624,9 +629,10 @@ class OrderChangeManager:
return
with transaction.atomic():
with self.order.event.lock():
if self.order.status != Order.STATUS_PENDING:
raise OrderError(self.error_messages['not_pending'])
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
raise OrderError(self.error_messages['not_pending_or_paid'])
self._check_free_to_paid()
self._check_paid_price_change()
self._check_quotas()
self._check_complete_cancel()
self._perform_operations()

View File

@@ -1,6 +1,7 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
@@ -94,9 +95,12 @@ class OrderPositionChangeForm(forms.Form):
variations = list(i.variations.all())
if variations:
for v in variations:
choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (pname, v.value)))
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s %s)' % (pname, v.value, localize(v.price),
instance.order.event.currency)))
else:
choices.append((str(i.pk), pname))
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price),
instance.order.event.currency)))
self.fields['itemvar'].choices = choices
def clean(self):

View File

@@ -144,7 +144,7 @@
<div class="panel panel-default items">
<div class="panel-heading">
<div class="pull-right">
{% if order.status == "n" and request.eventperm.can_change_orders %}
{% if order.changable and request.eventperm.can_change_orders %}
<a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change products" %}

View File

@@ -446,8 +446,8 @@ class OrderChange(OrderView):
template_name = 'pretixcontrol/order/change.html'
def dispatch(self, request, *args, **kwargs):
if self.order.status != Order.STATUS_PENDING:
messages.error(self.request, _('This action is only allowed for pending orders.'))
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
messages.error(self.request, _('This action is only allowed for pending or paid orders.'))
return self._redirect_back()
return super().dispatch(request, *args, **kwargs)

View File

@@ -140,6 +140,8 @@ class OrderChangeManagerTests(TestCase):
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'),
default_price=Decimal('23.00'), admission=True)
self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rate=Decimal('7.00'),
default_price=Decimal('23.00'), admission=True)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'),
default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create(
@@ -303,3 +305,22 @@ class OrderChangeManagerTests(TestCase):
assert self.order.total == 0
assert self.order.status == Order.STATUS_PAID
assert self.order.payment_provider == 'free'
def test_change_paid_same_price(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.ocm.change_item(self.op1, self.ticket2, None)
self.ocm.commit()
self.order.refresh_from_db()
assert self.order.total == 46
assert self.order.status == Order.STATUS_PAID
def test_change_paid_different_price(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.ocm.change_price(self.op1, Decimal('5.00'))
with self.assertRaises(OrderError):
self.ocm.commit()
self.order.refresh_from_db()
assert self.order.total == 46
assert self.order.status == Order.STATUS_PAID