Allow to add positions to an existing order

This commit is contained in:
Raphael Michel
2017-06-19 15:22:57 +02:00
parent 3ada10c3f4
commit 123d2f6120
9 changed files with 262 additions and 23 deletions

View File

@@ -498,6 +498,10 @@ class OrderPosition(AbstractPosition):
verbose_name_plural = _("Order positions")
ordering = ("positionid", "id")
@cached_property
def sort_key(self):
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0
@classmethod
def transform_cart_positions(cls, cp: List, order) -> list:
from . import Voucher
@@ -529,6 +533,13 @@ class OrderPosition(AbstractPosition):
cartpos.delete()
return ops
def __str__(self):
if self.variation:
return '#{} {} {}'.format(
self.positionid, str(self.item), str(self.variation)
)
return '#{} {}'.format(self.positionid, str(self.item))
def __repr__(self):
return '<OrderPosition: item %d, variation %d for order %s>' % (
self.item.id, self.variation.id if self.variation else 0, self.order_id

View File

@@ -67,7 +67,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.save()
invoice.lines.all().delete()
for p in invoice.order.positions.all():
positions = list(invoice.order.positions.select_related('addon_to', 'item', 'variation'))
positions.sort(key=lambda p: p.sort_key)
for p in positions:
desc = str(p.item.name)
if p.variation:
desc += " - " + str(p.variation.value)

View File

@@ -9,7 +9,7 @@ import pytz
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
from django.db.models import F, Q
from django.db.models import F, Max, Q
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
@@ -495,11 +495,14 @@ class OrderChangeManager:
'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.')
'price of the order as partial payments or refunds are not yet supported.'),
'addon_to_required': _('This is an addon product, please select the base position it should be added to.'),
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
CancelOperation = namedtuple('CancelOperation', ('position',))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to'))
def __init__(self, order: Order, user):
self.order = order
@@ -528,6 +531,21 @@ class OrderChangeManager:
self._quotadiff.subtract(position.variation.quotas.all() if position.variation else position.item.quotas.all())
self._operations.append(self.CancelOperation(position))
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order):
if price is None:
price = item.default_price if variation is None else variation.price
if price is None:
raise OrderError(self.error_messages['product_invalid'])
if not addon_to and item.category and item.category.is_addon:
raise OrderError(self.error_messages['addon_to_required'])
if addon_to:
if not item.category or item.category_id not in addon_to.item.addons.values_list('addon_category', flat=True):
raise OrderError(self.error_messages['addon_invalid'])
self._totaldiff = price
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())
self._operations.append(self.AddOperation(item, variation, price, addon_to))
def _check_quotas(self):
for quota, diff in self._quotadiff.items():
if diff <= 0:
@@ -552,6 +570,7 @@ class OrderChangeManager:
raise OrderError(self.error_messages['paid_to_free_exceeded'])
def _perform_operations(self):
nextposid = self.order.positions.aggregate(m=Max('positionid'))['m'] + 1
for op in self._operations:
if isinstance(op, self.ItemOperation):
self.order.log_action('pretix.event.order.changed.item', user=self.user, data={
@@ -600,6 +619,21 @@ class OrderChangeManager:
'addon_to': None,
})
op.position.delete()
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price, order=self.order,
positionid=nextposid
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, data={
'position': pos.pk,
'item': op.item.pk,
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price,
'positionid': pos.positionid
})
def _recalculate_total_and_payment_fee(self):
self.order.total = sum([p.price for p in self.order.positions.all()])

View File

@@ -7,7 +7,7 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, Order
from pretix.base.models import Item, ItemAddOn, Order, OrderPosition
class ExtendForm(I18nModelForm):
@@ -55,6 +55,52 @@ class CommentForm(I18nModelForm):
}
class OrderPositionAddForm(forms.Form):
do = forms.BooleanField(
label=_('Add a new product to the order'),
required=False
)
itemvar = forms.ChoiceField(
label=_('Product')
)
addon_to = forms.ModelChoiceField(
OrderPosition.objects.none(),
required=False,
label=_('Add-on to'),
)
price = forms.DecimalField(
required=False,
max_digits=10, decimal_places=2,
label=_('Gross price'),
help_text=_("Keep empty for the product's default price")
)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
choices = []
for i in order.event.items.prefetch_related('variations').all():
pname = str(i.name)
if not i.is_available():
pname += ' ({})'.format(_('inactive'))
variations = list(i.variations.all())
if variations:
for v in variations:
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s %s)' % (pname, v.value, localize(v.price),
order.event.currency)))
else:
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price),
order.event.currency)))
self.fields['itemvar'].choices = choices
if ItemAddOn.objects.filter(base_item__event=order.event).exists():
self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related(
'item', 'variation'
)
else:
del self.fields['addon_to']
class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField()
price = forms.DecimalField(

View File

@@ -6,7 +6,7 @@ from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretix.base.models import Event, ItemVariation, LogEntry
from pretix.base.models import Event, ItemVariation, LogEntry, OrderPosition
from pretix.base.signals import logentry_display
OVERVIEW_BLACKLIST = [
@@ -51,6 +51,26 @@ def _display_order_changed(event: Event, logentry: LogEntry):
old_price=formats.localize(Decimal(data['old_price'])),
currency=event.currency
)
elif logentry.action_type == 'pretix.event.order.changed.add':
item = str(event.items.get(pk=data['item']))
if data['variation']:
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
if data['addon_to']:
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}) as an add-on to '
'position #{addon_to}.').format(
posid=data.get('positionid', '?'),
item=item, addon_to=addon_to.positionid,
price=formats.localize(Decimal(data['price'])),
currency=event.currency
)
else:
return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}).').format(
posid=data.get('positionid', '?'),
item=item,
price=formats.localize(Decimal(data['price'])),
currency=event.currency
)
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")

