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.

abstract_models.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. from decimal import Decimal
  2. import datetime
  3. from django.contrib.auth.models import User
  4. from django.db import models
  5. from django.utils.translation import ugettext as _
  6. from django.core.exceptions import ValidationError
  7. from oscar.apps.offer.managers import ActiveOfferManager
  8. SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
  9. class AbstractConditionalOffer(models.Model):
  10. u"""
  11. A conditional offer (eg buy 1, get 10% off)
  12. """
  13. name = models.CharField(max_length=128)
  14. description = models.TextField(blank=True, null=True)
  15. # Offers come in a few different types:
  16. # (a) Offers that are available to all customers on the site. Eg a
  17. # 3-for-2 offer.
  18. # (b) Offers that are linked to a voucher, and only become available once
  19. # that voucher has been applied to the basket
  20. # (c) Offers that are linked to a user. Eg, all students get 10% off. The code
  21. # to apply this offer needs to be coded
  22. # (d) Session offers - these are temporarily available to a user after some trigger
  23. # event. Eg, users coming from some affiliate site get 10% off.
  24. TYPE_CHOICES = (
  25. (SITE, "Site offer - available to all users"),
  26. (VOUCHER, "Voucher offer - only available after entering the appropriate voucher code"),
  27. (USER, "User offer - available to certain types of user"),
  28. (SESSION, "Session offer - temporary offer, available for a user for the duration of their session"),
  29. )
  30. offer_type = models.CharField(_("Type"), choices=TYPE_CHOICES, default=SITE, max_length=128)
  31. condition = models.ForeignKey('offer.Condition')
  32. benefit = models.ForeignKey('offer.Benefit')
  33. # Range of availability. Note that if this is a voucher offer, then these
  34. # dates are ignored and only the dates from the voucher are used to determine
  35. # availability.
  36. start_date = models.DateField(blank=True, null=True)
  37. end_date = models.DateField(blank=True, null=True)
  38. # Some complicated situations require offers to be applied in a set order.
  39. priority = models.IntegerField(default=0, help_text="The highest priority offers are applied first")
  40. # We track some information on usage
  41. total_discount = models.DecimalField(decimal_places=2, max_digits=12, default=Decimal('0.00'))
  42. date_created = models.DateTimeField(auto_now_add=True)
  43. objects = models.Manager()
  44. active = ActiveOfferManager()
  45. # We need to track the voucher that this offer came from (if it is a voucher offer)
  46. _voucher = None
  47. class Meta:
  48. ordering = ['-priority']
  49. abstract = True
  50. def __unicode__(self):
  51. return self.name
  52. def is_active(self, test_date=None):
  53. if not test_date:
  54. test_date = datetime.date.today()
  55. return self.start_date <= test_date and test_date < self.end_date
  56. def is_condition_satisfied(self, basket):
  57. return self._proxy_condition().is_satisfied(basket)
  58. def apply_benefit(self, basket):
  59. u"""
  60. Applies the benefit to the given basket and returns the discount.
  61. """
  62. if not self.is_condition_satisfied(basket):
  63. return Decimal('0.00')
  64. return self._proxy_benefit().apply(basket, self._proxy_condition())
  65. def set_voucher(self, voucher):
  66. self._voucher = voucher
  67. def get_voucher(self):
  68. return self._voucher
  69. def _proxy_condition(self):
  70. u"""
  71. Returns the appropriate proxy model for the condition
  72. """
  73. return self.condition
  74. def _proxy_benefit(self):
  75. u"""
  76. Returns the appropriate proxy model for the benefit
  77. """
  78. return self.benefit
  79. class AbstractCondition(models.Model):
  80. COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage")
  81. TYPE_CHOICES = (
  82. (COUNT, _("Depends on number of items in basket that are in condition range")),
  83. (VALUE, _("Depends on value of items in basket that are in condition range")),
  84. (COVERAGE, _("Needs to contain a set number of DISTINCT items from the condition range"))
  85. )
  86. range = models.ForeignKey('offer.Range')
  87. type = models.CharField(max_length=128, choices=TYPE_CHOICES)
  88. value = models.DecimalField(decimal_places=2, max_digits=12)
  89. class Meta:
  90. abstract = True
  91. def __unicode__(self):
  92. if self.type == self.COUNT:
  93. return u"Basket includes %d item(s) from %s" % (self.value, str(self.range).lower())
  94. elif self.type == self.COVERAGE:
  95. return u"Basket includes %d distinct products from %s" % (self.value, str(self.range).lower())
  96. return u"Basket includes %.2f value from %s" % (self.value, str(self.range).lower())
  97. def consume_items(self, basket):
  98. pass
  99. def is_satisfied(self, basket):
  100. """
  101. Determines whether a given basket meets this condition. This is
  102. stubbed in this top-class object. The subclassing proxies are
  103. responsible for implementing it correctly.
  104. """
  105. return False
  106. class AbstractBenefit(models.Model):
  107. PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price")
  108. TYPE_CHOICES = (
  109. (PERCENTAGE, _("Discount is a % of the product's value")),
  110. (FIXED, _("Discount is a fixed amount off the product's value")),
  111. (MULTIBUY, _("Discount is to give the cheapest product for free")),
  112. (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")),
  113. )
  114. range = models.ForeignKey('offer.Range', null=True, blank=True)
  115. type = models.CharField(max_length=128, choices=TYPE_CHOICES)
  116. value = models.DecimalField(decimal_places=2, max_digits=12)
  117. # If this is not set, then there is no upper limit on how many products
  118. # can be discounted by this benefit.
  119. max_affected_items = models.PositiveIntegerField(blank=True, null=True, help_text="""Set this
  120. to prevent the discount consuming all items within the range that are in the basket.""")
  121. class Meta:
  122. abstract = True
  123. def __unicode__(self):
  124. if self.type == self.PERCENTAGE:
  125. desc = u"%s%% discount on %s" % (self.value, str(self.range).lower())
  126. elif self.type == self.MULTIBUY:
  127. desc = u"Cheapest product is free from %s" % str(self.range)
  128. elif self.type == self.FIXED_PRICE:
  129. desc = u"The products that meet the condition are sold for %s" % self.value
  130. else:
  131. desc = u"%.2f discount on %s" % (self.value, str(self.range).lower())
  132. if self.max_affected_items == 1:
  133. desc += u" (max 1 item)"
  134. elif self.max_affected_items > 1:
  135. desc += u" (max %d items)" % self.max_affected_items
  136. return desc
  137. def apply(self, basket, condition=None):
  138. return Decimal('0.00')
  139. def clean(self):
  140. # All benefits need a range apart from FIXED_PRICE
  141. if self.type != self.FIXED_PRICE and not self.range:
  142. raise ValidationError("Benefits of type %s need a range" % self.type)
  143. class AbstractRange(models.Model):
  144. u"""
  145. Represents a range of products that can be used within an offer
  146. """
  147. name = models.CharField(_("Name"), max_length=128)
  148. includes_all_products = models.BooleanField(default=False)
  149. included_products = models.ManyToManyField('product.Item', related_name='includes', blank=True)
  150. excluded_products = models.ManyToManyField('product.Item', related_name='excludes', blank=True)
  151. classes = models.ManyToManyField('product.ItemClass', related_name='classes', blank=True)
  152. __included_product_ids = None
  153. __excluded_product_ids = None
  154. __class_ids = None
  155. class Meta:
  156. abstract = True
  157. def __unicode__(self):
  158. return self.name
  159. def contains_product(self, product):
  160. excluded_product_ids = self._excluded_product_ids()
  161. if product.id in excluded_product_ids:
  162. return False
  163. if self.includes_all_products:
  164. return True
  165. if product.item_class_id in self._class_ids():
  166. return True
  167. included_product_ids = self._included_product_ids()
  168. return product.id in included_product_ids
  169. def _included_product_ids(self):
  170. if None == self.__included_product_ids:
  171. self.__included_product_ids = [row['id'] for row in self.included_products.values('id')]
  172. return self.__included_product_ids
  173. def _excluded_product_ids(self):
  174. if None == self.__excluded_product_ids:
  175. self.__excluded_product_ids = [row['id'] for row in self.excluded_products.values('id')]
  176. return self.__excluded_product_ids
  177. def _class_ids(self):
  178. if None == self.__class_ids:
  179. self.__class_ids = [row['id'] for row in self.classes.values('id')]
  180. return self.__class_ids
  181. class AbstractVoucher(models.Model):
  182. u"""
  183. A voucher. This is simply a link to a collection of offers
  184. Note that there are three possible "usage" models:
  185. (a) Single use
  186. (b) Multi-use
  187. (c) Once per customer
  188. """
  189. name = models.CharField(_("Name"), max_length=128,
  190. help_text="""This will be shown in the checkout and basket once the voucher is entered""")
  191. code = models.CharField(_("Code"), max_length=128, db_index=True, unique=True,
  192. help_text="""Case insensitive / No spaces allowed""")
  193. offers = models.ManyToManyField('offer.ConditionalOFfer', related_name='vouchers',
  194. limit_choices_to={'offer_type': VOUCHER})
  195. SINGLE_USE, MULTI_USE, ONCE_PER_CUSTOMER = ('Single use', 'Multi-use', 'Once per customer')
  196. USAGE_CHOICES = (
  197. (SINGLE_USE, "Can only be used by one customer"),
  198. (MULTI_USE, "Can only be used any number of times"),
  199. (ONCE_PER_CUSTOMER, "Can be used once by each customer"),
  200. )
  201. usage = models.CharField(_("Usage"), max_length=128, choices=USAGE_CHOICES, default=MULTI_USE)
  202. start_date = models.DateField()
  203. end_date = models.DateField()
  204. # Summary information
  205. num_basket_additions = models.PositiveIntegerField(default=0)
  206. num_orders = models.PositiveIntegerField(default=0)
  207. total_discount = models.DecimalField(decimal_places=2, max_digits=12, default=Decimal('0.00'))
  208. date_created = models.DateField(auto_now_add=True)
  209. class Meta:
  210. abstract = True
  211. get_latest_by = 'date_created'
  212. def __unicode__(self):
  213. return self.name
  214. def save(self, *args, **kwargs):
  215. self.code = self.code.upper()
  216. super(AbstractVoucher, self).save(*args, **kwargs)
  217. def is_active(self, test_date=None):
  218. u"""
  219. Tests whether this voucher is currently active.
  220. """
  221. if not test_date:
  222. test_date = datetime.date.today()
  223. return self.start_date <= test_date and test_date < self.end_date
  224. def is_available_to_user(self, user=None):
  225. u"""
  226. Tests whether this voucher is available to the passed user.
  227. Returns a tuple of a boolean for whether it is successulf, and a message
  228. """
  229. is_available, message = False, ''
  230. if self.usage == self.SINGLE_USE:
  231. is_available = self.applications.count() == 0
  232. if not is_available:
  233. message = "This voucher has already been used"
  234. elif self.usage == self.MULTI_USE:
  235. is_available = True
  236. elif self.usage == self.ONCE_PER_CUSTOMER:
  237. if not user.is_authenticated():
  238. is_available = False
  239. message = "This voucher is only available to signed in users"
  240. else:
  241. is_available = self.applications.filter(voucher=self, user=user).count() == 0
  242. if not is_available:
  243. message = "You have already used this voucher in a previous order"
  244. return is_available, message
  245. def record_usage(self, order, user):
  246. u"""
  247. Records a usage of this voucher in an order.
  248. """
  249. self.applications.create(voucher=self, order=order, user=user)
  250. class AbstractVoucherApplication(models.Model):
  251. u"""
  252. For tracking how often a voucher has been used
  253. """
  254. voucher = models.ForeignKey('offer.Voucher', related_name="applications")
  255. # It is possible for an anonymous user to apply a voucher so we need to allow
  256. # the user to be nullable
  257. user = models.ForeignKey('auth.User', blank=True, null=True)
  258. order = models.ForeignKey('order.Order')
  259. date_created = models.DateField(auto_now_add=True)
  260. class Meta:
  261. abstract = True
  262. def __unicode__(self):
  263. return u"'%s' used by '%s'" % (self.voucher, self.user)