forked from CGM_Public/pretix_original
Add add-on products
This commit is contained in:
@@ -11,14 +11,17 @@ from django.views.generic.base import TemplateResponseMixin
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
from pretix.base.services.cart import set_cart_addons
|
||||
from pretix.base.services.orders import perform_order
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.checkout import ContactForm, InvoiceAddressForm
|
||||
from pretix.presale.forms.checkout import (
|
||||
AddOnsForm, ContactForm, InvoiceAddressForm,
|
||||
)
|
||||
from pretix.presale.signals import (
|
||||
checkout_confirm_messages, checkout_flow_steps, order_meta_from_request,
|
||||
)
|
||||
from pretix.presale.views import CartMixin, get_cart_total
|
||||
from pretix.presale.views import CartMixin, get_cart, get_cart_total
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
from pretix.presale.views.questions import QuestionsViewMixin
|
||||
|
||||
@@ -126,6 +129,116 @@ class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
priority = 40
|
||||
identifier = "addons"
|
||||
template_name = "pretixpresale/event/checkout_addons.html"
|
||||
task = set_cart_addons
|
||||
known_errortypes = ['CartError']
|
||||
|
||||
def is_applicable(self, request):
|
||||
return get_cart(request).filter(item__addons__isnull=False).exists()
|
||||
|
||||
def is_completed(self, request, warn=False):
|
||||
for cartpos in get_cart(request).filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__item'
|
||||
):
|
||||
a = cartpos.addons.all()
|
||||
for iao in cartpos.item.addons.all():
|
||||
found = len([1 for p in a if p.item.category_id == iao.addon_category_id])
|
||||
if found < iao.min_count or found > iao.max_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
@cached_property
|
||||
def forms(self):
|
||||
"""
|
||||
A list of forms with one form for each cart position that has questions
|
||||
the user can answer. All forms have a custom prefix, so that they can all be
|
||||
submitted at once.
|
||||
"""
|
||||
formset = []
|
||||
for cartpos in get_cart(self.request).filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation'
|
||||
):
|
||||
current_addon_products = {
|
||||
a.item_id: a.variation_id for a in cartpos.addons.all()
|
||||
}
|
||||
formsetentry = {
|
||||
'cartpos': cartpos,
|
||||
'item': cartpos.item,
|
||||
'variation': cartpos.variation,
|
||||
'categories': []
|
||||
}
|
||||
for iao in cartpos.item.addons.all():
|
||||
category = {
|
||||
'category': iao.addon_category,
|
||||
'min_count': iao.min_count,
|
||||
'max_count': iao.max_count,
|
||||
'form': AddOnsForm(
|
||||
event=self.request.event,
|
||||
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
|
||||
category=iao.addon_category,
|
||||
initial=current_addon_products,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None)
|
||||
)
|
||||
}
|
||||
|
||||
if len(category['form'].fields) > 0:
|
||||
formsetentry['categories'].append(category)
|
||||
|
||||
formset.append(formsetentry)
|
||||
return formset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['forms'] = self.forms
|
||||
return ctx
|
||||
|
||||
def get_success_message(self, value):
|
||||
return None
|
||||
|
||||
def get_success_url(self, value):
|
||||
return self.get_next_url(self.request)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_step_url()
|
||||
|
||||
def get(self, request):
|
||||
self.request = request
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return TemplateFlowStep.get(self, request)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
is_valid = True
|
||||
data = []
|
||||
for f in self.forms:
|
||||
for c in f['categories']:
|
||||
is_valid = is_valid and c['form'].is_valid()
|
||||
if c['form'].is_valid():
|
||||
for k, v in c['form'].cleaned_data.items():
|
||||
itemid = int(k[5:])
|
||||
if v is True:
|
||||
data.append({
|
||||
'addon_to': f['cartpos'].pk,
|
||||
'item': itemid,
|
||||
'variation': None
|
||||
})
|
||||
elif v:
|
||||
data.append({
|
||||
'addon_to': f['cartpos'].pk,
|
||||
'item': itemid,
|
||||
'variation': int(v)
|
||||
})
|
||||
|
||||
if not is_valid:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
return self.do(self.request.event.id, data, self.request.session.session_key)
|
||||
|
||||
|
||||
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
priority = 50
|
||||
identifier = "questions"
|
||||
@@ -395,6 +508,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
|
||||
DEFAULT_FLOW = (
|
||||
AddOnsStep,
|
||||
QuestionsStep,
|
||||
PaymentStep,
|
||||
ConfirmStep
|
||||
|
||||
@@ -2,9 +2,12 @@ from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.utils.formats import number_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Question
|
||||
from pretix.base.models import ItemVariation, Question
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
|
||||
|
||||
@@ -141,3 +144,90 @@ class QuestionsForm(forms.Form):
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
self.fields['question_%s' % q.id] = field
|
||||
|
||||
|
||||
class AddOnsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for selecting add-ons to a product in the cart.
|
||||
"""
|
||||
|
||||
def _label(self, event, item_or_variation):
|
||||
if isinstance(item_or_variation, ItemVariation):
|
||||
variation = item_or_variation
|
||||
item = item_or_variation.item
|
||||
price = variation.price
|
||||
price_net = variation.net_price
|
||||
label = variation.value
|
||||
else:
|
||||
item = item_or_variation
|
||||
price = item.default_price
|
||||
price_net = item.default_price_net
|
||||
label = item.name
|
||||
|
||||
if not item.tax_rate or not price:
|
||||
return '{name} (+ {currency} {price})'.format(
|
||||
name=label, currency=event.currency, price=number_format(price)
|
||||
)
|
||||
elif event.settings.display_net_prices:
|
||||
return '{name} (+ {currency} {price} plus {taxes}% taxes)'.format(
|
||||
name=label, currency=event.currency, price=number_format(price_net),
|
||||
taxes=number_format(item.tax_rate)
|
||||
)
|
||||
else:
|
||||
return '{name} (+ {currency} {price} incl. {taxes}% taxes)'.format(
|
||||
name=label, currency=event.currency, price=number_format(price),
|
||||
taxes=number_format(item.tax_rate)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Takes additional keyword arguments:
|
||||
|
||||
:param category: The category to choose from
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
category = kwargs.pop('category')
|
||||
event = kwargs.pop('event')
|
||||
current_addons = kwargs.pop('initial')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
items = category.items.filter(
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(hide_without_voucher=False)
|
||||
).prefetch_related(
|
||||
'variations__quotas', # for .availability()
|
||||
Prefetch('quotas', queryset=event.quotas.all()),
|
||||
Prefetch('variations', to_attr='available_variations',
|
||||
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).distinct()),
|
||||
).annotate(
|
||||
quotac=Count('quotas'),
|
||||
has_variations=Count('variations')
|
||||
).filter(
|
||||
quotac__gt=0
|
||||
).order_by('category__position', 'category_id', 'position', 'name')
|
||||
|
||||
for i in items:
|
||||
if i.has_variations:
|
||||
field = forms.ChoiceField(
|
||||
choices=[('', '–')] + [
|
||||
(
|
||||
v.pk,
|
||||
self._label(event, v)
|
||||
) for v in i.available_variations
|
||||
],
|
||||
label=i.name,
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
initial=current_addons.get(i.pk)
|
||||
)
|
||||
else:
|
||||
field = forms.BooleanField(
|
||||
label=self._label(event, i),
|
||||
required=False,
|
||||
initial=i.pk in current_addons
|
||||
)
|
||||
|
||||
self.fields['item_%s' % i.pk] = field
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{% trans "Checkout" %}</h2>
|
||||
<p>
|
||||
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
|
||||
</p>
|
||||
<form class="form-horizontal" method="post" data-asynctask>
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="questions_group">
|
||||
{% for form in forms %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" href="#cp{{ form.pos.id }}">
|
||||
<strong>{{ form.item.name }}</strong>
|
||||
{% if form.variation %}
|
||||
– {{ form.variation }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="cp{{ form.pos.id }}"
|
||||
class="panel-collapse collapsed in">
|
||||
<div class="panel-body">
|
||||
{% for c in form.categories %}
|
||||
<fieldset>
|
||||
<legend>{{ c.category.name }}</legend>
|
||||
<p>
|
||||
{% if c.min_count == c.max_count %}
|
||||
{% blocktrans trimmed with min_count=c.min_count %}
|
||||
You need to choose {{ min_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
|
||||
You can choose between {{ min_count }} and {{ max_count }} options from
|
||||
this category.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% bootstrap_form c.form layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% empty %}
|
||||
<em>
|
||||
{% trans "There are no add-ons available for this product." %}
|
||||
</em>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{{ prev_url }}">
|
||||
{% trans "Go back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user