You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

models.py 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. from decimal import Decimal
  2. import math
  3. from oscar.offer.abstract_models import (AbstractConditionalOffer, AbstractCondition,
  4. AbstractBenefit, AbstractRange, AbstractVoucher,
  5. AbstractVoucherApplication)
  6. class Condition(AbstractCondition):
  7. pass
  8. class CountCondition(Condition):
  9. u"""
  10. An offer condition dependent on the NUMBER of matching items from the basket.
  11. """
  12. class Meta:
  13. proxy = True
  14. def is_satisfied(self, basket):
  15. u"""Determines whether a given basket meets this condition"""
  16. num_matches = 0
  17. for line in basket.all_lines():
  18. if self.range.contains_product(line.product):
  19. num_matches += line.quantity
  20. if num_matches >= self.value:
  21. return True
  22. return False
  23. def consume_items(self, basket):
  24. u"""
  25. Marks items within the basket lines as consumed so they
  26. can't be reused in other offers.
  27. """
  28. num_consumed = 0
  29. for line in basket.all_lines():
  30. if self.range.contains_product(line.product):
  31. quantity_to_consume = min(line.quantity_without_discount, self.value - num_consumed)
  32. line.consume(quantity_to_consume)
  33. if num_consumed == self.value:
  34. return
  35. class ValueCondition(Condition):
  36. u"""
  37. An offer condition dependent on the VALUE of matching items from the basket.
  38. """
  39. price_field = 'price_incl_tax'
  40. class Meta:
  41. proxy = True
  42. def is_satisfied(self, basket):
  43. u"""Determines whether a given basket meets this condition"""
  44. value_of_matches = Decimal('0.00')
  45. for line in basket.all_lines():
  46. if self.range.contains_product(line.product) and line.product.has_stockrecord:
  47. price = getattr(line.product.stockrecord, self.price_field)
  48. value_of_matches += price * line.quantity
  49. if value_of_matches >= self.value:
  50. return True
  51. return False
  52. def consume_items(self, basket):
  53. u"""
  54. Marks items within the basket lines as consumed so they
  55. can't be reused in other offers.
  56. """
  57. value_of_matches = Decimal('0.00')
  58. for line in basket.all_lines():
  59. if self.range.contains_product(line.product) and line.product.has_stockrecord:
  60. price = getattr(line.product.stockrecord, self.price_field)
  61. quantity_to_consume = min(line.quantity_without_discount,
  62. math.floor((self.value - value_of_matches)/price))
  63. value_of_matches += price * int(quantity_to_consume)
  64. line.consume(quantity_to_consume)
  65. if value_of_matches >= self.value:
  66. return
  67. # ========
  68. # Benefits
  69. # ========
  70. class Benefit(AbstractBenefit):
  71. price_field = 'price_incl_tax'
  72. def _effective_max_affected_items(self):
  73. max_affected_items = self.max_affected_items
  74. if not self.max_affected_items:
  75. max_affected_items = 10000
  76. return max_affected_items
  77. class PercentageDiscountBenefit(Benefit):
  78. u"""
  79. An offer benefit that gives a percentage discount
  80. """
  81. class Meta:
  82. proxy = True
  83. def apply(self, basket, condition=None):
  84. discount = Decimal('0.00')
  85. affected_items = 0
  86. max_affected_items = self._effective_max_affected_items()
  87. for line in basket.all_lines():
  88. if affected_items >= max_affected_items:
  89. break
  90. if self.range.contains_product(line.product) and line.product.has_stockrecord:
  91. price = getattr(line.product.stockrecord, self.price_field)
  92. quantity = min(line.quantity_without_discount,
  93. max_affected_items - affected_items)
  94. discount += self.value/100 * price * quantity
  95. affected_items += quantity
  96. line.discount(discount, quantity)
  97. return discount
  98. class AbsoluteDiscountBenefit(Benefit):
  99. u"""
  100. An offer benefit that gives an absolute discount
  101. """
  102. class Meta:
  103. proxy = True
  104. def apply(self, basket, condition=None):
  105. discount = Decimal('0.00')
  106. affected_items = 0
  107. max_affected_items = self._effective_max_affected_items()
  108. for line in basket.all_lines():
  109. if affected_items >= max_affected_items:
  110. break
  111. if self.range.contains_product(line.product) and line.product.has_stockrecord:
  112. price = getattr(line.product.stockrecord, self.price_field)
  113. remaining_discount = self.value - discount
  114. quantity = min(line.quantity_without_discount,
  115. max_affected_items - affected_items,
  116. math.floor(remaining_discount / price))
  117. discount += price * Decimal(str(quantity))
  118. affected_items += quantity
  119. line.discount(discount, quantity)
  120. return discount
  121. class MultibuyDiscountBenefit(Benefit):
  122. class Meta:
  123. proxy = True
  124. def apply(self, basket, condition=True):
  125. # We want cheapest item not in an offer and that becomes the discount
  126. discount = Decimal('0.00')
  127. line = self._get_cheapest_line(basket)
  128. if line:
  129. discount = getattr(line.product.stockrecord, self.price_field)
  130. line.discount(discount, 1)
  131. return discount
  132. def _get_cheapest_line(self, basket):
  133. min_price = Decimal('10000.00')
  134. cheapest_line = None
  135. for line in basket.all_lines():
  136. if line.quantity_without_discount > 0 and getattr(line.product.stockrecord, self.price_field) < min_price:
  137. min_price = getattr(line.product.stockrecord, self.price_field)
  138. cheapest_line = line
  139. return cheapest_line
  140. class ConditionalOffer(AbstractConditionalOffer):
  141. def _proxy_condition(self):
  142. u"""
  143. Returns the appropriate proxy model for the condition
  144. """
  145. field_dict = self.condition.__dict__
  146. if '_state' in field_dict:
  147. del field_dict['_state']
  148. if self.condition.type == self.condition.COUNT:
  149. return CountCondition(**field_dict)
  150. elif self.condition.type == self.condition.VALUE:
  151. return ValueCondition(**field_dict)
  152. return self.condition
  153. def _proxy_benefit(self):
  154. u"""
  155. Returns the appropriate proxy model for the condition
  156. """
  157. field_dict = self.benefit.__dict__
  158. if '_state' in field_dict:
  159. del field_dict['_state']
  160. if self.benefit.type == self.benefit.PERCENTAGE:
  161. return PercentageDiscountBenefit(**field_dict)
  162. elif self.benefit.type == self.benefit.FIXED:
  163. return AbsoluteDiscountBenefit(**field_dict)
  164. elif self.benefit.type == self.benefit.MULTIBUY:
  165. return MultibuyDiscountBenefit(**field_dict)
  166. return self.benefit
  167. class Range(AbstractRange):
  168. pass
  169. class Voucher(AbstractVoucher):
  170. pass
  171. class VoucherApplication(AbstractVoucherApplication):
  172. pass
  173. # We need to import receivers at the bottom of this script
  174. from oscar.offer.receivers import receive_basket_voucher_change