Add new option Item.min_per_order

This commit is contained in:
Raphael Michel
2017-04-13 14:16:23 +02:00
parent ae6ad8870d
commit 3c59a870e7
7 changed files with 172 additions and 10 deletions

File diff suppressed because one or more lines are too long

View File

@@ -113,6 +113,8 @@ class Item(LoggedModel):
:type allow_cancel: bool
:param max_per_order: Maximum number of times this item can be in an order. None for unlimited.
:type max_per_order: int
:param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
:type min_per_order: int
"""
event = models.ForeignKey(
@@ -205,6 +207,12 @@ class Item(LoggedModel):
'canceled by the user until the order is paid for. Users cannot cancel paid orders on their own '
'and you can cancel orders at all times, regardless of this setting')
)
min_per_order = models.IntegerField(
verbose_name=_('Minimum amount per order'),
null=True, blank=True,
help_text=_('This product can only be bought if it is added to the cart at least this many times. If you keep '
'the field empty or set it to 0, there is no special limit for this product.')
)
max_per_order = models.IntegerField(
verbose_name=_('Maximum amount per order'),
null=True, blank=True,

View File

@@ -34,6 +34,9 @@ error_messages = {
'the quantity you selected. Please see below for details.'),
'max_items': _("You cannot select more than %s items per order."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."),
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."),
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The presale period for this event has not yet started.'),
'ended': _('The presale period has ended.'),
'price_too_high': _('The entered price is to high.'),
@@ -74,7 +77,7 @@ class CartManager:
def positions(self):
return CartPosition.objects.filter(
Q(cart_id=self.cart_id) & Q(event=self.event)
)
).select_related('item')
def _calculate_expiry(self):
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
@@ -133,7 +136,7 @@ class CartManager:
raise CartError(error_messages['voucher_invalid_item'])
if isinstance(op, self.AddOperation):
if op.item.max_per_order:
if op.item.max_per_order or op.item.min_per_order:
new_total = (
len([1 for p in self.positions if p.item_id == op.item.pk]) +
sum([_op.count for _op in self._operations
@@ -143,13 +146,21 @@ class CartManager:
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
)
if new_total > op.item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': op.item.max_per_order,
'product': op.item.name
}
)
if op.item.max_per_order and new_total > op.item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': op.item.max_per_order,
'product': op.item.name
}
)
if op.item.min_per_order and new_total < op.item.min_per_order:
raise CartError(
_(error_messages['min_items_per_product']) % {
'min': op.item.min_per_order,
'product': op.item.name
}
)
def _get_price(self, item: Item, variation: Optional[ItemVariation],
voucher: Optional[Voucher], custom_price: Optional[Decimal]):
@@ -298,12 +309,47 @@ class CartManager:
return vouchers_ok
def _check_min_per_product(self):
per_product = Counter()
min_per_product = {}
for p in self.positions:
per_product[p.item_id] += 1
min_per_product[p.item.pk] = p.item.min_per_order
for op in self._operations:
if isinstance(op, self.AddOperation):
per_product[op.item.pk] += op.count
min_per_product[op.item.pk] = op.item.min_per_order
elif isinstance(op, self.RemoveOperation):
per_product[op.position.item_id] -= 1
min_per_product[op.position.item.pk] = op.position.item.min_per_order
err = None
for itemid, num in per_product.items():
min_p = min_per_product[itemid]
if min_p and num < min_p:
self._operations = [o for o in self._operations if not (
isinstance(o, self.AddOperation) and o.item.pk == itemid
)]
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
for p in self.positions:
if p.item_id == itemid and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['min_items_per_product_removed']) % {
'min': min_p,
'product': p.item.name
}
return err
def _perform_operations(self):
vouchers_ok = self._get_voucher_availability()
quotas_ok = self._get_quota_availability()
err = None
new_cart_positions = []
err = err or self._check_min_per_product()
self._operations.sort(key=lambda a: self.order[type(a)])
for op in self._operations:

View File

@@ -168,7 +168,8 @@ class ItemUpdateForm(I18nModelForm):
'require_voucher',
'hide_without_voucher',
'allow_cancel',
'max_per_order'
'max_per_order',
'min_per_order',
]
widgets = {
'available_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),

View File

@@ -26,6 +26,7 @@
{% bootstrap_field form.available_from layout="horizontal" %}
{% bootstrap_field form.available_until layout="horizontal" %}
{% bootstrap_field form.max_per_order layout="horizontal" %}
{% bootstrap_field form.min_per_order layout="horizontal" %}
{% bootstrap_field form.require_voucher layout="horizontal" %}
{% bootstrap_field form.hide_without_voucher layout="horizontal" %}
{% bootstrap_field form.allow_cancel layout="horizontal" %}

View File

@@ -112,6 +112,13 @@
</a>
{% if item.description %}<p>{{ item.description|localize|rich_text }}</p>
{% endif %}
{% if item.min_per_order %}
<p><small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small></p>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.min_price != item.max_price or item.free_price %}
@@ -207,6 +214,13 @@
{% if event.settings.show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order %}
<p><small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small></p>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}