Allow customers to change add-ons on existing orders (#2283)

This commit is contained in:
Raphael Michel
2021-11-19 14:59:54 +01:00
committed by GitHub
parent 34e4f7e0fc
commit 492288f437
19 changed files with 2511 additions and 687 deletions

View File

@@ -764,6 +764,7 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',
'change_allow_user_price',
'primary_color',

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0202_user_needs_password_change'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='is_bundled',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
def fill_is_bundled(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
ItemBundle = apps.get_model("pretixbase", "ItemBundle")
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
for ib in ItemBundle.objects.iterator():
OrderPosition.all.alias(
pos_earlier=Coalesce(Subquery(
OrderPosition.all.filter(
canceled=False,
addon_to=OuterRef('addon_to'),
item=ib.bundled_item,
variation=ib.bundled_variation,
positionid__lt=OuterRef('positionid'),
).values('addon_to').order_by().annotate(c=Count('*')).values('c'),
output_field=models.IntegerField()
), 0)
).filter(
canceled=False,
addon_to__item=ib.base_item,
item=ib.bundled_item,
variation=ib.bundled_variation,
pos_earlier__lt=ib.count,
).update(
is_bundled=True
)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0203_orderposition_is_bundled'),
]
operations = [
migrations.RunPython(
fill_is_bundled,
migrations.RunPython.noop,
),
]

View File

@@ -581,6 +581,7 @@ class Order(LockModel, LoggedModel):
Returns whether or not this order can be canceled by the user.
"""
from .checkin import Checkin
from .items import ItemAddOn
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
return False
@@ -606,7 +607,10 @@ class Order(LockModel, LoggedModel):
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
return (
(self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
(self.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
@property
@scopes_disabled()
@@ -1306,6 +1310,7 @@ class AbstractPosition(models.Model):
seat = models.ForeignKey(
'Seat', null=True, blank=True, on_delete=models.PROTECT
)
is_bundled = models.BooleanField(default=False)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
@@ -2566,7 +2571,6 @@ class CartPosition(AbstractPosition):
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')

View File

@@ -35,7 +35,7 @@
import json
import logging
from collections import Counter, namedtuple
from collections import Counter, defaultdict, namedtuple
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import List, Optional
@@ -46,7 +46,7 @@ from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
)
from django.db.models.functions import Coalesce, Greatest
from django.db.transaction import get_connection
@@ -73,7 +73,7 @@ from pretix.base.models.orders import (
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxRule
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.secrets import assign_ticket_secret
@@ -122,8 +122,7 @@ error_messages = {
'from your cart.'),
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
'removed this item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this '
'item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': _('The presale period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
@@ -131,6 +130,13 @@ error_messages = {
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': _('One of the selected products is not available in the selected country.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
}
logger = logging.getLogger(__name__)
@@ -1261,15 +1267,15 @@ class OrderChangeManager:
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
SeatOperation = namedtuple('SubeventOperation', ('position', 'seat'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price', 'price_diff'))
TaxRuleOperation = namedtuple('TaxRuleOperation', ('position', 'tax_rule'))
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position',))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee',))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
@@ -1386,7 +1392,7 @@ class OrderChangeManager:
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._operations.append(self.PriceOperation(position, price))
self._operations.append(self.PriceOperation(position, price, price.gross - position.price))
def change_tax_rule(self, position_or_fee, tax_rule: TaxRule):
self._operations.append(self.TaxRuleOperation(position_or_fee, tax_rule))
@@ -1426,28 +1432,28 @@ class OrderChangeManager:
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
override_tax_rate=new_rate)
self._totaldiff += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax))
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
def cancel_fee(self, fee: OrderFee):
self._totaldiff -= fee.value
self._operations.append(self.CancelFeeOperation(fee))
self._operations.append(self.CancelFeeOperation(fee, -fee.value))
self._invoice_dirty = True
def add_fee(self, fee: OrderFee):
self._totaldiff += fee.value
self._invoice_dirty = True
self._operations.append(self.AddFeeOperation(fee))
self._operations.append(self.AddFeeOperation(fee, fee.value))
def change_fee(self, fee: OrderFee, value: Decimal):
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
self._totaldiff += value.gross - fee.value
self._invoice_dirty = True
self._operations.append(self.FeeValueOperation(fee, value))
self._operations.append(self.FeeValueOperation(fee, value, value.gross - fee.value))
def cancel(self, position: OrderPosition):
self._totaldiff -= position.price
self._quotadiff.subtract(position.quotas)
self._operations.append(self.CancelOperation(position))
self._operations.append(self.CancelOperation(position, -position.price))
if position.seat:
self._seatdiff.subtract([position.seat])
@@ -1472,7 +1478,7 @@ class OrderChangeManager:
try:
if price is None:
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
else:
elif not isinstance(price, TaxedPrice):
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
@@ -1515,6 +1521,190 @@ class OrderChangeManager:
self._operations.append(self.SplitOperation(position))
def set_addons(self, addons):
if self._operations:
raise ValueError("Setting addons should be the first/only operation")
# Prepare various containers to hold data later
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
selected_addons = defaultdict(Counter) # OrderPos, ItemAddOn -> final desired set of add-ons
opcache = {} # OrderPos.pk -> OrderPos
quota_diff = Counter() # Quota -> Number of usages
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
toplevel_op = self.order.positions.filter(
addon_to__isnull=True
).prefetch_related(
'addons', 'item__addons', 'item__addons__addon_category'
).select_related('item', 'variation')
_items_cache = {
i.pk: i
for i in self.event.items.select_related('category').prefetch_related(
'addons', 'bundles', 'addons__addon_category', 'quotas'
).annotate(
has_variations=Count('variations'),
).filter(
id__in=[a['item'] for a in addons]
).order_by()
}
_variations_cache = {
v.pk: v
for v in ItemVariation.objects.filter(item__event=self.event).prefetch_related(
'quotas'
).select_related('item', 'item__event').filter(
id__in=[a['variation'] for a in addons if a.get('variation')]
).order_by()
}
# Prefill some of the cache containers
for op in toplevel_op:
if op.canceled:
continue
available_categories[op.pk] = {iao.addon_category_id for iao in op.item.addons.all()}
price_included[op.pk] = {iao.addon_category_id: iao.price_included for iao in op.item.addons.all()}
opcache[op.pk] = op
for a in op.addons.all():
if a.canceled:
continue
if not a.is_bundled:
current_addons[op][a.item_id, a.variation_id].append(a)
# Create operations, perform various checks
for a in addons:
# Check whether the specified items are part of what we just fetched from the database
# If they are not, the user supplied item IDs which either do not exist or belong to
# a different event
if a['item'] not in _items_cache or (a['variation'] and a['variation'] not in _variations_cache):
raise OrderError(error_messages['not_for_sale'])
# Only attach addons to things that are actually in this user's cart
if a['addon_to'] not in opcache:
raise OrderError(error_messages['addon_invalid_base'])
op = opcache[a['addon_to']]
item = _items_cache[a['item']]
variation = _variations_cache[a['variation']] if a['variation'] is not None else None
if item.category_id not in available_categories[op.pk]:
raise OrderError(error_messages['addon_invalid_base'])
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.filter(subevent=op.subevent)
if variation is None else variation.quotas.filter(subevent=op.subevent))
if not quotas:
raise OrderError(error_messages['unavailable'])
if (a['item'], a['variation']) in input_addons[op.id]:
raise OrderError(error_messages['addon_duplicate_item'])
if item.require_voucher or op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher):
raise OrderError(error_messages['voucher_required'])
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
if self.order.sales_channel not in item.sales_channels or (
variation and self.order.sales_channel not in variation.sales_channels):
raise OrderError(error_messages['unavailable'])
if op.subevent and item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
if op.subevent and variation and variation.pk in op.subevent.var_overrides and \
not op.subevent.var_overrides[variation.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
if item.has_variations and not variation:
raise OrderError(error_messages['not_for_sale'])
if variation and variation.item_id != item.pk:
raise OrderError(error_messages['not_for_sale'])
if op.subevent and op.subevent.presale_start and now() < op.subevent.presale_start:
raise OrderError(error_messages['not_started'])
if (op.subevent and op.subevent.presale_has_ended) or self.event.presale_has_ended:
raise OrderError(error_messages['ended'])
if item.require_bundling:
raise OrderError(error_messages['unavailable'])
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
if price_included[op.pk].get(item.category_id):
price = TAXED_ZERO
else:
price = get_price(
item, variation, voucher=None, custom_price=a.get('price'), subevent=op.subevent,
custom_price_is_net=self.event.settings.display_net_prices,
invoice_address=self._invoice_address,
)
if a.get('count', 1) > len(current_addons[op][a['item'], a['variation']]):
# This add-on is new, add it to the cart
for quota in quotas:
quota_diff[quota] += a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])
for i in range(a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])):
self.add_position(
item=item, variation=variation, price=price,
addon_to=op, subevent=op.subevent, seat=None,
)
# Check constraints on the add-on combinations
for op in toplevel_op:
item = op.item
for iao in item.addons.all():
selected = selected_addons[op.id, iao.addon_category_id]
n_per_i = Counter()
for (i, v), c in selected.items():
n_per_i[i] += c
if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_max_count'],
{
'base': str(item.name),
'max': iao.max_count,
'cat': str(iao.addon_category.name),
}
)
elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_min_count'],
{
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
}
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise OrderError(
error_messages['addon_no_multi'],
{
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
)
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
self.cancel(a)
def _check_seats(self):
for seat, diff in self._seatdiff.items():
if diff <= 0:

View File

@@ -1301,6 +1301,15 @@ DEFAULTS = {
label=_("Customers can change the variation of the products they purchased"),
)
},
'change_allow_user_addons': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Customers can change their selected add-on products"),
)
},
'change_allow_user_price': {
'default': 'gte',
'type': str,

View File

@@ -0,0 +1,29 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import template
register = template.Library()
@register.filter
def classname(obj):
return obj.__class__.__name__

View File

@@ -639,6 +639,7 @@ class CancelSettingsForm(SettingsForm):
'change_allow_user_variation',
'change_allow_user_price',
'change_allow_user_until',
'change_allow_user_addons',
]
def __init__(self, *args, **kwargs):

View File

@@ -42,13 +42,37 @@
<fieldset>
<legend>{% trans "Order changes" %}</legend>
<div class="alert alert-info">
{% blocktrans trimmed %}
Allowing users to change their order is a feature under development. Therefore, currently only specific changes (such as changing the variation of a product) are possible. More options might be added later.
{% endblocktrans %}
<p>
{% blocktrans trimmed %}
Allowing customers to change their own orders is a complex process due to the many different options pretix provides. Therefore, this feature currently has the following
limitations:
{% endblocktrans %}
</p>
<ul>
<li>{% trans "It is possible to switch to a different variation of the same product, but not to an entirely different product (except for add-on products)." %}</li>
<li>{% trans "Changing the seat or the event date in an event series will become available in the future, but is not possible now." %}</li>
<li>{% trans "If a change leads to a price change, there will not be a change to fees such as payment, service, or shipping fees, even though an additional payment might be required." %}</li>
<li>{% trans "If an add-on product is newly added, the system currently does not validate if there are required questions or fields that need to be filled out." %}</li>
<li>{% trans "Customers currently cannot switch to a product variation or add an add-on product that requires them to use a voucher or membership." %}</li>
<li>{% trans "Additional constraints and validation steps added by plugins are not enforced." %}</li>
</ul>
</div>
{% bootstrap_field form.change_allow_user_variation layout="control" %}
{% bootstrap_field form.change_allow_user_price layout="control" %}
{% bootstrap_field form.change_allow_user_addons layout="control" %}
{% bootstrap_field form.change_allow_user_until layout="control" %}
{% bootstrap_field form.change_allow_user_price layout="control" %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
If the change leads to a price reduction and automatic refunds are enabled for self-service cancellations,
the system will try to refund the money automatically.
{% endblocktrans %}
{% blocktrans trimmed %}
Refunds can be issued as a gift card if the respective option is set, but there is no customer choice between
gift card and direct refund.
{% endblocktrans %}
</p>
</div>
</fieldset>
</div>
<div class="form-group submit-group">

View File

@@ -475,7 +475,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation',
).order_by('pk'):
formsetentry = {
'cartpos': cartpos,
'pos': cartpos,
'item': cartpos.item,
'variation': cartpos.variation,
'categories': []
@@ -582,13 +582,13 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
for i in category['items']:
if i.has_variations:
for v in i.available_variations:
val = int(self.request.POST.get(f'cp_{form["cartpos"].pk}_variation_{i.pk}_{v.pk}') or '0')
price = self.request.POST.get(f'cp_{form["cartpos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}') or '0')
price = self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
if val:
selected[i, v] = val, price
else:
val = int(self.request.POST.get(f'cp_{form["cartpos"].pk}_item_{i.pk}') or '0')
price = self.request.POST.get(f'cp_{form["cartpos"].pk}_item_{i.pk}_price') or '0'
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}') or '0')
price = self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}_price') or '0'
if val:
selected[i, None] = val, price
@@ -627,7 +627,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
validate_cart_addons.send(
sender=self.event,
addons={k: v[0] for k, v in selected.items()},
base_position=form["cartpos"],
base_position=form["pos"],
iao=category['iao']
)
except CartError as e:
@@ -648,7 +648,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
for (i, v), (c, price) in selected.items():
data.append({
'addon_to': f['cartpos'].pk,
'addon_to': f['pos'].pk,
'item': i.pk,
'variation': v.pk if v else None,
'count': c,

View File

@@ -57,7 +57,7 @@ class OrderPositionChangeForm(forms.Form):
pname = str(i)
variations = list(i.variations.all())
if variations:
if variations and event.settings.change_allow_user_variation:
current_quotas = (
instance.variation.quotas.filter(subevent=instance.subevent)
if instance.variation
@@ -126,6 +126,7 @@ class OrderPositionChangeForm(forms.Form):
else:
choices.append((str(i.pk), '%s' % pname))
self.fields['itemvar'].widget.attrs['disabled'] = True
self.fields['itemvar'].help_text = _('No other variations of this product exist.')
if event.settings.change_allow_user_variation:
self.fields['itemvar'].help_text = _('No other variations of this product exist.')
self.fields['itemvar'].choices = choices

View File

@@ -24,327 +24,19 @@
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
</h3>
</summary>
<div id="cp{{ form.cartpos.pk }}">
<div id="cp{{ form.pos.pk }}">
<div class="panel-body">
{% if form.cartpos.subevent %}
{% if form.pos.subevent %}
<p>
<span class="fa fa-calendar" aria-hidden="true"></span>
{{ form.cartpos.subevent.name }} &middot; {{ form.cartpos.subevent.get_date_range_display_as_html }}
{% if form.cartpos.event.settings.show_times %}
{{ form.pos.subevent.name }} &middot; {{ form.pos.subevent.get_date_range_display_as_html }}
{% if form.pos.event.settings.show_times %}
<span class="fa fa-clock-o" aria-hidden="true"></span>
{{ form.cartpos.subevent.date_from|date:"TIME_FORMAT" }}
{{ form.pos.subevent.date_from|date:"TIME_FORMAT" }}
{% endif %}
</p>
{% endif %}
{% for c in form.categories %}
<fieldset>
<legend>{{ c.category.name }}</legend>
{% if c.category.description %}
{{ c.category.description|rich_text }}
{% endif %}
{% if c.min_count == c.max_count %}
<p>
{% blocktrans trimmed count min_count=c.min_count %}
You need to choose exactly one option from this category.
{% plural %}
You need to choose {{ min_count }} options from this category.
{% endblocktrans %}
</p>
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
{% elif c.min_count == 0 %}
<p>
{% blocktrans trimmed count max_count=c.max_count %}
You can choose {{ max_count }} option from this category.
{% plural %}
You can choose up to {{ max_count }} options from this category.
{% endblocktrans %}
</p>
{% else %}
<p>
{% 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 %}
</p>
{% endif %}
{% for item in c.items %}
{% if item.has_variations %}
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="item-{{ item.pk }}">
<div class="row-fluid product-row headline">
<div class="col-md-8 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small>
</p>
{% endif %}
</div>
</div>
<div class="col-md-2 col-xs-6 price">
<p>
{% if c.price_included %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% elif item.free_price %}
{% blocktrans trimmed with price=item.min_price|money:event.currency %}
from {{ price }}
{% endblocktrans %}
{% elif item.min_price != item.max_price %}
<span class="sr-only">
{% blocktrans trimmed with from_price=item.min_price|money:event.currency to_price=item.max_price|money:event.currency %}
from {{ from_price }} to {{ to_price }}
{% endblocktrans %}
</span>
<span aria-hidden="true">{{ item.min_price|money:event.currency }} {{ item.max_price|money:event.currency }}</span>
{% elif not item.min_price and not item.max_price %}
{% else %}
{{ item.min_price|money:event.currency }}
{% endif %}
</p>
</div>
<div class="col-md-2 col-xs-6 availability-box">
{% if not event.settings.show_variations_expanded %}
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
data-label-alt="{% trans "Hide variants" %}"
aria-expanded="false"
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{item}}{% endblocktrans %}">
{% trans "Show variants" %}
</button>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
{% for var in item.available_variations %}
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
<h5 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
{% if var.description %}
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if not c.price_included %}
{% if var.original_price %}
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ var.original_price.net|money:event.currency }}
{% else %}
{{ var.original_price.gross|money:event.currency }}
{% endif %}
</del>
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% if item.free_price %}
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price"
placeholder="0"
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
step="any"
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
>
</div>
{% elif not var.display_price.gross %}
{% elif event.settings.display_net_prices %}
{{ var.display_price.net|money:event.currency }}
{% else %}
{{ var.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price or var.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
{% else %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% endif %}
</div>
{% if var.cached_availability.0 == 100 or var.initial %}
<div class="col-md-2 col-xs-6 availability-box available">
{% if c.max_count == 1 or not c.multi_allowed %}
<label class="item-checkbox-label">
<input type="checkbox" value="1"
{% if var.initial %}checked="checked"{% endif %}
id="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
data-exclusive-prefix="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_"
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if var.initial %}value="{{ var.initial }}"{% endif %}
max="{{ c.max_count }}"
id="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.cartpos.pk }}_variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 event=event item=item var=var %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endfor %}
</div>
</article>
{% else %}
<article aria-labelledby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description"{% endif %} class="row-fluid product-row simple">
<div class="col-md-8 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="cp-{{ form.cartpos.pk }}-item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small>
</p>
{% endif %}
</div>
</div>
<div class="col-md-2 col-xs-6 price">
<p>
{% if not c.price_included %}
{% if item.original_price %}
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ item.original_price.net|money:event.currency }}
{% else %}
{{ item.original_price.gross|money:event.currency }}
{% endif %}
</del>
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% if item.free_price %}
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price" placeholder="0"
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}_price"
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
step="any">
</div>
{% elif not item.display_price.gross %}
{% elif event.settings.display_net_prices %}
{{ item.display_price.net|money:event.currency }}
{% else %}
{{ item.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
{% else %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% endif %}
</p>
</div>
{% if item.cached_availability.0 == 100 or item.initial %}
<div class="col-md-2 col-xs-6 availability-box available">
{% if c.max_count == 1 or not c.multi_allowed %}
<label class="item-checkbox-label">
<input type="checkbox" value="1"
{% if item.initial %}checked="checked"{% endif %}
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
id="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.id }}-description"{% endif %}>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ c.max_count }}"
{% if item.initial %}value="{{ item.initial }}"{% endif %}
name="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
id="cp_{{ form.cartpos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.cartpos.pk }}-item-{{ item.id }}-description"{% endif %}>
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 event=event item=item var=0 %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endif %}
{% endfor %}
</fieldset>
{% empty %}
<em>
{% trans "There are no add-ons available for this product." %}
</em>
{% endfor %}
{% include "pretixpresale/event/fragment_addon_choice.html" with form=form %}
</div>
</div>
</details>

View File

@@ -0,0 +1,316 @@
{% load i18n %}
{% load l10n %}
{% load eventurl %}
{% load money %}
{% load thumb %}
{% load eventsignal %}
{% load rich_text %}
{% for c in form.categories %}
<fieldset>
<legend>{{ c.category.name }}</legend>
{% if c.category.description %}
{{ c.category.description|rich_text }}
{% endif %}
{% if c.min_count == c.max_count %}
<p>
{% blocktrans trimmed count min_count=c.min_count %}
You need to choose exactly one option from this category.
{% plural %}
You need to choose {{ min_count }} options from this category.
{% endblocktrans %}
</p>
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
{% elif c.min_count == 0 %}
<p>
{% blocktrans trimmed count max_count=c.max_count %}
You can choose {{ max_count }} option from this category.
{% plural %}
You can choose up to {{ max_count }} options from this category.
{% endblocktrans %}
</p>
{% else %}
<p>
{% 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 %}
</p>
{% endif %}
{% for item in c.items %}
{% if item.has_variations %}
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="item-{{ item.pk }}">
<div class="row-fluid product-row headline">
<div class="col-md-8 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small>
</p>
{% endif %}
</div>
</div>
<div class="col-md-2 col-xs-6 price">
<p>
{% if c.price_included %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% elif item.free_price %}
{% blocktrans trimmed with price=item.min_price|money:event.currency %}
from {{ price }}
{% endblocktrans %}
{% elif item.min_price != item.max_price %}
<span class="sr-only">
{% blocktrans trimmed with from_price=item.min_price|money:event.currency to_price=item.max_price|money:event.currency %}
from {{ from_price }} to {{ to_price }}
{% endblocktrans %}
</span>
<span aria-hidden="true">{{ item.min_price|money:event.currency }} {{ item.max_price|money:event.currency }}</span>
{% elif not item.min_price and not item.max_price %}
{% else %}
{{ item.min_price|money:event.currency }}
{% endif %}
</p>
</div>
<div class="col-md-2 col-xs-6 availability-box">
{% if not event.settings.show_variations_expanded %}
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
data-label-alt="{% trans "Hide variants" %}"
aria-expanded="false"
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{item}}{% endblocktrans %}">
{% trans "Show variants" %}
</button>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
{% for var in item.available_variations %}
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
<h5 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
{% if var.description %}
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if not c.price_included %}
{% if var.original_price %}
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ var.original_price.net|money:event.currency }}
{% else %}
{{ var.original_price.gross|money:event.currency }}
{% endif %}
</del>
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% if item.free_price %}
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price"
placeholder="0"
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
step="any"
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
>
</div>
{% elif not var.display_price.gross %}
{% elif event.settings.display_net_prices %}
{{ var.display_price.net|money:event.currency }}
{% else %}
{{ var.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price or var.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
{% else %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% endif %}
</div>
{% if var.cached_availability.0 == 100 or var.initial %}
<div class="col-md-2 col-xs-6 availability-box available">
{% if c.max_count == 1 or not c.multi_allowed %}
<label class="item-checkbox-label">
<input type="checkbox" value="1"
{% if var.initial %}checked="checked"{% endif %}
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
data-exclusive-prefix="cp_{{ form.pos.pk }}_variation_{{ item.id }}_"
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if var.initial %}value="{{ var.initial }}"{% endif %}
max="{{ c.max_count }}"
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 event=event item=item var=var %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endfor %}
</div>
</article>
{% else %}
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description"{% endif %} class="row-fluid product-row simple">
<div class="col-md-8 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small>
</p>
{% endif %}
</div>
</div>
<div class="col-md-2 col-xs-6 price">
<p>
{% if not c.price_included %}
{% if item.original_price %}
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ item.original_price.net|money:event.currency }}
{% else %}
{{ item.original_price.gross|money:event.currency }}
{% endif %}
</del>
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% if item.free_price %}
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price" placeholder="0"
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="cp_{{ form.pos.pk }}_item_{{ item.id }}_price"
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
step="any">
</div>
{% elif not item.display_price.gross %}
{% elif event.settings.display_net_prices %}
{{ item.display_price.net|money:event.currency }}
{% else %}
{{ item.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
{% else %}
<span class="sr-only">{% trans "free" context "price" %}</span>
{% endif %}
</p>
</div>
{% if item.cached_availability.0 == 100 or item.initial %}
<div class="col-md-2 col-xs-6 availability-box available">
{% if c.max_count == 1 or not c.multi_allowed %}
<label class="item-checkbox-label">
<input type="checkbox" value="1"
{% if item.initial %}checked="checked"{% endif %}
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ c.max_count }}"
{% if item.initial %}value="{{ item.initial }}"{% endif %}
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 event=event item=item var=0 %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endif %}
{% endfor %}
</fieldset>
{% empty %}
<em>
{% trans "There are no add-ons available for this product." %}
</em>
{% endfor %}

View File

@@ -2,7 +2,9 @@
{% load i18n %}
{% load bootstrap3 %}
{% load rich_text %}
{% block title %}{% trans "Modify order" %}{% endblock %}
{% block title %}{% blocktrans trimmed with code=order.code %}
Change order: {{ code }}
{% endblocktrans %}{% endblock %}
{% block content %}
<h2>
{% blocktrans trimmed with code=order.code %}
@@ -11,7 +13,7 @@
</h2>
<form method="post" href="">
{% csrf_token %}
{% for position, positions in formgroups.items %}
{% for position, addon_positions in formgroups.items %}
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
@@ -21,43 +23,48 @@
{% endif %}
</h3>
</div>
<div class="panel-body">
<div class="panel-body addons">
<div class="form-order-change form-horizontal">
{% if position.subevent %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Date" context "subevent" %}
</label>
<div class="col-md-9 form-control-text">
<ul class="addon-list">
{{ pos.subevent.name }} &middot; {{ pos.subevent.get_date_range_display_as_html }}
{% if pos.event.settings.show_times %}
<span class="fa fa-clock-o" aria-hidden="true"></span>
{{ pos.subevent.date_from|date:"TIME_FORMAT" }}
{% endif %}
</ul>
</div>
</div>
{% endif %}
{% for p in positions %}
{% if p.pk != position.pk %}
{# Add-Ons #}
<legend>+ {{ p.item.name }}{% if p.variation %} {{ p.variation.value }}{% endif %}</legend>
{% endif %}
{% if p.attendee_name %}
<div class="form-order-change-main">
{% if position.subevent %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Attendee name" %}
{% trans "Date" context "subevent" %}
</label>
<div class="col-md-9 form-control-text">
{{ p.attendee_name }}
<ul class="addon-list">
{{ pos.subevent.name }} &middot; {{ pos.subevent.get_date_range_display_as_html }}
{% if pos.event.settings.show_times %}
<span class="fa fa-clock-o" aria-hidden="true"></span>
{{ pos.subevent.date_from|date:"TIME_FORMAT" }}
{% endif %}
</ul>
</div>
</div>
{% endif %}
{% bootstrap_form p.form layout="checkout" %}
{% endfor %}
{% for p in addon_positions %}
{% if p.pk != position.pk %}
{# Add-Ons #}
<legend>+ {{ p.item.name }}{% if p.variation %} {{ p.variation.value }}{% endif %}</legend>
{% endif %}
{% if p.attendee_name %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Attendee name" %}
</label>
<div class="col-md-9 form-control-text">
{{ p.attendee_name }}
</div>
</div>
{% endif %}
{% bootstrap_form p.form layout="checkout" %}
{% endfor %}
</div>
</div>
{% if position.addon_form %}
{% include "pretixpresale/event/fragment_addon_choice.html" with form=position.addon_form %}
{% endif %}
</div>
</div>
{% endfor %}
@@ -71,7 +78,7 @@
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Save changes" %}
{% trans "Continue" %}
</button>
</div>
<div class="clearfix"></div>

View File

@@ -0,0 +1,219 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load classname %}
{% load eventurl %}
{% load money %}
{% block title %}{% blocktrans trimmed with code=order.code %}
Change order: {{ code }}
{% endblocktrans %}{% endblock %}
{% block content %}
<h2>
{% blocktrans trimmed with code=order.code %}
Change order: {{ code }}
{% endblocktrans %}
</h2>
<form method="post" class="form-horizontal" href="">
{% csrf_token %}
<p>{% trans "Please confirm the following changes to your order." %}</p>
<div class="row-fluid">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Change summary" %}
</h3>
</div>
<table class="panel-body table table-hover">
{% for op in operations %}
{% if op|classname == "ItemOperation" %}
<tr>
<td>
{% if op.position.variation or op.variation %}
{% blocktrans trimmed with positionid=op.position.positionid old_item=op.position.item.name old_variation=op.position.variation new_item=op.item.name new_variation=op.variation %}
Change position #{{ positionid }} from "{{ old_item }} {{ old_variation }}
" to "{{ new_item }} {{ new_variation }}"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with positionid=op.position.positionid old_item=op.position.item.name new_item=op.item.name %}
Change position #{{ positionid }} from "{{ old_item }}" to "{{ new_item }}"
{% endblocktrans %}
{% endif %}
{% if op.position.addon_to %}
<span class="text-muted">
<br>
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
</span>
{% endif %}
</td>
<td class="text-right flip">
</td>
</tr>
{% elif op|classname == "SubeventOperation" %}
<tr>
<td>
{% blocktrans trimmed with positionid=op.position.positionid old=op.position.subevent new=op.subevent %}
Change date of position #{{ positionid }} from "{{ old }}" to "{{ new }}"
{% endblocktrans %}
</td>
<td class="text-right flip">
</td>
</tr>
{% elif op|classname == "PriceOperation" %}
<tr>
<td>
{% blocktrans trimmed with positionid=op.position.positionid old=op.position.price new=op.price %}
Change price of position #{{ positionid }} from {{ old }} to {{ new }}
{% endblocktrans %}
{% if op.position.addon_to %}
<span class="text-muted">
<br>
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
</span>
{% endif %}
</td>
<td class="text-right flip">
{{ op.price_diff|money:request.event.currency }}
</td>
</tr>
{% elif op|classname == "AddOperation" %}
<tr>
<td>
{% if op.variation %}
{% blocktrans trimmed with positionid=op.position.positionid item=op.item.name variation=op.variation.value %}
Add position #{{ positionid }} ({{ item }} {{ variation }})
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with positionid=op.position.positionid item=op.item.name %}
Add position #{{ positionid }} ({{ item }})
{% endblocktrans %}
{% endif %}
{% if op.addon_to %}
<span class="text-muted">
<br>
<small>{% blocktrans with positionid=op.addon_to.positionid %}Add-on product
to position #{{ positionid }}{% endblocktrans %}</small>
</span>
{% endif %}
</td>
<td class="text-right flip">
{{ op.price.gross|money:request.event.currency }}
</td>
</tr>
{% elif op|classname == "CancelOperation" %}
<tr>
<td>
{% if op.position.variation %}
{% blocktrans trimmed with positionid=op.position.positionid item=op.position.item.name variation=op.position.variation.value %}
Remove position #{{ positionid }} ({{ item }} {{ variation }})
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with positionid=op.position.positionid item=op.position.item.name %}
Remove position #{{ positionid }} ({{ item }})
{% endblocktrans %}
{% endif %}
{% if op.position.addon_to %}
<span class="text-muted">
<br>
<small>{% blocktrans with positionid=op.position.addon_to.positionid %}
Add-on product to position #{{ positionid }}{% endblocktrans %}</small>
</span>
{% endif %}
</td>
<td class="text-right flip">
{{ op.price_diff|money:request.event.currency }}
</td>
</tr>
{% endif %}
{% endfor %}
<tfoot>
<tr>
<td><strong>{% trans "Total price change" %}</strong></td>
<td class="text-right flip">
<strong>
{{ totaldiff|money:request.event.currency }}
</strong>
</td>
</tr>
{% if totaldiff %}
<tr>
<td><strong>{% trans "New order total" %}</strong></td>
<td class="text-right flip">
{{ totaldiff|add:order.total|money:request.event.currency }}
</td>
</tr>
<tr>
<td><strong>{% trans "You already paid" %}</strong></td>
<td class="text-right flip">
{{ order.payment_refund_sum|money:request.event.currency }}
</td>
</tr>
<tr>
<td>
{% if new_pending_sum > 0 %}
<strong>{% trans "You will need to pay" %}</strong>
<br>
<span class="text-muted">
{% trans "Your entire order will be considered unpaid until you paid this difference." %}
</span>
{% else %}
<strong>{% trans "You will be refunded" %}</strong>
<br>
<span class="text-muted">
{% if request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "manually" %}
{% trans "The organizer will get in touch with you to clarify the details of your refund." %}
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "force" %}
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
{% else %}
{% if can_auto_refund %}
{% blocktrans trimmed %}
The refund amount will automatically be sent back to your original payment method. Depending
on the payment method, please allow for up to two weeks before this appears on your
statement.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
With the payment method you used, the refund amount <strong>can not be sent back to you
automatically</strong>. Instead, the event organizer will need to initiate the transfer
manually. Please be patient as this might take a bit longer.
{% endblocktrans %}
{% endif %}
{% endif %}
</span>
{% endif %}
</td>
<td class="text-right flip">
<strong>
{{ new_pending_sum|money:request.event.currency }}
</strong>
</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
</div>
{% for k, l in request.POST.lists %}
{% for v in l %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}
{% endfor %}
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% eventurl request.event "presale:event.order.change" secret=order.secret order=order.code %}">
{% trans "Back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit" name="confirm" value="true">
{% trans "Perform changes" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -38,19 +38,20 @@ import json
import mimetypes
import os
import re
from collections import OrderedDict
from collections import OrderedDict, defaultdict
from decimal import Decimal
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.files import File
from django.db import transaction
from django.db.models import Exists, OuterRef, Q, Sum
from django.http import (
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -65,6 +66,7 @@ from pretix.base.models.orders import (
CachedCombinedTicket, InvoiceAddress, OrderFee, OrderPayment, OrderRefund,
QuestionAnswer,
)
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
@@ -72,7 +74,8 @@ from pretix.base.services.invoices import (
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, cancel_order, change_payment_provider,
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
change_payment_provider, error_messages,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate, invalidate_cache
@@ -90,6 +93,7 @@ from pretix.presale.signals import question_form_fields_overrides
from pretix.presale.views import (
CartMixin, EventViewMixin, iframe_entry_view_wrapper,
)
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin
@@ -1165,6 +1169,8 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
def formdict(self):
storage = OrderedDict()
for pos in self.positions:
if self.request.event.settings.change_allow_user_addons and pos.addon_to_id:
continue
if pos.addon_to_id:
if pos.addon_to not in storage:
storage[pos.addon_to] = []
@@ -1186,10 +1192,11 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
def positions(self):
positions = list(
self.order.positions.select_related('item', 'item__tax_rule').prefetch_related(
'item__variations',
'item__variations', 'addons',
)
)
quota_cache = {}
item_cache = {}
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
@@ -1198,6 +1205,87 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p,
invoice_address=ia, event=self.request.event, quota_cache=quota_cache,
data=self.request.POST if self.request.method == "POST" else None)
if p.addon_to_id is None and self.request.event.settings.change_allow_user_addons:
p.addon_form = {
'pos': p,
'categories': []
}
current_addon_products = defaultdict(list)
for a in p.addons.all():
if a.canceled:
continue
if not a.is_bundled:
current_addon_products[a.item_id, a.variation_id].append(a)
for iao in p.item.addons.all():
ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk)
if ckey not in item_cache:
# Get all items to possibly show
items, _btn = get_grouped_items(
self.request.event,
subevent=p.subevent,
voucher=None,
channel=self.order.sales_channel,
base_qs=iao.addon_category.items,
allow_addons=True,
quota_cache=quota_cache,
memberships=(
self.request.customer.usable_memberships(
for_event=p.subevent or self.request.event,
testmode=self.order.testmode
)
if self.order.customer else None
),
)
item_cache[ckey] = items
else:
items = item_cache[ckey]
for i in items:
i.allow_waitinglist = False
if i.has_variations:
for v in i.available_variations:
v.initial = len(current_addon_products[i.pk, v.pk])
if v.initial and i.free_price:
a = current_addon_products[i.pk, v.pk][0]
v.initial_price = TaxedPrice(
net=a.price - a.tax_value,
gross=a.price,
tax=a.tax_value,
name=a.item.tax_rule.name if a.item.tax_rule else "",
rate=a.tax_rate,
)
else:
v.initial_price = v.display_price
i.expand = any(v.initial for v in i.available_variations)
else:
i.initial = len(current_addon_products[i.pk, None])
if i.initial and i.free_price:
a = current_addon_products[i.pk, None][0]
i.initial_price = TaxedPrice(
net=a.price - a.tax_value,
gross=a.price,
tax=a.tax_value,
name=a.item.tax_rule.name if a.item.tax_rule else "",
rate=a.tax_rate,
)
else:
i.initial_price = i.display_price
if items:
p.addon_form['categories'].append({
'category': iao.addon_category,
'price_included': iao.price_included,
'multi_allowed': iao.multi_allowed,
'min_count': iao.min_count,
'max_count': iao.max_count,
'iao': iao,
'items': [i for i in items if not i.require_voucher]
})
return positions
def _process_change(self, ocm):
@@ -1241,7 +1329,56 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
return False
return True
def post(self, *args, **kwargs):
def _clean_category(self, form, category):
selected = {}
for i in category['items']:
if i.has_variations:
for v in i.available_variations:
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}') or '0')
price = self.request.POST.get(f'cp_{form["pos"].pk}_variation_{i.pk}_{v.pk}_price') or '0'
if val:
selected[i, v] = val, price
else:
val = int(self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}') or '0')
price = self.request.POST.get(f'cp_{form["pos"].pk}_item_{i.pk}_price') or '0'
if val:
selected[i, None] = val, price
if sum(a[0] for a in selected.values()) > category['max_count']:
# TODO: Proper pluralization
raise ValidationError(
_(error_messages['addon_max_count']),
'addon_max_count',
{
'base': str(form['pos'].item.name),
'max': category['max_count'],
'cat': str(category['category'].name),
}
)
elif sum(a[0] for a in selected.values()) < category['min_count']:
# TODO: Proper pluralization
raise ValidationError(
_(error_messages['addon_min_count']),
'addon_min_count',
{
'base': str(form['pos'].item.name),
'min': category['min_count'],
'cat': str(category['category'].name),
}
)
elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']:
raise ValidationError(
_(error_messages['addon_no_multi']),
'addon_no_multi',
{
'base': str(form['pos'].item.name),
'cat': str(category['category'].name),
}
)
return selected
def post(self, request, *args, **kwargs):
was_paid = self.order.status == Order.STATUS_PAID
ocm = OrderChangeManager(
self.order,
@@ -1249,28 +1386,106 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
notify=True,
reissue_invoice=True,
)
form_valid = self._process_change(ocm)
addons_data = []
for p in self.positions:
if p.addon_to_id or not hasattr(p, 'addon_form'):
continue
for c in p.addon_form['categories']:
try:
selected = self._clean_category(p.addon_form, c)
except ValidationError as e:
messages.error(request, e.message % e.params if e.params else e.message)
return self.get(request, *args, **kwargs)
for (i, v), (c, price) in selected.items():
addons_data.append({
'addon_to': p.pk,
'item': i.pk,
'variation': v.pk if v else None,
'count': c,
'price': price,
})
try:
ocm.set_addons(addons_data)
except OrderError as e:
messages.error(self.request, str(e))
form_valid = False
else:
form_valid = self._process_change(ocm)
if not form_valid:
messages.error(self.request, _('An error occurred. Please see the details below.'))
else:
try:
ocm.commit(check_quotas=True)
self._validate_total_diff(ocm)
except OrderError as e:
messages.error(self.request, str(e))
else:
if self.order.status != Order.STATUS_PAID and was_paid:
messages.success(self.request, _('The order has been changed. You can now proceed by paying the open amount of {amount}.').format(
amount=money_filter(self.order.pending_sum, self.request.event.currency)
))
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.change', kwargs={
'order': self.order.code,
'secret': self.order.secret
}))
if "confirm" in request.POST:
try:
ocm.commit(check_quotas=True)
except OrderError as e:
messages.error(self.request, str(e))
else:
messages.success(self.request, _('The order has been changed.'))
if self.order.pending_sum < Decimal('0.00'):
auto_refund = (
not self.request.event.settings.cancel_allow_user_paid_require_approval
and self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard != "manually"
)
refund_as_giftcard = self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard == 'force'
if auto_refund:
try:
_try_auto_refund(self.order, refund_as_giftcard=refund_as_giftcard)
except OrderError as e:
messages.error(self.request, str(e))
if self.order.status != Order.STATUS_PAID and was_paid:
messages.success(self.request, _('The order has been changed. You can now proceed by paying the open amount of {amount}.').format(
amount=money_filter(self.order.pending_sum, self.request.event.currency)
))
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.change', kwargs={
'order': self.order.code,
'secret': self.order.secret
}))
else:
messages.success(self.request, _('The order has been changed.'))
return redirect(self.get_order_url())
elif not ocm._operations:
messages.info(self.request, _('You did not make any changes.'))
return redirect(self.get_order_url())
else:
new_pending_sum = self.order.pending_sum + ocm._totaldiff
can_auto_refund = False
if new_pending_sum < Decimal('0.00'):
proposals = self.order.propose_auto_refunds(Decimal('-1.00') * new_pending_sum)
can_auto_refund = sum(proposals.values()) == Decimal('-1.00') * new_pending_sum
return self.get(*args, **kwargs)
return render(request, 'pretixpresale/event/order_change_confirm.html', {
'operations': ocm._operations,
'totaldiff': ocm._totaldiff,
'order': self.order,
'payment_refund_sum': self.order.payment_refund_sum,
'new_pending_sum': new_pending_sum,
'can_auto_refund': can_auto_refund,
})
return self.get(request, *args, **kwargs)
def _validate_total_diff(self, ocm):
if ocm._totaldiff < Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'gte':
raise OrderError(_('You may not change your order in a way that reduces the total price.'))
if ocm._totaldiff <= Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'gt':
raise OrderError(_('You may only change your order in a way that increases the total price.'))
if ocm._totaldiff != Decimal('0.00') and self.request.event.settings.change_allow_user_price == 'eq':
raise OrderError(_('You may not change your order in a way that changes the total price.'))
if ocm._totaldiff > Decimal('0.00') and self.order.status == Order.STATUS_PAID:
self.order.set_expires(
now(),
self.order.event.subevents.filter(id__in=self.order.positions.values_list('subevent_id', flat=True))
)
if self.order.expires < now():
raise OrderError(_('You may not change your order in a way that increases the total price since '
'payments are no longer being accepted for this event.'))

View File

@@ -109,6 +109,13 @@
font-weight: bold;
}
}
.form-order-change-main {
border-bottom: 1px solid $hr-border;
background: $panel-footer-bg;
margin: -15px -15px 15px;
padding: 15px 15px 0;
}
.profile-pre-select {
background: $panel-footer-bg;
margin-top: -15px;
@@ -171,4 +178,4 @@
.panel-heading-flex-gap {
flex: 1;
}
}
}