mirror of
https://github.com/pretix/pretix.git
synced 2026-02-02 02:02:27 +00:00
Refs #340 -- Allow order changes for paid orders if they don't change the total
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user