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) # pylint: disable=W0201 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: # pylint: disable=W0612 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 # pylint: disable=W0201 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