Fix #163 -- Form to change orders (#191)

This commit is contained in:
Raphael Michel
2016-08-31 19:10:11 +02:00
committed by GitHub
parent 57374aec1a
commit b21ed4d99f
15 changed files with 751 additions and 16 deletions

View File

@@ -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,

View File

@@ -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.'))

View File

@@ -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 _

View File

@@ -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)

View File

@@ -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>

View 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 %}

View File

@@ -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>

View File

@@ -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'),

View File

@@ -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'