mirror of
https://github.com/pretix/pretix.git
synced 2026-08-28 13:34:40 +00:00
Vouchers: Allow to set all addons or bundles as included (#3322)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
co-authored by
Richard Schreiber
parent
5eff9a86f4
commit
c75c080c5c
@@ -47,6 +47,8 @@ tag string A string that i
|
|||||||
comment string An internal comment on the voucher
|
comment string An internal comment on the voucher
|
||||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||||
|
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
||||||
|
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -95,6 +97,9 @@ Endpoints
|
|||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -198,7 +206,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -225,7 +236,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||||
@@ -264,7 +278,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "ASDKLJCYXCASDASD",
|
"code": "ASDKLJCYXCASDASD",
|
||||||
@@ -279,7 +296,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
},
|
},
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -353,7 +373,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
|||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
|
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||||
|
'all_bundles_included')
|
||||||
read_only_fields = ('id', 'redeemed')
|
read_only_fields = ('id', 'redeemed')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-05-16 11:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0239_giftcard_info'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='voucher',
|
||||||
|
name='all_addons_included',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='voucher',
|
||||||
|
name='all_bundles_included',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2832,8 +2832,12 @@ class CartPosition(AbstractPosition):
|
|||||||
if self.is_bundled:
|
if self.is_bundled:
|
||||||
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
||||||
if bundle:
|
if bundle:
|
||||||
listed_price = bundle.designated_price
|
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
|
||||||
price_after_voucher = bundle.designated_price
|
listed_price = Decimal('0.00')
|
||||||
|
price_after_voucher = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
listed_price = bundle.designated_price
|
||||||
|
price_after_voucher = bundle.designated_price
|
||||||
|
|
||||||
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
||||||
self.listed_price = listed_price
|
self.listed_price = listed_price
|
||||||
|
|||||||
@@ -296,6 +296,14 @@ class Voucher(LoggedModel):
|
|||||||
verbose_name=_("Shows hidden products that match this voucher"),
|
verbose_name=_("Shows hidden products that match this voucher"),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
all_addons_included = models.BooleanField(
|
||||||
|
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
all_bundles_included = models.BooleanField(
|
||||||
|
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
|
|||||||
@@ -512,7 +512,10 @@ class CartManager:
|
|||||||
if cp.is_bundled:
|
if cp.is_bundled:
|
||||||
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
||||||
if bundle:
|
if bundle:
|
||||||
listed_price = bundle.designated_price or Decimal('0.00')
|
if cp.addon_to.voucher_id and cp.addon_to.voucher.all_bundles_included:
|
||||||
|
listed_price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
listed_price = bundle.designated_price
|
||||||
else:
|
else:
|
||||||
listed_price = cp.price
|
listed_price = cp.price
|
||||||
price_after_voucher = listed_price
|
price_after_voucher = listed_price
|
||||||
@@ -712,6 +715,11 @@ class CartManager:
|
|||||||
else:
|
else:
|
||||||
bundle_quotas = []
|
bundle_quotas = []
|
||||||
|
|
||||||
|
if voucher and voucher.all_bundles_included:
|
||||||
|
bundled_price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
bundled_price = bundle.designated_price
|
||||||
|
|
||||||
bop = self.AddOperation(
|
bop = self.AddOperation(
|
||||||
count=bundle.count,
|
count=bundle.count,
|
||||||
item=bitem,
|
item=bitem,
|
||||||
@@ -722,8 +730,8 @@ class CartManager:
|
|||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
bundled=[],
|
bundled=[],
|
||||||
seat=None,
|
seat=None,
|
||||||
listed_price=bundle.designated_price,
|
listed_price=bundled_price,
|
||||||
price_after_voucher=bundle.designated_price,
|
price_after_voucher=bundled_price,
|
||||||
custom_price_input=None,
|
custom_price_input=None,
|
||||||
custom_price_input_is_net=False,
|
custom_price_input_is_net=False,
|
||||||
voucher_ignored=False,
|
voucher_ignored=False,
|
||||||
@@ -809,7 +817,6 @@ class CartManager:
|
|||||||
quota_diff = Counter() # Quota -> Number of usages
|
quota_diff = Counter() # Quota -> Number of usages
|
||||||
operations = []
|
operations = []
|
||||||
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
||||||
price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included)
|
|
||||||
toplevel_cp = self.positions.filter(
|
toplevel_cp = self.positions.filter(
|
||||||
addon_to__isnull=True
|
addon_to__isnull=True
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -819,7 +826,6 @@ class CartManager:
|
|||||||
# Prefill some of the cache containers
|
# Prefill some of the cache containers
|
||||||
for cp in toplevel_cp:
|
for cp in toplevel_cp:
|
||||||
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
||||||
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
|
|
||||||
cpcache[cp.pk] = cp
|
cpcache[cp.pk] = cp
|
||||||
for a in cp.addons.all():
|
for a in cp.addons.all():
|
||||||
if not a.is_bundled:
|
if not a.is_bundled:
|
||||||
|
|||||||
@@ -1892,7 +1892,7 @@ class OrderChangeManager:
|
|||||||
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
|
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)
|
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
||||||
|
|
||||||
if price_included[op.pk].get(item.category_id):
|
if price_included[op.pk].get(item.category_id) or (op.voucher_id and op.voucher.all_addons_included):
|
||||||
price = TAXED_ZERO
|
price = TAXED_ZERO
|
||||||
else:
|
else:
|
||||||
price = get_price(
|
price = get_price(
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ def is_included_for_free(item: Item, addon_to: AbstractPosition):
|
|||||||
return True
|
return True
|
||||||
except ItemAddOn.DoesNotExist:
|
except ItemAddOn.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
if addon_to.voucher_id and addon_to.voucher.all_addons_included:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||||
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||||
|
'all_bundles_included', 'budget'
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
@@ -308,7 +309,8 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||||
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||||
|
'all_bundles_included', 'budget'
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
{% bootstrap_field form.tag layout="control" %}
|
{% bootstrap_field form.tag layout="control" %}
|
||||||
{% bootstrap_field form.comment layout="control" %}
|
{% bootstrap_field form.comment layout="control" %}
|
||||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||||
|
{% bootstrap_field form.all_addons_included layout="control" %}
|
||||||
|
{% bootstrap_field form.all_bundles_included layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Send out emails" %}</legend>
|
<legend>{% trans "Send out emails" %}</legend>
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
{% bootstrap_field form.tag layout="control" %}
|
{% bootstrap_field form.tag layout="control" %}
|
||||||
{% bootstrap_field form.comment layout="control" %}
|
{% bootstrap_field form.comment layout="control" %}
|
||||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||||
|
{% bootstrap_field form.all_addons_included layout="control" %}
|
||||||
|
{% bootstrap_field form.all_bundles_included layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -585,7 +585,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
if items:
|
if items:
|
||||||
formsetentry['categories'].append({
|
formsetentry['categories'].append({
|
||||||
'category': iao.addon_category,
|
'category': iao.addon_category,
|
||||||
'price_included': iao.price_included,
|
'price_included': iao.price_included or (cartpos.voucher_id and cartpos.voucher.all_addons_included),
|
||||||
'multi_allowed': iao.multi_allowed,
|
'multi_allowed': iao.multi_allowed,
|
||||||
'min_count': iao.min_count,
|
'min_count': iao.min_count,
|
||||||
'max_count': iao.max_count,
|
'max_count': iao.max_count,
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
from {{ minprice }}
|
from {{ minprice }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% elif not item.min_price and not item.max_price %}
|
{% elif not item.min_price and not item.max_price %}
|
||||||
{% if not item.mandatory_priced_addons %}
|
{% if not item.mandatory_priced_addons or voucher.all_addons_included %}
|
||||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{% elif not var.display_price.gross %}
|
{% elif not var.display_price.gross %}
|
||||||
{% if not item.mandatory_priced_addons or var.original_price %}
|
{% if not item.mandatory_priced_addons or var.original_price or voucher.all_addons_included %}
|
||||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif event.settings.display_net_prices %}
|
{% elif event.settings.display_net_prices %}
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{% elif not item.display_price.gross %}
|
{% elif not item.display_price.gross %}
|
||||||
{% if not item.mandatory_priced_addons or item.original_price %}
|
{% if not item.mandatory_priced_addons or item.original_price or voucher.all_addons_included %}
|
||||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif event.settings.display_net_prices %}
|
{% elif event.settings.display_net_prices %}
|
||||||
|
|||||||
@@ -1361,7 +1361,7 @@ class OrderChangeMixin:
|
|||||||
if items:
|
if items:
|
||||||
p.addon_form['categories'].append({
|
p.addon_form['categories'].append({
|
||||||
'category': iao.addon_category,
|
'category': iao.addon_category,
|
||||||
'price_included': iao.price_included,
|
'price_included': iao.price_included or (p.voucher_id and p.voucher.all_addons_included),
|
||||||
'multi_allowed': iao.multi_allowed,
|
'multi_allowed': iao.multi_allowed,
|
||||||
'min_count': iao.min_count,
|
'min_count': iao.min_count,
|
||||||
'max_count': iao.max_count,
|
'max_count': iao.max_count,
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ TEST_VOUCHER_RES = {
|
|||||||
'tag': 'Foo',
|
'tag': 'Foo',
|
||||||
'comment': '',
|
'comment': '',
|
||||||
'show_hidden_items': True,
|
'show_hidden_items': True,
|
||||||
|
'all_addons_included': False,
|
||||||
|
'all_bundles_included': False,
|
||||||
'subevent': None,
|
'subevent': None,
|
||||||
'seat': None,
|
'seat': None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from django.utils.timezone import now
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, Item, OrderPosition, Organizer, Quota,
|
CartPosition, Event, Item, OrderPosition, Organizer, Quota, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.services.orders import _perform_order
|
from pretix.base.services.orders import _perform_order
|
||||||
from pretix.testutils.sessions import get_cart_session_key
|
from pretix.testutils.sessions import get_cart_session_key
|
||||||
@@ -116,6 +116,47 @@ class BundlePricesTest(TestCase):
|
|||||||
assert op2.item == self.food
|
assert op2.item == self.food
|
||||||
assert op2.tax_rate == Decimal('7.00')
|
assert op2.tax_rate == Decimal('7.00')
|
||||||
|
|
||||||
|
def test_voucher_includes_bundles(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
|
||||||
|
all_bundles_included=True)
|
||||||
|
|
||||||
|
# Verify correct price displayed on event page
|
||||||
|
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||||
|
self.assertContains(response, '23.00')
|
||||||
|
|
||||||
|
# Verify correct price being added to cart
|
||||||
|
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||||
|
'item_%d' % self.ticket.id: '1',
|
||||||
|
'_voucher_code': v.code
|
||||||
|
}, follow=True)
|
||||||
|
with scopes_disabled():
|
||||||
|
cp1 = CartPosition.objects.get(is_bundled=False)
|
||||||
|
cp2 = CartPosition.objects.get(is_bundled=True)
|
||||||
|
|
||||||
|
assert cp1.price == Decimal('0.00')
|
||||||
|
assert cp1.item == self.ticket
|
||||||
|
assert cp2.price == Decimal('0.00')
|
||||||
|
assert cp2.item == self.food
|
||||||
|
|
||||||
|
# Make sure cart expires
|
||||||
|
cp1.expires = now() - datetime.timedelta(minutes=120)
|
||||||
|
cp1.save()
|
||||||
|
cp2.expires = now() - datetime.timedelta(minutes=120)
|
||||||
|
cp2.save()
|
||||||
|
|
||||||
|
# Verify price is kept if cart expires and order is sent
|
||||||
|
with scopes_disabled():
|
||||||
|
_perform_order(self.event, self._manual_payment(), [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||||
|
op1 = OrderPosition.objects.get(is_bundled=False)
|
||||||
|
op2 = OrderPosition.objects.get(is_bundled=True)
|
||||||
|
assert op1.price == Decimal('0.00')
|
||||||
|
assert op1.item == self.ticket
|
||||||
|
assert op1.tax_rate == Decimal('19.00')
|
||||||
|
assert op2.price == Decimal('0.00')
|
||||||
|
assert op2.item == self.food
|
||||||
|
assert op2.tax_rate == Decimal('7.00')
|
||||||
|
|
||||||
def test_net_price_definitions(self):
|
def test_net_price_definitions(self):
|
||||||
self.tr19.price_includes_tax = False
|
self.tr19.price_includes_tax = False
|
||||||
self.tr19.save()
|
self.tr19.save()
|
||||||
|
|||||||
@@ -3119,6 +3119,27 @@ class CartBundleTest(CartTestMixin, TestCase):
|
|||||||
assert a.item == self.trans
|
assert a.item == self.trans
|
||||||
assert a.price == 1.5
|
assert a.price == 1.5
|
||||||
|
|
||||||
|
@classscope(attr='orga')
|
||||||
|
def test_simple_bundled_voucher_all_free(self):
|
||||||
|
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
|
||||||
|
all_bundles_included=True)
|
||||||
|
self.cm.add_new_items([
|
||||||
|
{
|
||||||
|
'item': self.ticket.pk,
|
||||||
|
'variation': None,
|
||||||
|
'voucher': v.code,
|
||||||
|
'count': 1
|
||||||
|
}
|
||||||
|
])
|
||||||
|
self.cm.commit()
|
||||||
|
cp = CartPosition.objects.get(addon_to__isnull=True)
|
||||||
|
assert cp.item == self.ticket
|
||||||
|
assert cp.price == 0
|
||||||
|
assert cp.addons.count() == 1
|
||||||
|
a = cp.addons.get()
|
||||||
|
assert a.item == self.trans
|
||||||
|
assert a.price == 0
|
||||||
|
|
||||||
@classscope(attr='orga')
|
@classscope(attr='orga')
|
||||||
def test_voucher_on_base_product(self):
|
def test_voucher_on_base_product(self):
|
||||||
v = self.event.vouchers.create(code="foo", item=self.ticket)
|
v = self.event.vouchers.create(code="foo", item=self.ticket)
|
||||||
|
|||||||
@@ -2263,6 +2263,29 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
|
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
|
||||||
|
|
||||||
|
def test_addon_price_included_in_voucher(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
|
||||||
|
valid_until=now() + timedelta(days=2), all_addons_included=True)
|
||||||
|
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||||
|
price_included=False)
|
||||||
|
cp1 = CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=0, expires=now() - timedelta(minutes=10), voucher=v
|
||||||
|
)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.workshop1,
|
||||||
|
price=0, expires=now() - timedelta(minutes=10),
|
||||||
|
addon_to=cp1
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_payment()
|
||||||
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||||
|
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||||
|
with scopes_disabled():
|
||||||
|
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
|
||||||
|
|
||||||
def test_confirm_price_changed_reverse_charge(self):
|
def test_confirm_price_changed_reverse_charge(self):
|
||||||
self._enable_reverse_charge()
|
self._enable_reverse_charge()
|
||||||
self.ticket.default_price = 24
|
self.ticket.default_price = 24
|
||||||
@@ -3970,6 +3993,27 @@ class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
|
|||||||
assert a.item == self.trans
|
assert a.item == self.trans
|
||||||
assert a.price == 1.5
|
assert a.price == 1.5
|
||||||
|
|
||||||
|
@classscope(attr='orga')
|
||||||
|
def test_expired_bundle_with_voucher_bundles_included(self):
|
||||||
|
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, price_mode='none',
|
||||||
|
valid_until=now() + timedelta(days=2), all_bundles_included=True)
|
||||||
|
self.cp1.voucher = v
|
||||||
|
self.cp1.price = 23
|
||||||
|
self.cp1.expires = now() - timedelta(minutes=10)
|
||||||
|
self.cp1.save()
|
||||||
|
self.bundled1.price = 0
|
||||||
|
self.bundled1.expires = now() - timedelta(minutes=10)
|
||||||
|
self.bundled1.save()
|
||||||
|
oid = _perform_order(self.event, self._manual_payment(), [self.cp1.pk, self.bundled1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||||
|
o = Order.objects.get(pk=oid['order_id'])
|
||||||
|
cp = o.positions.get(addon_to__isnull=True)
|
||||||
|
assert cp.item == self.ticket
|
||||||
|
assert cp.price == 23
|
||||||
|
assert cp.addons.count() == 1
|
||||||
|
a = cp.addons.get()
|
||||||
|
assert a.item == self.trans
|
||||||
|
assert a.price == 0
|
||||||
|
|
||||||
@classscope(attr='orga')
|
@classscope(attr='orga')
|
||||||
def test_expired_keep_price(self):
|
def test_expired_keep_price(self):
|
||||||
self.cp1.expires = now() - timedelta(minutes=10)
|
self.cp1.expires = now() - timedelta(minutes=10)
|
||||||
|
|||||||
@@ -650,6 +650,43 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
|||||||
self.order.refresh_from_db()
|
self.order.refresh_from_db()
|
||||||
assert self.order.total == Decimal('35.00')
|
assert self.order.total == Decimal('35.00')
|
||||||
|
|
||||||
|
def test_add_addon_included_in_voucher(self):
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'Workshop 1' in response.content.decode()
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
v = self.event.vouchers.create(item=self.ticket, all_addons_included=True)
|
||||||
|
self.ticket_pos.voucher = v
|
||||||
|
self.ticket_pos.save()
|
||||||
|
|
||||||
|
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||||
|
assert doc.select(f'input[name=cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}]')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_item_{self.workshop1.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'
|
||||||
|
response = 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
|
||||||
|
)
|
||||||
|
assert 'alert-success' in response.content.decode()
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
new_pos = self.ticket_pos.addons.get()
|
||||||
|
assert new_pos.item == self.workshop1
|
||||||
|
assert new_pos.price == Decimal('0.00')
|
||||||
|
self.order.refresh_from_db()
|
||||||
|
assert self.order.total == Decimal('23.00')
|
||||||
|
|
||||||
def test_add_addon_free_price(self):
|
def test_add_addon_free_price(self):
|
||||||
self.workshop1.free_price = True
|
self.workshop1.free_price = True
|
||||||
self.workshop1.save()
|
self.workshop1.save()
|
||||||
|
|||||||
Reference in New Issue
Block a user