Source code for oscar.apps.offer.conditions

from decimal import ROUND_UP
from decimal import Decimal as D

from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext

from oscar.core.loading import get_classes, get_model
from oscar.templatetags.currency_filters import currency

Condition = get_model('offer', 'Condition')
range_anchor, unit_price = get_classes('offer.utils', ['range_anchor', 'unit_price'])

__all__ = [
    'CountCondition', 'CoverageCondition', 'ValueCondition'
]


[docs]class CountCondition(Condition): """ An offer condition dependent on the NUMBER of matching items from the basket. """ _description = _("Basket includes %(count)d item(s) from %(range)s") @property def name(self): return self._description % { 'count': self.value, 'range': str(self.range).lower()} @property def description(self): return self._description % { 'count': self.value, 'range': range_anchor(self.range)} class Meta: app_label = 'offer' proxy = True verbose_name = _("Count condition") verbose_name_plural = _("Count conditions")
[docs] def is_satisfied(self, offer, basket): """ Determines whether a given basket meets this condition """ num_matches = 0 for line in basket.all_lines(): if self.can_apply_condition(line): num_matches += line.quantity_without_offer_discount(offer) if num_matches >= self.value: return True return False
def _get_num_matches(self, basket, offer): if hasattr(self, '_num_matches'): return getattr(self, '_num_matches') num_matches = 0 for line in basket.all_lines(): if self.can_apply_condition(line): num_matches += line.quantity_available_for_offer(offer) self._num_matches = num_matches return num_matches
[docs] def is_partially_satisfied(self, offer, basket): num_matches = self._get_num_matches(basket, offer) return 0 < num_matches < self.value
def get_upsell_message(self, offer, basket): num_matches = self._get_num_matches(basket, offer) delta = self.value - num_matches if delta > 0: return ngettext( 'Buy %(delta)d more product from %(range)s', 'Buy %(delta)d more products from %(range)s', int(delta) ) % {'delta': delta, 'range': self.range}
[docs] def consume_items(self, offer, basket, affected_lines): """ Marks items within the basket lines as consumed so they can't be reused in other offers. :basket: The basket :affected_lines: The lines that have been affected by the discount. This should be list of tuples (line, discount, qty) """ # We need to count how many items have already been consumed as part of # applying the benefit, so we don't consume too many items. num_consumed = 0 for line, __, quantity in affected_lines: num_consumed += quantity to_consume = max(0, self.value - num_consumed) if to_consume == 0: return for __, line in self.get_applicable_lines(offer, basket, most_expensive_first=True): num_consumed = line.consume(to_consume, offer=offer) to_consume -= num_consumed if to_consume == 0: break
[docs]class CoverageCondition(Condition): """ An offer condition dependent on the number of DISTINCT matching items from the basket. """ _description = _("Basket includes %(count)d distinct item(s) from" " %(range)s") @property def name(self): return self._description % { 'count': self.value, 'range': str(self.range).lower()} @property def description(self): return self._description % { 'count': self.value, 'range': range_anchor(self.range)} class Meta: app_label = 'offer' proxy = True verbose_name = _("Coverage Condition") verbose_name_plural = _("Coverage Conditions")
[docs] def is_satisfied(self, offer, basket): """ Determines whether a given basket meets this condition """ covered_ids = [] for line in basket.all_lines(): if not line.is_available_for_offer_discount(offer): continue product = line.product if (self.can_apply_condition(line) and product.id not in covered_ids): covered_ids.append(product.id) if len(covered_ids) >= self.value: return True return False
def _get_num_covered_products(self, basket, offer): covered_ids = set() for line in basket.all_lines(): product = line.product if self.can_apply_condition(line) and line.quantity_available_for_offer(offer) > 0: covered_ids.add(product.id) return len(covered_ids) def get_upsell_message(self, offer, basket): delta = self.value - self._get_num_covered_products(basket, offer) if delta > 0: return ngettext( 'Buy %(delta)d more product from %(range)s', 'Buy %(delta)d more products from %(range)s', int(delta) ) % {'delta': delta, 'range': self.range}
[docs] def is_partially_satisfied(self, offer, basket): return 0 < self._get_num_covered_products(basket, offer) < self.value
[docs] def consume_items(self, offer, basket, affected_lines): """ Marks items within the basket lines as consumed so they can't be reused in other offers. """ # Determine products that have already been consumed by applying the # benefit consumed_products = [] for line, __, quantity in affected_lines: consumed_products.append(line.product) to_consume = max(0, self.value - len(consumed_products)) if to_consume == 0: return for line in basket.all_lines(): product = line.product if not self.can_apply_condition(line): continue if product in consumed_products: continue if not line.is_available_for_offer_discount(offer): continue # Only consume a quantity of 1 from each line line.consume(1, offer=offer) consumed_products.append(product) to_consume -= 1 if to_consume == 0: break
def get_value_of_satisfying_items(self, offer, basket): covered_ids = [] value = D('0.00') for line in basket.all_lines(): if (self.can_apply_condition(line) and line.product.id not in covered_ids): covered_ids.append(line.product.id) value += unit_price(offer, line) if len(covered_ids) >= self.value: return value return value
[docs]class ValueCondition(Condition): """ An offer condition dependent on the VALUE of matching items from the basket. """ _description = _("Basket includes %(amount)s from %(range)s") @property def name(self): return self._description % { 'amount': currency(self.value), 'range': str(self.range).lower()} @property def description(self): return self._description % { 'amount': currency(self.value), 'range': range_anchor(self.range)} class Meta: app_label = 'offer' proxy = True verbose_name = _("Value condition") verbose_name_plural = _("Value conditions")
[docs] def is_satisfied(self, offer, basket): """ Determine whether a given basket meets this condition """ value_of_matches = D('0.00') for line in basket.all_lines(): if (self.can_apply_condition(line) and line.quantity_without_offer_discount(offer) > 0): price = unit_price(offer, line) value_of_matches += price * int( line.quantity_without_offer_discount(offer) ) if value_of_matches >= self.value: return True return False
def _get_value_of_matches(self, offer, basket): if hasattr(self, '_value_of_matches'): return getattr(self, '_value_of_matches') value_of_matches = D('0.00') for line in basket.all_lines(): if self.can_apply_condition(line): price = unit_price(offer, line) value_of_matches += price * int(line.quantity_available_for_offer(offer)) self._value_of_matches = value_of_matches return value_of_matches
[docs] def is_partially_satisfied(self, offer, basket): value_of_matches = self._get_value_of_matches(offer, basket) return D('0.00') < value_of_matches < self.value
def get_upsell_message(self, offer, basket): value_of_matches = self._get_value_of_matches(offer, basket) delta = self.value - value_of_matches if delta > 0: return _('Spend %(value)s more from %(range)s') % { 'value': currency(delta, basket.currency), 'range': self.range, }
[docs] def consume_items(self, offer, basket, affected_lines): """ Marks items within the basket lines as consumed so they can't be reused in other offers. We allow lines to be passed in as sometimes we want them sorted in a specific order. """ # Determine value of items already consumed as part of discount value_consumed = D('0.00') for line, __, qty in affected_lines: price = unit_price(offer, line) value_consumed += price * qty to_consume = max(0, self.value - value_consumed) if to_consume == 0: return for price, line in self.get_applicable_lines( offer, basket, most_expensive_first=True): quantity_to_consume = min( line.quantity_without_offer_discount(offer), (to_consume / price).quantize(D(1), ROUND_UP)) line.consume(quantity_to_consume, offer=offer) to_consume -= price * quantity_to_consume if to_consume <= 0: break