diff --git a/src/pretix/base/migrations/0022_auto_20150320_2239.py b/src/pretix/base/migrations/0022_auto_20150320_2239.py new file mode 100644 index 0000000000..2acc92a0a7 --- /dev/null +++ b/src/pretix/base/migrations/0022_auto_20150320_2239.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0021_auto_20150320_1622'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='payment_manual', + field=models.BooleanField(verbose_name='Payment state was manually modified', default=False), + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.CharField(verbose_name='Status', max_length=3, choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')]), + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 9117f49847..5de71c9c51 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1537,6 +1537,10 @@ class Order(Versionable): verbose_name=_("Payment information"), null=True, blank=True ) + payment_manual = models.BooleanField( + verbose_name=_("Payment state was manually modified"), + default=False + ) total = models.DecimalField( decimal_places=2, max_digits=10, verbose_name=_("Total amount") @@ -1590,7 +1594,7 @@ class Order(Versionable): return True return False # nothing there to modify - def mark_paid(self, provider, info, date=None): + def mark_paid(self, provider=None, info=None, date=None, manual=None): """ Mark this order as paid. This clones the order object, sets the payment provider, info and date and returns the cloned order object. @@ -1604,9 +1608,11 @@ class Order(Versionable): :type date: datetime """ order = self.clone() - order.payment_provider = provider - order.payment_info = info + order.payment_provider = provider or order.payment_provider + order.payment_info = info or order.payment_info order.payment_date = date or now() + if manual is not None: + order.payment_manual = manual order.status = Order.STATUS_PAID order.save() return order diff --git a/src/pretix/control/static/pretixcontrol/less/main.less b/src/pretix/control/static/pretixcontrol/less/main.less index 52417816f9..4a8d618bd7 100644 --- a/src/pretix/control/static/pretixcontrol/less/main.less +++ b/src/pretix/control/static/pretixcontrol/less/main.less @@ -47,3 +47,10 @@ nav.navbar { } } } + +.btn-toolbar { + margin-bottom: 20px; +} +.container-fluid > .alert:first-child { + margin-top: 20px; +} \ No newline at end of file diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index 4c9f876a21..8884592f22 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -74,6 +74,13 @@
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} {% block content %} {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/order/cancel.html b/src/pretix/control/templates/pretixcontrol/order/cancel.html new file mode 100644 index 0000000000..4d29b40fe7 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/order/cancel.html @@ -0,0 +1,32 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% block title %} + {% trans "Cancel order" %} +{% endblock %} +{% block content %} +

+ {% trans "Cancel order" %} +

+

{% blocktrans trimmed %} + Do you really want to cancel this order? You cannot revert this action. + {% endblocktrans %}

+ +
+ {% csrf_token %} + +
+ +
+ +
+
+
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index ffaead64bc..f6a2841fdc 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -12,6 +12,27 @@ {% endblocktrans %} {% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right" %} + {% if order.status == 'n' or order.status == 'p' %} +
+ {% csrf_token %} + +
+ {% endif %}

@@ -86,6 +107,11 @@

+ {% if order.payment_manual %} +
+ {% trans "The payment state of this order was manually modified." %} +
+ {% endif %} {{ payment }} {% if order.status == 'n' %}

{% blocktrans trimmed with date=order.expires %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index c12abb346f..35bc0eff80 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -46,6 +46,8 @@ urlpatterns = [ url(r'^quotas/(?P[0-9a-f-]+)/delete$', item.QuotaDelete.as_view(), name='event.items.quotas.delete'), url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'), + url(r'^orders/(?P[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(), + name='event.order.transition'), url(r'^orders/(?P[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'), url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'), ])), diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index c04b37d39e..b6afbd8ae5 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1,5 +1,10 @@ from itertools import groupby +from django.contrib import messages +from django.core.urlresolvers import reverse from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponse +from django.shortcuts import redirect, render from django.utils.functional import cached_property from django.views.generic import ListView, DetailView @@ -21,11 +26,9 @@ class OrderList(EventPermissionRequiredMixin, ListView): ).select_related("user") -class OrderDetail(EventPermissionRequiredMixin, DetailView): - model = Order +class OrderView(DetailView): context_object_name = 'order' - template_name = 'pretixcontrol/order/index.html' - permission = 'can_view_orders' + model = Order def get_object(self, queryset=None): return Order.objects.current.get( @@ -33,6 +36,15 @@ class OrderDetail(EventPermissionRequiredMixin, DetailView): code=self.kwargs['code'].upper() ) + @cached_property + def order(self): + return self.get_object() + + +class OrderDetail(EventPermissionRequiredMixin, OrderView): + template_name = 'pretixcontrol/order/index.html' + permission = 'can_view_orders' + @cached_property def payment_provider(self): responses = register_payment_providers.send(self.request.event) @@ -85,3 +97,51 @@ class OrderDetail(EventPermissionRequiredMixin, DetailView): 'total': self.object.total, 'payment_fee': self.object.payment_fee, } + + +class OrderTransition(EventPermissionRequiredMixin, OrderView): + permission = 'can_view_orders' + + def post(self, *args, **kwargs): + to = self.request.POST.get('status', '') + if self.order.status == 'n' and to == 'p': + self.order.mark_paid(manual=True) + messages.success(self.request, _('The order has been marked as paid.')) + elif self.order.status == 'n' and to == 'c': + order = self.order.clone() + order.status = Order.STATUS_CANCELLED + order.save() + messages.success(self.request, _('The order has been cancelled.')) + elif self.order.status == 'p' and to == 'n': + order = self.order.clone() + order.status = Order.STATUS_PENDING + order.payment_manual = True + order.save() + messages.success(self.request, _('The order has been marked as not paid.')) + return redirect(reverse( + 'control:event.order', + kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + 'code': self.order.code, + } + )) + + def get(self, *args, **kwargs): + to = self.request.GET.get('status', '') + if self.order.status == 'n' and to == 'c': + return render(self.request, 'pretixcontrol/order/cancel.html', { + 'order': self.order, + }) + elif self.order.status == 'p' and to == 'r': + messages.error(self.request, _('Refunding orders is not yet implemented.')) + return redirect(reverse( + 'control:event.order', + kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + 'code': self.order.code, + } + )) + else: + return HttpResponse(status=405)