mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
@@ -1,6 +1,7 @@
|
||||
from collections import Counter, namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
@@ -12,7 +13,8 @@ from pretix.base.i18n import (
|
||||
LazyDate, LazyLocaleException, LazyNumber, language,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, EventLock, Order, OrderPosition, Quota, User,
|
||||
CartPosition, Event, EventLock, Item, ItemVariation, Order, OrderPosition,
|
||||
Quota, User,
|
||||
)
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
@@ -364,6 +366,168 @@ def expire_orders(sender, **kwargs):
|
||||
o.save()
|
||||
|
||||
|
||||
class OrderChangeManager:
|
||||
error_messages = {
|
||||
'free_to_paid': _('You cannot change a free order to a paid order.'),
|
||||
'product_without_variation': _('You need to select a variation of the product.'),
|
||||
'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.'),
|
||||
'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however '
|
||||
'no quota is available.'),
|
||||
}
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||
|
||||
def __init__(self, order: Order, user):
|
||||
self.order = order
|
||||
self.user = user
|
||||
self._totaldiff = 0
|
||||
self._quotadiff = Counter()
|
||||
self._operations = []
|
||||
|
||||
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)
|
||||
if not price:
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
self._totaldiff = price - position.price
|
||||
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())
|
||||
self._quotadiff.subtract(position.variation.quotas.all() if position.variation else position.item.quotas.all())
|
||||
self._operations.append(self.ItemOperation(position, item, variation, price))
|
||||
|
||||
def change_price(self, position: OrderPosition, price: Decimal):
|
||||
self._totaldiff = price - position.price
|
||||
self._operations.append(self.PriceOperation(position, price))
|
||||
|
||||
def cancel(self, position: OrderPosition):
|
||||
self._totaldiff = -position.price
|
||||
self._quotadiff.subtract(position.variation.quotas.all() if position.variation else position.item.quotas.all())
|
||||
self._operations.append(self.CancelOperation(position))
|
||||
|
||||
def _check_quotas(self):
|
||||
for quota, diff in self._quotadiff.items():
|
||||
if diff <= 0:
|
||||
continue
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or avail[1] < diff:
|
||||
raise OrderError(self.error_messages['quota'].format(name=quota.name))
|
||||
|
||||
def _check_free_to_paid(self):
|
||||
if self.order.total == Decimal('0.00') and self._totaldiff > 0:
|
||||
raise OrderError(self.error_messages['free_to_paid'])
|
||||
|
||||
def _check_paid_to_free(self):
|
||||
if self.order.total == 0:
|
||||
try:
|
||||
mark_order_paid(self.order, 'free', send_mail=False)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(self.error_messages['paid_to_free_exceeded'])
|
||||
|
||||
def _perform_operations(self):
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.ItemOperation):
|
||||
self.order.log_action('pretix.event.order.changed.item', user=self.user, data={
|
||||
'position': op.position.pk,
|
||||
'old_item': op.position.item.pk,
|
||||
'old_variation': op.position.variation.pk if op.position.variation else None,
|
||||
'new_item': op.item.pk,
|
||||
'new_variation': op.variation.pk if op.variation else None,
|
||||
'old_price': op.position.price,
|
||||
'new_price': op.price
|
||||
})
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position.price = op.price
|
||||
op.position._calculate_tax()
|
||||
op.position.save()
|
||||
elif isinstance(op, self.PriceOperation):
|
||||
self.order.log_action('pretix.event.order.changed.price', user=self.user, data={
|
||||
'position': op.position.pk,
|
||||
'old_price': op.position.price,
|
||||
'new_price': op.price
|
||||
})
|
||||
op.position.price = op.price
|
||||
op.position._calculate_tax()
|
||||
op.position.save()
|
||||
elif isinstance(op, self.CancelOperation):
|
||||
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={
|
||||
'position': op.position.pk,
|
||||
'old_item': op.position.item.pk,
|
||||
'old_variation': op.position.variation.pk if op.position.variation else None,
|
||||
'old_price': op.position.price,
|
||||
})
|
||||
op.position.delete()
|
||||
|
||||
def _recalculate_total_and_payment_fee(self):
|
||||
self.order.total = sum([p.price for p in self.order.positions.all()])
|
||||
if self.order.total == 0:
|
||||
payment_fee = Decimal('0.00')
|
||||
else:
|
||||
payment_fee = self._get_payment_provider().calculate_fee(self.order.total)
|
||||
self.order.payment_fee = payment_fee
|
||||
self.order.total += payment_fee
|
||||
self.order._calculate_tax()
|
||||
self.order.save()
|
||||
|
||||
def _reissue_invoice(self):
|
||||
i = self.order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
generate_invoice(self.order)
|
||||
|
||||
def _check_complete_cancel(self):
|
||||
cancels = len([o for o in self._operations if isinstance(o, self.CancelOperation)])
|
||||
if cancels == self.order.positions.count():
|
||||
raise OrderError(self.error_messages['complete_cancel'])
|
||||
|
||||
def _notify_user(self):
|
||||
with language(self.order.locale):
|
||||
mail(
|
||||
self.order.email, _('Your order has been changed: %(code)s') % {'code': self.order.code},
|
||||
self.order.event.settings.mail_text_order_changed,
|
||||
{
|
||||
'event': self.order.event.name,
|
||||
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}),
|
||||
},
|
||||
self.order.event, locale=self.order.locale
|
||||
)
|
||||
|
||||
def commit(self):
|
||||
if not self._operations:
|
||||
# Do nothing
|
||||
return
|
||||
with transaction.atomic():
|
||||
with self.order.event.lock():
|
||||
if self.order.status != Order.STATUS_PENDING:
|
||||
raise OrderError(self.error_messages['not_pending'])
|
||||
self._check_free_to_paid()
|
||||
self._check_quotas()
|
||||
self._check_complete_cancel()
|
||||
self._perform_operations()
|
||||
self._recalculate_total_and_payment_fee()
|
||||
self._reissue_invoice()
|
||||
self._check_paid_to_free()
|
||||
self._notify_user()
|
||||
|
||||
def _get_payment_provider(self):
|
||||
responses = register_payment_providers.send(self.order.event)
|
||||
pprov = None
|
||||
for rec, response in responses:
|
||||
provider = response(self.order.event)
|
||||
if provider.identifier == self.order.payment_provider:
|
||||
return provider
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
|
||||
|
||||
if settings.HAS_CELERY:
|
||||
from pretix.celery import app
|
||||
|
||||
|
||||
@@ -191,6 +191,18 @@ of {total} {currency}. Please complete your payment before {date}.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_changed': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
your order for {event} has been changed.
|
||||
|
||||
You can view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
|
||||
@@ -329,6 +329,12 @@ class MailSettingsForm(SettingsForm):
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
)
|
||||
mail_text_order_changed = I18nFormField(
|
||||
label=_("Changed order"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {url}")
|
||||
)
|
||||
mail_text_resend_link = I18nFormField(
|
||||
label=_("Resend link"),
|
||||
required=False,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models import Item, Order
|
||||
|
||||
|
||||
class ExtendForm(I18nModelForm):
|
||||
@@ -35,3 +37,54 @@ class CommentForm(I18nModelForm):
|
||||
'class': 'helper-width-100',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionChangeForm(forms.Form):
|
||||
itemvar = forms.ChoiceField()
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
label=_('New price')
|
||||
)
|
||||
operation = forms.ChoiceField(
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('product', 'Change product'),
|
||||
('price', 'Change price'),
|
||||
('cancel', 'Remove product')
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.pop('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
if instance:
|
||||
try:
|
||||
if instance.variation:
|
||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||
elif instance.item:
|
||||
initial['itemvar'] = str(instance.item.pk)
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
|
||||
initial['price'] = instance.price
|
||||
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
choices = []
|
||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||
pname = 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' % (pname, v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), pname))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get('operation') == 'price' and not self.cleaned_data.get('price', '') != '':
|
||||
raise ValidationError(_('You need to enter a price if you want to change the product price.'))
|
||||
|
||||
@@ -3,7 +3,6 @@ import copy
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms import model_to_dict
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
@@ -1,11 +1,50 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils import formats
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, ItemVariation, LogEntry
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
|
||||
def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
data = json.loads(logentry.data)
|
||||
|
||||
text = _('The order has been changed:')
|
||||
if logentry.action_type == 'pretix.event.order.changed.item':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(event.itemvariations.get(pk=data['old_variation']))
|
||||
new_item = str(event.items.get(pk=data['new_item']))
|
||||
if data['new_variation']:
|
||||
new_item += ' - ' + str(event.itemvariations.get(pk=data['new_variation']))
|
||||
return text + ' ' + _('{old_item} ({old_price} {currency}) changed to {new_item} ({new_price} {currency}).').format(
|
||||
old_item=old_item, new_item=new_item,
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
new_price=formats.localize(Decimal(data['new_price'])),
|
||||
currency=event.currency
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.price':
|
||||
return text + ' ' + _('Price of a position changed from {old_price} {currency} to {new_price} {currency}.').format(
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
new_price=formats.localize(Decimal(data['new_price'])),
|
||||
currency=event.currency
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.cancel':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
return text + ' ' + _('{old_item} ({old_price} {currency}) removed.').format(
|
||||
old_item=old_item,
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
currency=event.currency
|
||||
)
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
plains = {
|
||||
'pretix.event.order.modified': _('The order details have been modified.'),
|
||||
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
|
||||
@@ -23,3 +62,6 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
}
|
||||
if logentry.action_type in plains:
|
||||
return plains[logentry.action_type]
|
||||
|
||||
if logentry.action_type.startswith('pretix.event.order.changed'):
|
||||
return _display_order_changed(sender, logentry)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
{% bootstrap_field form.mail_text_order_paid layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_order_free layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_resend_link layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_order_changed layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "SMTP settings" %}</legend>
|
||||
|
||||
105
src/pretix/control/templates/pretixcontrol/order/change.html
Normal file
105
src/pretix/control/templates/pretixcontrol/order/change.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Change order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can use this tool to change the ordered products or to partially cancel the order. Please keep
|
||||
in mind that changing an order can have several implications, e.g. the payment method fee might change or
|
||||
additional questions can be added to the order that need to be answered by the user.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<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.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
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">
|
||||
{% 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.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
{% for position in positions %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<strong>{{ position.item.name }}</strong>
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-inline form-order-change">
|
||||
{% bootstrap_form_errors position.form %}
|
||||
{% if position.custom_error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ position.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value=""
|
||||
{% if not position.form.operation.value %}checked="checked"{% endif %}>
|
||||
{% trans "Keep unchanged" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="product"
|
||||
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}>
|
||||
{% trans "Change product to" %}
|
||||
{% bootstrap_field position.form.itemvar layout='inline' %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="price"
|
||||
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
|
||||
{% trans "Change price to" %}
|
||||
{% bootstrap_field position.form.price layout='inline' %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel"
|
||||
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}>
|
||||
{% trans "Remove from order" %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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 %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button class="btn btn-primary btn-save btn-lg" type="submit">
|
||||
{% trans "Perform changes" %}
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -148,6 +148,14 @@
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
{% if order.status == "n" 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" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="panel-title">
|
||||
{% trans "Ordered items" %}
|
||||
</h3>
|
||||
|
||||
@@ -83,6 +83,8 @@ urlpatterns = [
|
||||
name='event.order.extend'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/comment$', orders.OrderComment.as_view(),
|
||||
name='event.order.comment'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/change$', orders.OrderChange.as_view(),
|
||||
name='event.order.change'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/download/(?P<output>[^/]+)$', orders.OrderDownload.as_view(),
|
||||
name='event.order.download'),
|
||||
|
||||
@@ -13,7 +13,8 @@ from django.views.generic import DetailView, ListView, TemplateView, View
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CachedTicket, EventLock, Invoice, Item, Order, Quota,
|
||||
CachedFile, CachedTicket, EventLock, Invoice, Item, ItemVariation, Order,
|
||||
Quota,
|
||||
)
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.export import export
|
||||
@@ -22,13 +23,17 @@ from pretix.base.services.invoices import (
|
||||
regenerate_invoice,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.orders import cancel_order, mark_order_paid
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, cancel_order, mark_order_paid,
|
||||
)
|
||||
from pretix.base.services.stats import order_overview
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_payment_providers,
|
||||
register_ticket_outputs,
|
||||
)
|
||||
from pretix.control.forms.orders import CommentForm, ExporterForm, ExtendForm
|
||||
from pretix.control.forms.orders import (
|
||||
CommentForm, ExporterForm, ExtendForm, OrderPositionChangeForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -77,6 +82,12 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
|
||||
code=self.kwargs['code'].upper()
|
||||
)
|
||||
|
||||
def _redirect_back(self):
|
||||
return redirect('control:event.order',
|
||||
event=self.request.event.slug,
|
||||
organizer=self.request.event.organizer.slug,
|
||||
code=self.order.code)
|
||||
|
||||
@cached_property
|
||||
def order(self):
|
||||
return self.get_object()
|
||||
@@ -441,12 +452,6 @@ class OrderExtend(OrderView):
|
||||
else:
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def _redirect_back(self):
|
||||
return redirect('control:event.order',
|
||||
event=self.request.event.slug,
|
||||
organizer=self.request.event.organizer.slug,
|
||||
code=self.order.code)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
if self.order.status != Order.STATUS_PENDING:
|
||||
messages.error(self.request, _('This action is only allowed for pending orders.'))
|
||||
@@ -462,6 +467,75 @@ class OrderExtend(OrderView):
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
|
||||
class OrderChange(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
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.'))
|
||||
return self._redirect_back()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
positions = list(self.order.positions.all())
|
||||
for p in positions:
|
||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p,
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
return positions
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['positions'] = self.positions
|
||||
return ctx
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
ocm = OrderChangeManager(self.order, self.request.user)
|
||||
form_valid = True
|
||||
for p in self.positions:
|
||||
if not p.form.is_valid():
|
||||
print(p.pk, 'Form invalid')
|
||||
form_valid = False
|
||||
break
|
||||
|
||||
try:
|
||||
if p.form.cleaned_data['operation'] == 'product':
|
||||
if '-' in p.form.cleaned_data['itemvar']:
|
||||
itemid, varid = p.form.cleaned_data['itemvar'].split('-')
|
||||
else:
|
||||
itemid, varid = p.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
|
||||
ocm.change_item(p, item, variation)
|
||||
elif p.form.cleaned_data['operation'] == 'price':
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
elif p.form.cleaned_data['operation'] == 'cancel':
|
||||
ocm.cancel(p)
|
||||
|
||||
except OrderError as e:
|
||||
p.custom_error = str(e)
|
||||
form_valid = False
|
||||
break
|
||||
|
||||
if not form_valid:
|
||||
messages.error(self.request, _('An error occured. Please see the details below.'))
|
||||
else:
|
||||
try:
|
||||
ocm.commit()
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been changed and the user has been notified.'))
|
||||
return self._redirect_back()
|
||||
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
|
||||
class OverView(EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/orders/overview.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
Reference in New Issue
Block a user