diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py
index 899d1193f..5d3deb22d 100644
--- a/src/pretix/base/models.py
+++ b/src/pretix/base/models.py
@@ -714,7 +714,7 @@ class Item(Versionable):
if self.event:
self.event.get_cache().clear()
- def delete(self):
+ def delete(self, *args, **kwargs):
self.deleted = True
self.active = False
super().save()
@@ -1229,11 +1229,13 @@ class Order(Versionable):
STATUS_PAID = "p"
STATUS_EXPIRED = "e"
STATUS_CANCELLED = "c"
+ STATUS_REFUNDED = "r"
STATUS_CHOICE = (
(STATUS_PAID, _("pending")),
(STATUS_PENDING, _("paid")),
(STATUS_EXPIRED, _("expired")),
(STATUS_CANCELLED, _("cancelled")),
+ (STATUS_REFUNDED, _("refunded"))
)
code = models.CharField(
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 877a7fc29..c166de11c 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -150,3 +150,28 @@ class BasePaymentProvider:
:param order: The order object
"""
return None
+
+ def order_pending_render(self, request, order) -> str:
+ """
+ Will be called if the user views the detail page of an unpaid order which is
+ associated with this payment provider.
+
+ It should return HTML code which should be displayed to the user. It should contian
+ instructions on how to continue with the payment process, either in form of text
+ or buttons/links/etc.
+
+ :param order: The order object
+ """
+ raise NotImplementedError()
+
+ def order_paid_render(self, request, order) -> str:
+ """
+ Will be called if the user views the detail page of an paid order which is
+ associated with this payment provider.
+
+ It should return HTML code which should be displayed to the user or None,
+ if there is nothing to say.
+
+ :param order: The order object
+ """
+ return None
diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py
index a2063f57d..8b57a0a3a 100644
--- a/src/pretix/plugins/banktransfer/payment.py
+++ b/src/pretix/plugins/banktransfer/payment.py
@@ -35,3 +35,8 @@ class BankTransfer(BasePaymentProvider):
template = get_template('pretixplugins/banktransfer/checkout_payment_confirm.html')
ctx = Context({'request': request, 'form': form, 'settings': self.settings})
return template.render(ctx)
+
+ def order_pending_render(self, request, order) -> str:
+ template = get_template('pretixplugins/banktransfer/pending.html')
+ ctx = Context({'request': request, 'order': order, 'settings': self.settings})
+ return template.render(ctx)
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html
new file mode 100644
index 000000000..a8fef7870
--- /dev/null
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+
+
{% blocktrans trimmed %}
+ Please transfer the full amount to the following bank account:
+{% endblocktrans %}
+
+
+ {{ settings.bank_details|linebreaksbr }}
+ {% trans "Reference code (important):" %} {{ order.full_code }}
+
\ No newline at end of file
diff --git a/src/pretix/presale/static/pretixpresale/less/event.less b/src/pretix/presale/static/pretixpresale/less/event.less
index 19f97f1f3..8a76ffa8c 100644
--- a/src/pretix/presale/static/pretixpresale/less/event.less
+++ b/src/pretix/presale/static/pretixpresale/less/event.less
@@ -25,6 +25,11 @@
}
}
}
+
+.panel-body address:last-child {
+ margin-bottom: 0;
+}
+
.cart-row, .product-row {
padding: 10px 0;
@@ -43,6 +48,15 @@
&.total {
border-top: 1px solid @table-border-color;
}
+
+ dl {
+ padding-left: 20px;
+ margin-bottom: 0;
+
+ dd {
+ padding-left: 20px;
+ }
+ }
}
.panel-primary .panel-heading a {
color: white;
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
index 7715ff46a..4fa0fc5fc 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
@@ -1,44 +1,57 @@
{% load i18n %}
{% for line in cart.positions %}
-
+
{{ line.item }}
{% if line.variation %}
– {{ line.variation }}
{% endif %}
-
-
- {{ event.currency }} {{ line.price|floatformat:2 }}
-
-
- {% if editable %}
-
+
+ {{ event.currency }} {{ line.price|floatformat:2 }}
+
+
+ {% if editable %}
+
+ {% endif %}
+ {{ line.count }}
+ {% if editable %}
+
{% endif %}
-
-
- {% endif %}
- {{ line.count }}
- {% if editable %}
-
{% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html
new file mode 100644
index 000000000..9cf2b4395
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/order.html
@@ -0,0 +1,69 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}{% trans "Order details" %}{% endblock %}
+{% block content %}
+
+ {% blocktrans trimmed with code=order.code %}
+ Your order: {{ code }}
+ {% endblocktrans %}
+ {% if order.status == "n" %}
+ {% trans "Payment pending" %}
+ {% elif order.status == "p" %}
+ {% trans "Paid" %}
+ {% elif order.status == "e" %}
+ {% trans "Payment pending" %}
+ {% elif order.status == "c" %}
+ {% trans "Cancelled" %}
+ {% elif order.status == "r" %}
+ {% trans "Refunded" %}
+ {% endif %}
+
+ {% if order.status == "n" %}
+
+
+
+ {% trans "Payment" %}
+
+
+
+ {{ payment }}
+ {% blocktrans trimmed with date=order.expires|date %}
+ Please complete your payment before {{ date }}
+ {% endblocktrans %}
+
+
+ {% endif %}
+
+
+
+ {% trans "Ordered items" %}
+
+
+
+ {% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
+
+
+ {% if order.status == "n" %}
+
+ {% endif %}
+ {% if order.status == "p" and payment %}
+
+
+
+ {% trans "Payment" %}
+
+
+
+ {{ payment }}
+
+
+ {% endif %}
+{% endblock %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 99de7e299..45c29ac27 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -3,6 +3,7 @@ from django.conf.urls import patterns, url, include
import pretix.presale.views.event
import pretix.presale.views.cart
import pretix.presale.views.checkout
+import pretix.presale.views.order
urlpatterns = patterns(
@@ -18,7 +19,7 @@ urlpatterns = patterns(
name='event.checkout.payment'),
url(r'^checkout/confirm$', pretix.presale.views.checkout.OrderConfirm.as_view(),
name='event.checkout.confirm'),
- url(r'^order/(?P
[^/]+)/$', pretix.presale.views.checkout.OrderConfirm.as_view(),
+ url(r'^order/(?P[^/]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
name='event.order'),
url(r'^login$', pretix.presale.views.event.EventLogin.as_view(), name='event.checkout.login'),
)
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
index 6beaa45df..39bea6948 100644
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -52,22 +52,33 @@ class CartDisplayMixin:
'item__questions', 'answers'
))
- def get_cart(self):
- cartpos = CartPosition.objects.current.filter(
- Q(user=self.request.user) & Q(event=self.request.event)
- ).order_by(
+ def get_cart(self, answers=False, queryset=None, payment_fee=None):
+ if queryset is None:
+ queryset = CartPosition.objects.current.filter(
+ Q(user=self.request.user) & Q(event=self.request.event)
+ )
+
+ prefetch = ['variation__values', 'variation__values__prop']
+ if answers:
+ prefetch.append('item__questions')
+ prefetch.append('answers')
+
+ cartpos = queryset.order_by(
'item', 'variation'
).select_related(
'item', 'variation'
).prefetch_related(
- 'variation__values', 'variation__values__prop'
+ *prefetch
)
# Group items of the same variation
# We do this by list manipulations instead of a GROUP BY query, as
# Django is unable to join related models in a .values() query
def keyfunc(pos):
- return pos.item_id, pos.variation_id, pos.price
+ if answers and ((pos.item.admission and self.request.event.settings.attendee_names_asked == 'True')
+ or pos.item.questions.all()):
+ return pos.id, "", "", ""
+ return "", pos.item_id, pos.variation_id, pos.price
positions = []
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
@@ -75,27 +86,42 @@ class CartDisplayMixin:
group = g[0]
group.count = len(g)
group.total = group.count * group.price
+ group.has_questions = answers and k[0] != ""
+ if answers:
+ group.answ = {}
+ for a in group.answers.all():
+ group.answ[a.question_id] = a.answer
+ group.questions = []
+ for q in group.item.questions.all():
+ if q.identity in group.answ:
+ q.answer = group.answ[q.identity]
+ else:
+ q.answer = ""
+ group.questions.append(q)
positions.append(group)
total = sum(p.total for p in positions)
- payment_fee = 0
- if 'payment' in self.request.session:
- responses = register_payment_providers.send(self.request.event)
- for receiver, response in responses:
- provider = response(self.request.event)
- if provider.identifier == self.request.session['payment']:
- payment_fee = provider.calculate_fee(total)
+ if payment_fee is None:
+ payment_fee = 0
+ if 'payment' in self.request.session:
+ responses = register_payment_providers.send(self.request.event)
+ for receiver, response in responses:
+ provider = response(self.request.event)
+ if provider.identifier == self.request.session['payment']:
+ payment_fee = provider.calculate_fee(total)
+ try:
+ minutes_left = max(min(p.expires for p in positions) - now(), timedelta()).seconds // 60 if positions else 0
+ except AttributeError:
+ minutes_left = None
return {
'positions': positions,
'raw': cartpos,
'total': total + payment_fee,
'payment_fee': payment_fee,
- 'minutes_left': (
- max(min(p.expires for p in positions) - now(), timedelta()).seconds // 60
- if positions else 0
- ),
+ 'answers': answers,
+ 'minutes_left': minutes_left,
}
diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py
index 31633cd05..a13344b74 100644
--- a/src/pretix/presale/views/checkout.py
+++ b/src/pretix/presale/views/checkout.py
@@ -251,7 +251,7 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
- ctx['cart'] = self.get_cart()
+ ctx['cart'] = self.get_cart(answers=True)
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
return ctx
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
new file mode 100644
index 000000000..bcbca5502
--- /dev/null
+++ b/src/pretix/presale/views/order.py
@@ -0,0 +1,90 @@
+from itertools import groupby
+from django.db.models import Q
+from django.utils.functional import cached_property
+from django.views.generic import TemplateView
+from django.http import HttpResponseNotFound
+from pretix.base.models import Order, OrderPosition
+from pretix.base.signals import register_payment_providers
+from pretix.presale.views import EventViewMixin, EventLoginRequiredMixin, CartDisplayMixin
+
+
+class OrderDetails(EventViewMixin, EventLoginRequiredMixin, CartDisplayMixin, TemplateView):
+ template_name = "pretixpresale/event/order.html"
+
+ @cached_property
+ def order(self):
+ try:
+ return Order.objects.current.get(
+ user=self.request.user,
+ event=self.request.event,
+ code=self.kwargs['order'],
+ )
+ except Order.DoesNotExist:
+ return None
+
+ def get(self, request, *args, **kwargs):
+ self.kwargs = kwargs
+ if not self.order:
+ return HttpResponseNotFound
+ return super().get(request, *args, **kwargs)
+
+ def itemlist_cartlike(self):
+ """
+ Returns the list of ordered items a format compatible to the
+ CardDisplayMixin, so we can reuse template code
+ """
+ cartpos = OrderPosition.objects.current.filter(
+ order=self.order,
+ ).order_by(
+ 'item', 'variation'
+ ).select_related(
+ 'item', 'variation'
+ ).prefetch_related(
+ 'variation__values', 'variation__values__prop',
+ 'item__questions'
+ )
+
+ # Group items of the same variation
+ # We do this by list manipulations instead of a GROUP BY query, as
+ # Django is unable to join related models in a .values() query
+ def keyfunc(pos):
+ if (pos.item.admission and self.request.event.settings.attendee_names_asked == 'True') \
+ or pos.item.questions.all():
+ return pos.id, "", "", ""
+ return "", pos.item_id, pos.variation_id, pos.price
+
+ positions = []
+ for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
+ g = list(g)
+ group = g[0]
+ group.count = len(g)
+ group.total = group.count * group.price
+ positions.append(group)
+
+ return {
+ 'positions': positions,
+ 'raw': cartpos,
+ 'total': self.order.total,
+ 'payment_fee': self.order.payment_fee,
+ }
+
+ @cached_property
+ def payment_provider(self):
+ responses = register_payment_providers.send(self.request.event)
+ for receiver, response in responses:
+ provider = response(self.request.event)
+ if provider.identifier == self.order.payment_provider:
+ return provider
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+ ctx['order'] = self.order
+ ctx['cart'] = self.get_cart(
+ answers=True,
+ queryset=OrderPosition.objects.current.filter(order=self.order)
+ )
+ if self.order.status == Order.STATUS_PENDING:
+ ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order)
+ elif self.order.status == Order.STATUS_PAID:
+ ctx['payment'] = self.payment_provider.order_paid_render(self.request, self.order)
+ return ctx