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.

strategy.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from collections import namedtuple
  2. from decimal import Decimal as D
  3. from . import availability, prices
  4. # A container for policies
  5. PurchaseInfo = namedtuple(
  6. 'PurchaseInfo', ['price', 'availability', 'stockrecord'])
  7. class Selector(object):
  8. """
  9. Responsible for returning the appropriate strategy class for a given
  10. user/session.
  11. This can be called in three ways:
  12. #) Passing a request and user. This is for determining
  13. prices/availability for a normal user browsing the site.
  14. #) Passing just the user. This is for offline processes that don't
  15. have a request instance but do know which user to determine prices for.
  16. #) Passing nothing. This is for offline processes that don't
  17. correspond to a specific user. Eg, determining a price to store in
  18. a search index.
  19. """
  20. def strategy(self, request=None, user=None, **kwargs):
  21. """
  22. Return an instanticated strategy instance
  23. """
  24. # Default to the backwards-compatible strategy of picking the first
  25. # stockrecord but charging zero tax.
  26. return Default(request)
  27. class Base(object):
  28. """
  29. The base strategy class
  30. Given a product, strategies are responsible for returning a
  31. ``PurchaseInfo`` instance which contains:
  32. - The appropriate stockrecord for this customer
  33. - A pricing policy instance
  34. - An availability policy instance
  35. """
  36. def __init__(self, request=None):
  37. self.request = request
  38. self.user = None
  39. if request and request.user.is_authenticated():
  40. self.user = request.user
  41. def fetch_for_product(self, product, stockrecord=None):
  42. """
  43. Given a product, return a ``PurchaseInfo`` instance.
  44. The ``PurchaseInfo`` class is a named tuple with attributes:
  45. - ``price``: a pricing policy object.
  46. - ``availability``: an availability policy object.
  47. - ``stockrecord``: the stockrecord that is being used
  48. If a stockrecord is passed, return the appropriate ``PurchaseInfo``
  49. instance for that product and stockrecord is returned.
  50. """
  51. raise NotImplementedError(
  52. "A strategy class must define a fetch_for_product method "
  53. "for returning the availability and pricing "
  54. "information."
  55. )
  56. def fetch_for_group(self, product):
  57. """
  58. Given a group product, fetch a ``StockInfo`` instance
  59. """
  60. raise NotImplementedError(
  61. "A strategy class must define a fetch_for_group method "
  62. "for returning the availability and pricing "
  63. "information."
  64. )
  65. def fetch_for_line(self, line, stockrecord=None):
  66. """
  67. Given a basket line instance, fetch a ``PurchaseInfo`` instance.
  68. This method is provided to allow purchase info to be determined using a
  69. basket line's attributes. For instance, "bundle" products often use
  70. basket line attributes to store SKUs of contained products. For such
  71. products, we need to look at the availability of each contained product
  72. to determine overall availability.
  73. """
  74. # Default to ignoring any basket line options as we don't know what to
  75. # do with them within Oscar - that's up to your project to implement.
  76. return self.fetch_for_product(line.product)
  77. class Structured(Base):
  78. """
  79. A strategy class which provides separate, overridable methods for
  80. determining the 3 things that a ``PurchaseInfo`` instance requires:
  81. #) A stockrecord
  82. #) A pricing policy
  83. #) An availability policy
  84. """
  85. def fetch_for_product(self, product, stockrecord=None):
  86. """
  87. Return the appropriate ``PurchaseInfo`` instance.
  88. This method is not intended to be overridden.
  89. """
  90. if stockrecord is None:
  91. stockrecord = self.select_stockrecord(product)
  92. return PurchaseInfo(
  93. price=self.pricing_policy(product, stockrecord),
  94. availability=self.availability_policy(product, stockrecord),
  95. stockrecord=stockrecord)
  96. def fetch_for_group(self, product):
  97. # Select variants and associated stockrecords
  98. variant_stock = self.select_variant_stockrecords(product)
  99. return PurchaseInfo(
  100. price=self.group_pricing_policy(product, variant_stock),
  101. availability=self.group_availability_policy(
  102. product, variant_stock),
  103. stockrecord=None)
  104. def select_stockrecord(self, product):
  105. """
  106. Select the appropriate stockrecord
  107. """
  108. raise NotImplementedError(
  109. "A structured strategy class must define a "
  110. "'select_stockrecord' method")
  111. def select_variant_stockrecords(self, product):
  112. """
  113. Select appropriate stock record for all variants of a product
  114. """
  115. records = []
  116. for variant in product.variants.all():
  117. records.append((variant, self.select_stockrecord(variant)))
  118. return records
  119. def pricing_policy(self, product, stockrecord):
  120. """
  121. Return the appropriate pricing policy
  122. """
  123. raise NotImplementedError(
  124. "A structured strategy class must define a "
  125. "'pricing_policy' method")
  126. def availability_policy(self, product, stockrecord):
  127. """
  128. Return the appropriate availability policy
  129. """
  130. raise NotImplementedError(
  131. "A structured strategy class must define a "
  132. "'availability_policy' method")
  133. # Mixins - these can be used to construct the appropriate strategy class
  134. class UseFirstStockRecord(object):
  135. """
  136. Stockrecord selection mixin for use with the ``Structured`` base strategy.
  137. This mixin picks the first (normally only) stockrecord to fulfil a product.
  138. This is backwards compatible with Oscar<0.6 where only one stockrecord per
  139. product was permitted.
  140. """
  141. def select_stockrecord(self, product):
  142. try:
  143. return product.stockrecords.all()[0]
  144. except IndexError:
  145. return None
  146. class StockRequired(object):
  147. """
  148. Availability policy mixin for use with the ``Structured`` base strategy.
  149. This mixin ensures that a product can only be bought if it has stock
  150. available (if stock is being tracked).
  151. """
  152. def availability_policy(self, product, stockrecord):
  153. if not stockrecord:
  154. return availability.Unavailable()
  155. if not product.get_product_class().track_stock:
  156. return availability.Available()
  157. else:
  158. return availability.StockRequired(
  159. stockrecord.net_stock_level)
  160. def group_availability_policy(self, product, variant_stock):
  161. # A parent product is available if one of its variants is
  162. for variant, stockrecord in variant_stock:
  163. policy = self.availability_policy(product, stockrecord)
  164. if policy.is_available_to_buy:
  165. return availability.Available()
  166. return availability.Unavailable()
  167. class NoTax(object):
  168. """
  169. Pricing policy mixin for use with the ``Structured`` base strategy.
  170. This mixin specifies zero tax and uses the ``price_excl_tax`` from the
  171. stockrecord.
  172. """
  173. def pricing_policy(self, product, stockrecord):
  174. if not stockrecord:
  175. return prices.Unavailable()
  176. return prices.FixedPrice(
  177. currency=stockrecord.price_currency,
  178. excl_tax=stockrecord.price_excl_tax,
  179. tax=D('0.00'))
  180. def group_pricing_policy(self, product, variant_stock):
  181. stockrecords = [x[1] for x in variant_stock if x[1] is not None]
  182. if not stockrecords:
  183. return prices.Unavailable()
  184. # We take price from first record
  185. stockrecord = stockrecords[0]
  186. return prices.FixedPrice(
  187. currency=stockrecord.price_currency,
  188. excl_tax=stockrecord.price_excl_tax,
  189. tax=D('0.00'))
  190. class FixedRateTax(object):
  191. """
  192. Pricing policy mixin for use with the ``Structured`` base strategy. This
  193. mixin applies a fixed rate tax to the base price from the product's
  194. stockrecord. The price_incl_tax is quantized to two decimal places.
  195. Rounding behaviour is Decimal's default
  196. """
  197. rate = D('0') # Subclass and specify the correct rate
  198. exponent = D('0.01') # Default to two decimal places
  199. def pricing_policy(self, product, stockrecord):
  200. if not stockrecord:
  201. return prices.Unavailable()
  202. tax = (stockrecord.price_excl_tax * self.rate).quantize(self.exponent)
  203. return prices.TaxInclusiveFixedPrice(
  204. currency=stockrecord.price_currency,
  205. excl_tax=stockrecord.price_excl_tax,
  206. tax=tax)
  207. class DeferredTax(object):
  208. """
  209. Pricing policy mixin for use with the ``Structured`` base strategy.
  210. This mixin does not specify the product tax and is suitable to territories
  211. where tax isn't known until late in the checkout process.
  212. """
  213. def pricing_policy(self, product, stockrecord):
  214. if not stockrecord:
  215. return prices.Unavailable()
  216. return prices.FixedPrice(
  217. currency=stockrecord.price_currency,
  218. excl_tax=stockrecord.price_excl_tax)
  219. # Example strategy composed of above mixins. For real projects, it's likely
  220. # you'll want to use a different pricing mixin as you'll probably want to
  221. # charge tax!
  222. class Default(UseFirstStockRecord, StockRequired, NoTax, Structured):
  223. """
  224. Default stock/price strategy that uses the first found stockrecord for a
  225. product, ensures that stock is available (unless the product class
  226. indicates that we don't need to track stock) and charges zero tax.
  227. """
  228. class UK(UseFirstStockRecord, StockRequired, FixedRateTax, Structured):
  229. """
  230. Sample strategy for the UK that:
  231. - uses the first stockrecord for each product (effectively assuming
  232. there is only one).
  233. - requires that a product has stock available to be bought
  234. - applies a fixed rate of tax on all products
  235. This is just a sample strategy used for internal development. It is not
  236. recommended to be used in production, especially as the tax rate is
  237. hard-coded.
  238. """
  239. # Use UK VAT rate (as of December 2013)
  240. rate = D('0.20')
  241. class US(UseFirstStockRecord, StockRequired, DeferredTax, Structured):
  242. """
  243. Sample strategy for the US.
  244. - uses the first stockrecord for each product (effectively assuming
  245. there is only one).
  246. - requires that a product has stock available to be bought
  247. - doesn't apply a tax to product prices (normally this will be done
  248. after the shipping address is entered).
  249. This is just a sample one used for internal development. It is not
  250. recommended to be used in production.
  251. """