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): def __str__(self):
return str(self.value) 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): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
if self.item: if self.item:

View File

@@ -190,6 +190,10 @@ class Order(LoggedModel):
""" """
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code) 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): def save(self, *args, **kwargs):
if not self.code: if not self.code:
self.assign_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.'), '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.'), '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.'), '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 ' 'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however '
'no quota is available.'), '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')) ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price')) PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
@@ -498,8 +500,7 @@ class OrderChangeManager:
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): 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): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation']) raise OrderError(self.error_messages['product_without_variation'])
price = item.default_price if variation is None else ( price = item.default_price if variation is None else variation.price
variation.default_price if variation.default_price is not None else item.default_price)
if not price: if not price:
raise OrderError(self.error_messages['product_invalid']) raise OrderError(self.error_messages['product_invalid'])
self._totaldiff = price - position.price self._totaldiff = price - position.price
@@ -528,6 +529,10 @@ class OrderChangeManager:
if self.order.total == Decimal('0.00') and self._totaldiff > 0: if self.order.total == Decimal('0.00') and self._totaldiff > 0:
raise OrderError(self.error_messages['free_to_paid']) 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): def _check_paid_to_free(self):
if self.order.total == 0: if self.order.total == 0:
try: try:
@@ -624,9 +629,10 @@ class OrderChangeManager:
return return
with transaction.atomic(): with transaction.atomic():
with self.order.event.lock(): with self.order.event.lock():
if self.order.status != Order.STATUS_PENDING: if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
raise OrderError(self.error_messages['not_pending']) raise OrderError(self.error_messages['not_pending_or_paid'])
self._check_free_to_paid() self._check_free_to_paid()
self._check_paid_price_change()
self._check_quotas() self._check_quotas()
self._check_complete_cancel() self._check_complete_cancel()
self._perform_operations() self._perform_operations()

View File

@@ -1,6 +1,7 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.formats import localize
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -94,9 +95,12 @@ class OrderPositionChangeForm(forms.Form):
variations = list(i.variations.all()) variations = list(i.variations.all())
if variations: if variations:
for v in 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: 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 self.fields['itemvar'].choices = choices
def clean(self): def clean(self):

View File

@@ -144,7 +144,7 @@
<div class="panel panel-default items"> <div class="panel panel-default items">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right"> <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 %}"> <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> <span class="fa fa-edit"></span>
{% trans "Change products" %} {% trans "Change products" %}

View File

@@ -446,8 +446,8 @@ class OrderChange(OrderView):
template_name = 'pretixcontrol/order/change.html' template_name = 'pretixcontrol/order/change.html'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if self.order.status != Order.STATUS_PENDING: if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
messages.error(self.request, _('This action is only allowed for pending orders.')) messages.error(self.request, _('This action is only allowed for pending or paid orders.'))
return self._redirect_back() return self._redirect_back()
return super().dispatch(request, *args, **kwargs) 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'), self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'),
default_price=Decimal('23.00'), admission=True) 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'), self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'),
default_price=Decimal('12.00')) default_price=Decimal('12.00'))
self.op1 = OrderPosition.objects.create( self.op1 = OrderPosition.objects.create(
@@ -303,3 +305,22 @@ class OrderChangeManagerTests(TestCase):
assert self.order.total == 0 assert self.order.total == 0
assert self.order.status == Order.STATUS_PAID assert self.order.status == Order.STATUS_PAID
assert self.order.payment_provider == 'free' 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