forked from CGM_Public/pretix_original
Allow to add positions to an existing order
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()])
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 %}">
|
||||
|
||||
@@ -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.'))
|
||||
|
||||
Reference in New Issue
Block a user