mirror of
https://github.com/pretix/pretix.git
synced 2026-02-02 02:02:27 +00:00
Allow attendees to change selected add-ons of same price (#3150)
This commit is contained in:
@@ -785,6 +785,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_price',
|
||||
'change_allow_attendee',
|
||||
'primary_color',
|
||||
'theme_color_success',
|
||||
'theme_color_danger',
|
||||
|
||||
@@ -2555,6 +2555,27 @@ class OrderPosition(AbstractPosition):
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
def attendee_change_allowed(self) -> bool:
|
||||
"""
|
||||
Returns whether or not this order can be changed by the attendee.
|
||||
"""
|
||||
from .items import ItemAddOn
|
||||
|
||||
if not self.event.settings.change_allow_attendee or not self.order.user_change_allowed:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
self.order.positions.filter(Q(pk=self.pk) | Q(addon_to_id=self.pk)).annotate(
|
||||
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
|
||||
).select_related('item').prefetch_related('issued_gift_cards')
|
||||
)
|
||||
return (
|
||||
(self.order.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
|
||||
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
|
||||
)
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
"""
|
||||
|
||||
@@ -1484,6 +1484,19 @@ DEFAULTS = {
|
||||
label=_("Do not allow changes after"),
|
||||
)
|
||||
},
|
||||
'change_allow_attendee': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow individual attendees to change their ticket"),
|
||||
help_text=_("By default, only the person who ordered the tickets can make any changes. If you check this "
|
||||
"box, individual attendees can also make changes. However, individual attendees can always "
|
||||
"only make changes that do not change the total price of the order. Such changes can always "
|
||||
"only be made by the main customer."),
|
||||
)
|
||||
},
|
||||
'cancel_allow_user': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
|
||||
@@ -690,6 +690,7 @@ class CancelSettingsForm(SettingsForm):
|
||||
'change_allow_user_price',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_attendee',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
{% 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" %}
|
||||
{% bootstrap_field form.change_allow_attendee layout="control" %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -42,6 +42,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
invoice_address = kwargs.pop('invoice_address')
|
||||
initial = kwargs.get('initial', {})
|
||||
event = kwargs.pop('event')
|
||||
hide_prices = kwargs.pop('hide_prices')
|
||||
quota_cache = kwargs.pop('quota_cache')
|
||||
kwargs['initial'] = initial
|
||||
if instance.variation_id:
|
||||
@@ -105,23 +106,24 @@ class OrderPositionChangeForm(forms.Form):
|
||||
if new_price.gross != current_price.gross and event.settings.change_allow_user_price == 'eq':
|
||||
continue
|
||||
|
||||
if new_price.gross < current_price.gross:
|
||||
if event.settings.display_net_prices:
|
||||
label += ' (- {} {})'.format(money_filter(current_price.gross - new_price.gross, event.currency), _('plus taxes'))
|
||||
else:
|
||||
label += ' (- {})'.format(money_filter(current_price.gross - new_price.gross, event.currency))
|
||||
elif current_price.gross < new_price.gross:
|
||||
if event.settings.display_net_prices:
|
||||
label += ' ({}{} {})'.format(
|
||||
'+ ' if current_price.gross != Decimal('0.00') else '',
|
||||
money_filter(new_price.gross - current_price.gross, event.currency),
|
||||
_('plus taxes')
|
||||
)
|
||||
else:
|
||||
label += ' ({}{})'.format(
|
||||
'+ ' if current_price.gross != Decimal('0.00') else '',
|
||||
money_filter(new_price.gross - current_price.gross, event.currency)
|
||||
)
|
||||
if not hide_prices:
|
||||
if new_price.gross < current_price.gross:
|
||||
if event.settings.display_net_prices:
|
||||
label += ' (- {} {})'.format(money_filter(current_price.gross - new_price.gross, event.currency), _('plus taxes'))
|
||||
else:
|
||||
label += ' (- {})'.format(money_filter(current_price.gross - new_price.gross, event.currency))
|
||||
elif current_price.gross < new_price.gross:
|
||||
if event.settings.display_net_prices:
|
||||
label += ' ({}{} {})'.format(
|
||||
'+ ' if current_price.gross != Decimal('0.00') else '',
|
||||
money_filter(new_price.gross - current_price.gross, event.currency),
|
||||
_('plus taxes')
|
||||
)
|
||||
else:
|
||||
label += ' ({}{})'.format(
|
||||
'+ ' if current_price.gross != Decimal('0.00') else '',
|
||||
money_filter(new_price.gross - current_price.gross, event.currency)
|
||||
)
|
||||
|
||||
choices.append((f'{i.pk}-{v.pk}', label))
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 price">
|
||||
<p>
|
||||
{% if not hide_prices %}
|
||||
{% if c.price_included %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% elif item.free_price %}
|
||||
@@ -87,6 +88,7 @@
|
||||
{% else %}
|
||||
{{ item.min_price|money:event.currency }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box">
|
||||
@@ -117,6 +119,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 price">
|
||||
{% if not hide_prices %}
|
||||
{% if not c.price_included %}
|
||||
{% if var.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
@@ -169,6 +172,7 @@
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if var.cached_availability.0 == 100 or var.initial %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
||||
@@ -240,6 +244,7 @@
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 price">
|
||||
<p>
|
||||
{% if not hide_prices %}
|
||||
{% if not c.price_included %}
|
||||
{% if item.original_price %}
|
||||
<del><span class="sr-only">{% trans "Original price:" %}</span>
|
||||
@@ -290,6 +295,7 @@
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "free" context "price" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.cached_availability.0 == 100 or item.initial %}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
{% load i18n %}
|
||||
{% load classname %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
|
||||
<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">
|
||||
{% if not hide_prices %}
|
||||
{{ op.price_diff|money:request.event.currency }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif op|classname == "AddOperation" %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if op.variation %}
|
||||
{% blocktrans trimmed with item=op.item.name variation=op.variation.value %}
|
||||
Add position ({{ item }} – {{ variation }})
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with item=op.item.name %}
|
||||
Add position ({{ 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">
|
||||
{% if not hide_prices %}
|
||||
{{ op.price.gross|money:request.event.currency }}
|
||||
{% endif %}
|
||||
</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">
|
||||
{% if not hide_prices %}
|
||||
{{ op.price_diff|money:request.event.currency }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not hide_prices %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% for k, l in request.POST.lists %}
|
||||
{% for v in l %}
|
||||
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% for position, addon_positions in formgroups.items %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<strong>{{ position.item }}</strong>
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body addons">
|
||||
<div class="form-order-change form-horizontal">
|
||||
<div class="form-order-change-main">
|
||||
{% 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">
|
||||
{{ position.subevent.name }} · {{ position.subevent.get_date_range_display_as_html }}
|
||||
{% if position.event.settings.show_times %}
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ position.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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 hide_prices=hide_prices %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -13,61 +13,7 @@
|
||||
</h2>
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
{% for position, addon_positions in formgroups.items %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<strong>{{ position.item }}</strong>
|
||||
{% if position.variation %}
|
||||
– {{ position.variation }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body addons">
|
||||
<div class="form-order-change form-horizontal">
|
||||
<div class="form-order-change-main">
|
||||
{% 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">
|
||||
{{ position.subevent.name }} · {{ position.subevent.get_date_range_display_as_html }}
|
||||
{% if position.event.settings.show_times %}
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ position.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
{% include "pretixpresale/event/fragment_change_form.html" %}
|
||||
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -17,193 +17,11 @@
|
||||
{% 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 item=op.item.name variation=op.variation.value %}
|
||||
Add position ({{ item }} – {{ variation }})
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with item=op.item.name %}
|
||||
Add position ({{ 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 %}
|
||||
{% include "pretixpresale/event/fragment_change_confirm.html" %}
|
||||
<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 %}">
|
||||
href="{% eventurl request.event "presale:event.order.change" secret=order.secret order=order.code %}">
|
||||
{% trans "Back" %}
|
||||
</a>
|
||||
</div>
|
||||
@@ -215,5 +33,4 @@
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -51,4 +51,33 @@
|
||||
</div>
|
||||
</div>
|
||||
{% eventsignal event "pretix.presale.signals.position_info" order=order position=position request=request %}
|
||||
{% if attendee_change_allowed %}
|
||||
<div class="panel panel-default panel-cancellation">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Change your ticket" context "action" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you want to make changes to the components of your ticket, you can click on the following button.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed with email=order.email %}
|
||||
You can only make some changes to this ticket yourself. For additional changes, please
|
||||
get in touch with the person who bought the ticket ({{ email }}).
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{% eventurl event 'presale:event.order.position.change' secret=position.web_secret position=position.positionid order=order.code %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit" aria-hidden="true"></span>
|
||||
{% trans "Change ticket" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block title %}{% blocktrans trimmed %}
|
||||
Change ticket
|
||||
{% endblocktrans %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Change ticket
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<p>{% trans "Please select the desired changes to your ticket. Note that you can only perform changes that do not change the total price of the ticket." %}</p>
|
||||
|
||||
{% include "pretixpresale/event/fragment_change_form.html" with hide_prices=request.event.settings.hide_prices_from_attendees %}
|
||||
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{{ view.get_position_url }}">
|
||||
{% trans "Cancel" %}
|
||||
</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 %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load classname %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% block title %}{% blocktrans trimmed %}
|
||||
Change ticket
|
||||
{% endblocktrans %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Change ticket
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
|
||||
<p>{% trans "Please confirm the following changes to your ticket." %}</p>
|
||||
{% include "pretixpresale/event/fragment_change_confirm.html" with hide_prices=request.event.settings.hide_prices_from_attendees %}
|
||||
<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.position.change" secret=position.web_secret position=position.positionid 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 %}
|
||||
@@ -150,6 +150,9 @@ event_patterns = [
|
||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<pid>[0-9]+)/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderPositionDownload.as_view(),
|
||||
name='event.order.position.download'),
|
||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/change$',
|
||||
pretix.presale.views.order.OrderPositionChange.as_view(),
|
||||
name='event.order.position.change'),
|
||||
|
||||
re_path(r'^ical/?$',
|
||||
pretix.presale.views.event.EventIcalDownload.as_view(),
|
||||
|
||||
@@ -371,6 +371,7 @@ class OrderPositionDetails(EventViewMixin, OrderPositionDetailMixin, CartMixin,
|
||||
order=self.order
|
||||
)
|
||||
ctx['tickets_with_download'] = [p for p in ctx['cart']['positions'] if p.generate_ticket]
|
||||
ctx['attendee_change_allowed'] = self.position.attendee_change_allowed
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -1192,19 +1193,7 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
return resp
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/order_change.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if not self.order.user_change_allowed:
|
||||
messages.error(request, _('You cannot change this order.'))
|
||||
return redirect(self.get_order_url())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
class OrderChangeMixin:
|
||||
|
||||
@cached_property
|
||||
def formdict(self):
|
||||
@@ -1232,7 +1221,7 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
@cached_property
|
||||
def positions(self):
|
||||
positions = list(
|
||||
self.order.positions.select_related('item', 'item__tax_rule').prefetch_related(
|
||||
self.get_position_queryset().select_related('item', 'item__tax_rule').prefetch_related(
|
||||
'item__variations', 'addons',
|
||||
)
|
||||
)
|
||||
@@ -1245,7 +1234,8 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
for p in positions:
|
||||
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)
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
hide_prices=self.get_hide_prices())
|
||||
|
||||
if p.addon_to_id is None and self.request.event.settings.change_allow_user_addons:
|
||||
p.addon_form = {
|
||||
@@ -1470,7 +1460,7 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
if self.order.pending_sum < Decimal('0.00'):
|
||||
if self.order.pending_sum < Decimal('0.00') and ocm._totaldiff < 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"
|
||||
@@ -1493,10 +1483,10 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
else:
|
||||
messages.success(self.request, _('The order has been changed.'))
|
||||
|
||||
return redirect(self.get_order_url())
|
||||
return redirect(self.get_self_url())
|
||||
elif not ocm._operations:
|
||||
messages.info(self.request, _('You did not make any changes.'))
|
||||
return redirect(self.get_order_url())
|
||||
return redirect(self.get_self_url())
|
||||
else:
|
||||
new_pending_sum = self.order.pending_sum + ocm._totaldiff
|
||||
can_auto_refund = False
|
||||
@@ -1504,23 +1494,25 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
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 render(request, 'pretixpresale/event/order_change_confirm.html', {
|
||||
return render(request, self.confirm_template_name, {
|
||||
'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,
|
||||
**self.get_confirm_context_data(),
|
||||
})
|
||||
|
||||
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':
|
||||
pr = self.get_price_requirement()
|
||||
if ocm._totaldiff < Decimal('0.00') and pr == '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':
|
||||
if ocm._totaldiff <= Decimal('0.00') and pr == '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':
|
||||
if ocm._totaldiff != Decimal('0.00') and pr == '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:
|
||||
@@ -1531,3 +1523,70 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
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.'))
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderChange(OrderChangeMixin, EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/order_change.html"
|
||||
confirm_template_name = 'pretixpresale/event/order_change_confirm.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if not self.order.user_change_allowed:
|
||||
messages.error(request, _('You cannot change this order.'))
|
||||
return redirect(self.get_order_url())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_self_url(self):
|
||||
return self.get_order_url()
|
||||
|
||||
def get_position_queryset(self):
|
||||
return self.order.positions
|
||||
|
||||
def get_price_requirement(self):
|
||||
return self.request.event.settings.change_allow_user_price
|
||||
|
||||
def get_confirm_context_data(self):
|
||||
return {}
|
||||
|
||||
def get_hide_prices(self):
|
||||
return False
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPositionChange(OrderChangeMixin, EventViewMixin, OrderPositionDetailMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/position_change.html"
|
||||
confirm_template_name = 'pretixpresale/event/position_change_confirm.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
if not self.position:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if not self.position.attendee_change_allowed:
|
||||
messages.error(request, _('You cannot change this order.'))
|
||||
return redirect(self.get_position_url())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_self_url(self):
|
||||
return self.get_position_url()
|
||||
|
||||
def get_position_queryset(self):
|
||||
return self.order.positions.filter(Q(pk=self.position.pk) | Q(addon_to_id=self.position.id))
|
||||
|
||||
def get_price_requirement(self):
|
||||
return 'eq'
|
||||
|
||||
def get_confirm_context_data(self):
|
||||
return {'position': self.position}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['position'] = self.position
|
||||
return ctx
|
||||
|
||||
def get_hide_prices(self):
|
||||
return self.request.event.settings.hide_prices_from_attendees
|
||||
|
||||
@@ -1703,20 +1703,29 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_change_order(self):
|
||||
self.event.settings.change_allow_attendee = True
|
||||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||
admission=True, allow_cancel=True)
|
||||
v = item1.variations.create(value="V")
|
||||
OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=v, price=23)
|
||||
op = OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=v, price=23)
|
||||
assert not self.order.user_change_allowed
|
||||
assert not op.attendee_change_allowed
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
assert self.order.user_change_allowed
|
||||
assert op.attendee_change_allowed
|
||||
|
||||
self.event.settings.change_allow_attendee = False
|
||||
assert not op.attendee_change_allowed
|
||||
self.event.settings.change_allow_attendee = True
|
||||
|
||||
self.event.settings.change_allow_user_variation = False
|
||||
self.order.require_approval = True
|
||||
assert not self.order.user_change_allowed
|
||||
assert not op.attendee_change_allowed
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
assert not self.order.user_change_allowed
|
||||
assert not op.attendee_change_allowed
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_change_order_with_giftcard(self):
|
||||
@@ -1726,10 +1735,12 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=v, price=23)
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_attendee = True
|
||||
self.event.organizer.issued_gift_cards.create(
|
||||
currency="EUR", issued_in=p
|
||||
)
|
||||
assert not self.order.user_change_allowed
|
||||
assert not p.attendee_change_allowed
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_change_checked_in(self):
|
||||
@@ -1738,12 +1749,14 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_attendee = True
|
||||
assert self.order.user_change_allowed
|
||||
Checkin.objects.create(
|
||||
position=self.order.positions.first(),
|
||||
list=CheckinList.objects.create(event=self.event, name='Default')
|
||||
)
|
||||
assert not self.order.user_change_allowed
|
||||
assert not self.order.positions.first().attendee_change_allowed
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_change_order_multiple(self):
|
||||
@@ -1758,7 +1771,9 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
OrderPosition.objects.create(order=self.order, item=item2,
|
||||
variation=v2, price=23)
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_attendee = True
|
||||
assert self.order.user_change_allowed
|
||||
assert not self.order.positions.first().attendee_change_allowed
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_not_change_order(self):
|
||||
@@ -1768,22 +1783,26 @@ class OrderTestCase(BaseQuotaTestCase):
|
||||
OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=v, price=23)
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_attendee = True
|
||||
assert self.order.user_change_allowed is False
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_require_any_variation(self):
|
||||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||
admission=True, allow_cancel=True)
|
||||
OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=None, price=23)
|
||||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||||
variation=None, price=23)
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_attendee = True
|
||||
assert self.order.user_change_allowed is False
|
||||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||
admission=True, allow_cancel=True)
|
||||
v2 = item2.variations.create(value="V")
|
||||
OrderPosition.objects.create(order=self.order, item=item2,
|
||||
variation=v2, price=23)
|
||||
p2 = OrderPosition.objects.create(order=self.order, item=item2,
|
||||
variation=v2, price=23)
|
||||
assert self.order.user_change_allowed is True
|
||||
assert p.attendee_change_allowed is False
|
||||
assert p2.attendee_change_allowed is True
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_can_not_change_order_multiple(self):
|
||||
|
||||
@@ -117,6 +117,11 @@ class OrderChangeVariationTest(BaseOrdersTest):
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
def test_change_variation_paid(self):
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_user_price = 'any'
|
||||
@@ -154,6 +159,13 @@ class OrderChangeVariationTest(BaseOrdersTest):
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert self.order.total == Decimal('35.00')
|
||||
|
||||
# Attendee is not allowed
|
||||
response = self.client.get(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (
|
||||
self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret)
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
def test_change_variation_require_higher_price(self):
|
||||
self.event.settings.change_allow_user_variation = True
|
||||
self.event.settings.change_allow_user_price = 'gt'
|
||||
@@ -1464,3 +1476,83 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
assert self.order.total == Decimal('23.00')
|
||||
r = self.order.refunds.get()
|
||||
assert r.provider == 'giftcard'
|
||||
|
||||
def test_attendee(self):
|
||||
self.workshop2a.default_price = Decimal('0.00')
|
||||
self.workshop2a.save()
|
||||
self.event.settings.change_allow_attendee = True
|
||||
response = self.client.post(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
{
|
||||
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||
form_data = extract_form_fields(doc.select('.main-box form')[0])
|
||||
form_data['confirm'] = 'true'
|
||||
self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), form_data, follow=True
|
||||
)
|
||||
|
||||
with scopes_disabled():
|
||||
a = self.ticket_pos.addons.get()
|
||||
assert a.variation == self.workshop2a
|
||||
|
||||
def test_attendee_limited_to_own_ticket(self):
|
||||
with scopes_disabled():
|
||||
ticket_pos2 = OrderPosition.objects.create(
|
||||
order=self.order,
|
||||
item=self.ticket,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.event.settings.change_allow_attendee = True
|
||||
response = self.client.post(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
{
|
||||
f'cp_{ticket_pos2.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=False
|
||||
)
|
||||
assert response.status_code == 302 # nothing changed
|
||||
|
||||
def test_attendee_needs_to_keep_price(self):
|
||||
self.event.settings.change_allow_user_price = 'any' # ignored, for attendees its always "eq"
|
||||
self.event.settings.change_allow_attendee = True
|
||||
response = self.client.post(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
{
|
||||
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert 'alert-danger' in response.content.decode()
|
||||
assert 'changes' in response.content.decode()
|
||||
|
||||
self.workshop2a.default_price = Decimal('0.00')
|
||||
self.workshop2a.save()
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
{
|
||||
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert 'alert-danger' not in response.content.decode()
|
||||
|
||||
def test_attendee_price_hidden(self):
|
||||
self.event.settings.change_allow_attendee = True
|
||||
response = self.client.get(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
follow=True
|
||||
)
|
||||
assert '€' not in response.content.decode()
|
||||
self.event.settings.hide_prices_from_attendees = False
|
||||
response = self.client.get(
|
||||
'/%s/%s/ticket/%s/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.ticket_pos.positionid, self.ticket_pos.web_secret),
|
||||
follow=True
|
||||
)
|
||||
assert '€' in response.content.decode()
|
||||
|
||||
Reference in New Issue
Block a user