From 8c802e534e33ffa3778585139679f6819e458c02 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 12 Mar 2015 22:54:59 +0100 Subject: [PATCH] Display payment details --- src/pretix/base/models.py | 4 +- src/pretix/base/payment.py | 25 ++++++ src/pretix/plugins/banktransfer/payment.py | 5 ++ .../pretixplugins/banktransfer/pending.html | 10 +++ .../static/pretixpresale/less/event.less | 14 +++ .../pretixpresale/event/fragment_cart.html | 75 +++++++++------- .../templates/pretixpresale/event/order.html | 69 ++++++++++++++ src/pretix/presale/urls.py | 3 +- src/pretix/presale/views/__init__.py | 60 +++++++++---- src/pretix/presale/views/checkout.py | 2 +- src/pretix/presale/views/order.py | 90 +++++++++++++++++++ 11 files changed, 306 insertions(+), 51 deletions(-) create mode 100644 src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html create mode 100644 src/pretix/presale/templates/pretixpresale/event/order.html create mode 100644 src/pretix/presale/views/order.py 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 %} -
+ {% if line.item.admission and event.settings.attendee_names_asked == 'True' %} +
{% trans "Attendee name" %}
+
{% if line.attendee_name %}{{ line.attendee_name }}{% else %}{% trans "not answered" %}{% endif %}
+ {% endif %} + {% for q in line.questions %} +
{{ q.question }}
+
{% if q.answer %}{{ q.answer }}{% else %}{% trans "not answered" %}{% endif %}
+ {% endfor %} + + {% else %} +
+
+ {{ event.currency }} {{ line.price|floatformat:2 }} +
+
+ {% if editable %} + + {% csrf_token %} + {% if line.variation %} + + {% else %} + + {% endif %} + + + {% endif %} + {{ line.count }} + {% if editable %} +
- {% csrf_token %} - {% if line.variation %} - - {% else %} - + {% else %} + + {% endif %} + +
{% endif %} - - - {% endif %} - {{ line.count }} - {% if editable %} -
- {% csrf_token %} - {% if line.variation %} - - {% else %} - - {% endif %} - -
{% 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" %} +
+
+
+ {% csrf_token %} + +
+
+
+ {% 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