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.

utils.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. from django.contrib.sites.models import Site
  2. from django.conf import settings
  3. from django.db.models import get_model
  4. from django.utils.translation import ugettext_lazy as _
  5. from oscar.apps.shipping.methods import Free
  6. from oscar.core.loading import get_class
  7. from . import exceptions
  8. from decimal import Decimal as D
  9. ShippingAddress = get_model('order', 'ShippingAddress')
  10. Order = get_model('order', 'Order')
  11. Line = get_model('order', 'Line')
  12. LinePrice = get_model('order', 'LinePrice')
  13. LineAttribute = get_model('order', 'LineAttribute')
  14. OrderDiscount = get_model('order', 'OrderDiscount')
  15. order_placed = get_class('order.signals', 'order_placed')
  16. class OrderNumberGenerator(object):
  17. """
  18. Simple object for generating order numbers.
  19. We need this as the order number is often required for payment
  20. which takes place before the order model has been created.
  21. """
  22. def order_number(self, basket):
  23. """
  24. Return an order number for a given basket
  25. """
  26. return 100000 + basket.id
  27. class OrderCreator(object):
  28. """
  29. Places the order by writing out the various models
  30. """
  31. def place_order(self, basket, total_incl_tax=None, total_excl_tax=None,
  32. user=None, shipping_method=None, shipping_address=None,
  33. billing_address=None, order_number=None, status=None, **kwargs):
  34. """
  35. Placing an order involves creating all the relevant models based on the
  36. basket and session data.
  37. """
  38. # Only a basket instance is required to place an order - everything else can be set
  39. # to defaults
  40. if basket.is_empty:
  41. raise ValueError(_("Empty baskets cannot be submitted"))
  42. if not shipping_method:
  43. shipping_method = Free()
  44. if total_incl_tax is None or total_excl_tax is None:
  45. total_incl_tax = basket.total_incl_tax + shipping_method.charge_incl_tax
  46. total_excl_tax = basket.total_excl_tax + shipping_method.charge_excl_tax
  47. if not order_number:
  48. generator = OrderNumberGenerator()
  49. order_number = generator.order_number(basket)
  50. if not status and hasattr(settings, 'OSCAR_INITIAL_ORDER_STATUS'):
  51. status = getattr(settings, 'OSCAR_INITIAL_ORDER_STATUS')
  52. try:
  53. Order._default_manager.get(number=order_number)
  54. except Order.DoesNotExist:
  55. pass
  56. else:
  57. raise ValueError(_("There is already an order with number %s") % order_number)
  58. # Ok - everything seems to be in order, let's place the order
  59. order = self.create_order_model(
  60. user, basket, shipping_address, shipping_method, billing_address,
  61. total_incl_tax, total_excl_tax, order_number, status, **kwargs)
  62. for line in basket.all_lines():
  63. self.create_line_models(order, line)
  64. self.update_stock_records(line)
  65. for application in basket.offer_applications:
  66. # Trigger any deferred benefits from offers and capture the
  67. # resulting message
  68. application['message'] = application['offer'].apply_deferred_benefit(basket)
  69. # Record offer application results
  70. if application['result'].affects_shipping:
  71. # Skip zero shipping discounts
  72. if shipping_method.discount <= D('0.00'):
  73. continue
  74. # If a shipping offer, we need to grab the actual discount off
  75. # the shipping method instance, which should be wrapped in an
  76. # OfferDiscount instance.
  77. application['discount'] = shipping_method.discount
  78. self.create_discount_model(order, application)
  79. self.record_discount(application)
  80. for voucher in basket.vouchers.all():
  81. self.record_voucher_usage(order, voucher, user)
  82. # Send signal for analytics to pick up
  83. order_placed.send(sender=self, order=order, user=user)
  84. return order
  85. def create_order_model(self, user, basket, shipping_address, shipping_method,
  86. billing_address, total_incl_tax, total_excl_tax,
  87. order_number, status, **extra_order_fields):
  88. """
  89. Creates an order model.
  90. """
  91. order_data = {'basket_id': basket.id,
  92. 'number': order_number,
  93. 'site': Site._default_manager.get_current(),
  94. 'total_incl_tax': total_incl_tax,
  95. 'total_excl_tax': total_excl_tax,
  96. 'shipping_incl_tax': shipping_method.charge_incl_tax,
  97. 'shipping_excl_tax': shipping_method.charge_excl_tax,
  98. 'shipping_method': shipping_method.name}
  99. if shipping_address:
  100. order_data['shipping_address'] = shipping_address
  101. if billing_address:
  102. order_data['billing_address'] = billing_address
  103. if user and user.is_authenticated():
  104. order_data['user_id'] = user.id
  105. if status:
  106. order_data['status'] = status
  107. if extra_order_fields:
  108. order_data.update(extra_order_fields)
  109. order = Order(**order_data)
  110. order.save()
  111. return order
  112. def create_line_models(self, order, basket_line, extra_line_fields=None):
  113. """
  114. Create the batch line model.
  115. You can set extra fields by passing a dictionary as the
  116. extra_line_fields value
  117. """
  118. product = basket_line.product
  119. stockrecord = basket_line.stockrecord
  120. if not stockrecord:
  121. raise exceptions.UnableToPlaceOrder(
  122. "Baket line #%d has no stockrecord" % basket_line.id)
  123. partner = stockrecord.partner
  124. line_data = {
  125. 'order': order,
  126. # Partner details
  127. 'partner': partner,
  128. 'partner_name': partner.name,
  129. 'partner_sku': stockrecord.partner_sku,
  130. 'stockrecord': stockrecord,
  131. # Product details
  132. 'product': product,
  133. 'title': product.get_title(),
  134. 'upc': product.upc,
  135. 'quantity': basket_line.quantity,
  136. # Price details
  137. 'line_price_excl_tax': basket_line.line_price_excl_tax_and_discounts,
  138. 'line_price_incl_tax': basket_line.line_price_incl_tax_and_discounts,
  139. 'line_price_before_discounts_excl_tax': basket_line.line_price_excl_tax,
  140. 'line_price_before_discounts_incl_tax': basket_line.line_price_incl_tax,
  141. # Reporting details
  142. 'unit_cost_price': stockrecord.cost_price,
  143. 'unit_price_incl_tax': basket_line.unit_price_incl_tax,
  144. 'unit_price_excl_tax': basket_line.unit_price_excl_tax,
  145. 'unit_retail_price': stockrecord.price_retail,
  146. # Shipping details
  147. 'est_dispatch_date': stockrecord.dispatch_date
  148. }
  149. extra_line_fields = extra_line_fields or {}
  150. if hasattr(settings, 'OSCAR_INITIAL_LINE_STATUS'):
  151. if not (extra_line_fields and 'status' in extra_line_fields):
  152. extra_line_fields['status'] = getattr(
  153. settings, 'OSCAR_INITIAL_LINE_STATUS')
  154. if extra_line_fields:
  155. line_data.update(extra_line_fields)
  156. order_line = Line._default_manager.create(**line_data)
  157. self.create_line_price_models(order, order_line, basket_line)
  158. self.create_line_attributes(order, order_line, basket_line)
  159. self.create_additional_line_models(order, order_line, basket_line)
  160. return order_line
  161. def update_stock_records(self, line):
  162. """
  163. Update any relevant stock records for this order line
  164. """
  165. if line.product.get_product_class().track_stock:
  166. line.stockrecord.allocate(line.quantity)
  167. def create_additional_line_models(self, order, order_line, basket_line):
  168. """
  169. Empty method designed to be overridden.
  170. Some applications require additional information about lines, this
  171. method provides a clean place to create additional models that
  172. relate to a given line.
  173. """
  174. pass
  175. def create_line_price_models(self, order, order_line, basket_line):
  176. """
  177. Creates the batch line price models
  178. """
  179. breakdown = basket_line.get_price_breakdown()
  180. for price_incl_tax, price_excl_tax, quantity in breakdown:
  181. order_line.prices.create(
  182. order=order,
  183. quantity=quantity,
  184. price_incl_tax=price_incl_tax,
  185. price_excl_tax=price_excl_tax)
  186. def create_line_attributes(self, order, order_line, basket_line):
  187. """
  188. Creates the batch line attributes.
  189. """
  190. for attr in basket_line.attributes.all():
  191. order_line.attributes.create(
  192. option=attr.option,
  193. type=attr.option.code,
  194. value=attr.value)
  195. def create_discount_model(self, order, discount):
  196. """
  197. Create an order discount model for each offer application attached to
  198. the basket.
  199. """
  200. order_discount = OrderDiscount(
  201. order=order,
  202. message=discount['message'],
  203. offer_id=discount['offer'].id,
  204. frequency=discount['freq'],
  205. amount=discount['discount'])
  206. result = discount['result']
  207. if result.affects_shipping:
  208. order_discount.category = OrderDiscount.SHIPPING
  209. elif result.affects_post_order:
  210. order_discount.category = OrderDiscount.DEFERRED
  211. voucher = discount.get('voucher', None)
  212. if voucher:
  213. order_discount.voucher_id = voucher.id
  214. order_discount.voucher_code = voucher.code
  215. order_discount.save()
  216. def record_discount(self, discount):
  217. discount['offer'].record_usage(discount)
  218. if 'voucher' in discount and discount['voucher']:
  219. discount['voucher'].record_discount(discount)
  220. def record_voucher_usage(self, order, voucher, user):
  221. """
  222. Updates the models that care about this voucher.
  223. """
  224. voucher.record_usage(order, user)