View File

@@ -22,8 +22,7 @@
<p>
{% blocktrans trimmed %}
The user will receive a notification about the change but in the case of new required questions, the user
will not be forced to answer them. You cannot use this form to add something to the order, please create
a second order instead.
will not be forced to answer them.
{% endblocktrans %}
</p>
<p>
@@ -31,13 +30,15 @@
If an invoice is attached to the order, a cancellation will be created together with a new invoice.
{% endblocktrans %}
</p>
<div class="alert alert-warning">
<div class="alert alert-warning"><strong>
{% blocktrans trimmed %}
Please use this tool carefully. Changes you make here are not reversible. In most cases it is easier to
cancel the order completely and create a new one.
Please use this tool carefully. Changes you make here are not reversible. Also, if you change an order
manually, not all constraints (e.g. on required add-ons) will be checked. Therefore, you might construct
an order that would not be able to exist otherwise.
In most cases it is easier to cancel the order completely and create a new one.
{% endblocktrans %}
</div>
<form method="post" class="form-horizontal" href="">
</strong></div>
<form method="post" href="">
{% csrf_token %}
{% for position in positions %}
<div class="panel panel-default items">
@@ -107,6 +108,29 @@
</div>
</div>
{% endfor %}
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
Add product
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_form_errors add_form %}
{% if add_form.custom_error %}
<div class="alert alert-danger">
{{ add_form.custom_error }}
</div>
{% endif %}
{% bootstrap_field add_form.do layout='horizontal' %}
{% bootstrap_field add_form.itemvar layout='horizontal' %}
{% bootstrap_field add_form.price layout='horizontal' %}
{% if add_form.addon_to %}
{% bootstrap_field add_form.addon_to layout='horizontal' %}
{% endif %}
</div>
</div>
</div>
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">

View File

@@ -33,7 +33,7 @@ from pretix.base.signals import (
from pretix.base.views.async import AsyncAction
from pretix.control.forms.orders import (
CommentForm, ExporterForm, ExtendForm, OrderContactForm, OrderLocaleForm,
OrderPositionChangeForm,
OrderPositionAddForm, OrderPositionChangeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -166,7 +166,7 @@ class OrderDetail(OrderView):
cartpos = queryset.order_by(
'item', 'variation'
).select_related(
'item', 'variation'
'item', 'variation', 'addon_to'
).prefetch_related(
'item__questions', 'answers', 'answers__question', 'checkins'
).order_by('positionid')
@@ -181,6 +181,8 @@ class OrderDetail(OrderView):
p.cache_answers()
positions.append(p)
positions.sort(key=lambda p: p.sort_key)
return {
'positions': positions,
'raw': cartpos,
@@ -460,6 +462,11 @@ class OrderChange(OrderView):
return self._redirect_back()
return super().dispatch(request, *args, **kwargs)
@cached_property
def add_form(self):
return OrderPositionAddForm(prefix='add', order=self.order,
data=self.request.POST if self.request.method == "POST" else None)
@cached_property
def positions(self):
positions = list(self.order.positions.all())
@@ -471,15 +478,37 @@ class OrderChange(OrderView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['positions'] = self.positions
ctx['add_form'] = self.add_form
return ctx
def post(self, *args, **kwargs):
ocm = OrderChangeManager(self.order, self.request.user)
form_valid = True
def _process_add(self, ocm):
if not self.add_form.is_valid():
return False
else:
if self.add_form.cleaned_data['do']:
if '-' in self.add_form.cleaned_data['itemvar']:
itemid, varid = self.add_form.cleaned_data['itemvar'].split('-')
else:
itemid, varid = self.add_form.cleaned_data['itemvar'], None
item = Item.objects.get(pk=itemid, event=self.request.event)
if varid:
variation = ItemVariation.objects.get(pk=varid, item=item)
else:
variation = None
try:
ocm.add_position(item, variation,
self.add_form.cleaned_data['price'],
self.add_form.cleaned_data['addon_to'])
except OrderError as e:
self.add_form.custom_error = str(e)
return False
return True
def _process_change(self, ocm):
for p in self.positions:
if not p.form.is_valid():
form_valid = False
break
return False
try:
if p.form.cleaned_data['operation'] == 'product':
@@ -501,8 +530,12 @@ class OrderChange(OrderView):
except OrderError as e:
p.custom_error = str(e)
form_valid = False
break
return False
return True
def post(self, *args, **kwargs):
ocm = OrderChangeManager(self.order, self.request.user)
form_valid = self._process_add(ocm) and self._process_change(ocm)
if not form_valid:
messages.error(self.request, _('An error occured. Please see the details below.'))