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.

processing.py 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. from decimal import Decimal as D
  2. from django.utils.translation import ugettext_lazy as _
  3. from oscar.core.loading import get_model
  4. from oscar.apps.order import exceptions
  5. ShippingEventQuantity = get_model('order', 'ShippingEventQuantity')
  6. PaymentEventQuantity = get_model('order', 'PaymentEventQuantity')
  7. class EventHandler(object):
  8. """
  9. Handle requested order events.
  10. This is an important class: it houses the core logic of your shop's order
  11. processing pipeline.
  12. """
  13. def __init__(self, user=None):
  14. self.user = user
  15. # Core API
  16. # --------
  17. def handle_shipping_event(self, order, event_type, lines,
  18. line_quantities, **kwargs):
  19. """
  20. Handle a shipping event for a given order.
  21. This is most common entry point to this class - most of your order
  22. processing should be modelled around shipping events. Shipping events
  23. can be used to trigger payment and communication events.
  24. You will generally want to override this method to implement the
  25. specifics of you order processing pipeline.
  26. """
  27. # Example implementation
  28. self.validate_shipping_event(
  29. order, event_type, lines, line_quantities, **kwargs)
  30. return self.create_shipping_event(
  31. order, event_type, lines, line_quantities, **kwargs)
  32. def handle_payment_event(self, order, event_type, amount, lines=None,
  33. line_quantities=None, **kwargs):
  34. """
  35. Handle a payment event for a given order.
  36. These should normally be called as part of handling a shipping event.
  37. It is rare to call to this method directly. It does make sense for
  38. refunds though where the payment event may be unrelated to a particular
  39. shipping event and doesn't directly correspond to a set of lines.
  40. """
  41. self.validate_payment_event(
  42. order, event_type, amount, lines, line_quantities, **kwargs)
  43. return self.create_payment_event(
  44. order, event_type, amount, lines, line_quantities, **kwargs)
  45. def handle_order_status_change(self, order, new_status, note_msg=None):
  46. """
  47. Handle a requested order status change
  48. This method is not normally called directly by client code. The main
  49. use-case is when an order is cancelled, which in some ways could be
  50. viewed as a shipping event affecting all lines.
  51. """
  52. order.set_status(new_status)
  53. if note_msg:
  54. self.create_note(order, note_msg)
  55. # Validation methods
  56. # ------------------
  57. def validate_shipping_event(self, order, event_type, lines,
  58. line_quantities, **kwargs):
  59. """
  60. Test if the requested shipping event is permitted.
  61. If not, raise InvalidShippingEvent
  62. """
  63. errors = []
  64. for line, qty in zip(lines, line_quantities):
  65. # The core logic should be in the model. Ensure you override
  66. # 'is_shipping_event_permitted' and enforce the correct order of
  67. # shipping events.
  68. if not line.is_shipping_event_permitted(event_type, qty):
  69. msg = _("The selected quantity for line #%(line_id)s is too"
  70. " large") % {'line_id': line.id}
  71. errors.append(msg)
  72. if errors:
  73. raise exceptions.InvalidShippingEvent(", ".join(errors))
  74. def validate_payment_event(self, order, event_type, amount, lines,
  75. line_quantities, **kwargs):
  76. errors = []
  77. for line, qty in zip(lines, line_quantities):
  78. if not line.is_payment_event_permitted(event_type, qty):
  79. msg = _("The selected quantity for line #%(line_id)s is too"
  80. " large") % {'line_id': line.id}
  81. errors.append(msg)
  82. if errors:
  83. raise exceptions.InvalidPaymentEvent(", ".join(errors))
  84. # Query methods
  85. # -------------
  86. # These are to help determine the status of lines
  87. def have_lines_passed_shipping_event(self, order, lines, line_quantities,
  88. event_type):
  89. """
  90. Test whether the passed lines and quantities have been through the
  91. specified shipping event.
  92. This is useful for validating if certain shipping events are allowed
  93. (ie you can't return something before it has shipped).
  94. """
  95. for line, line_qty in zip(lines, line_quantities):
  96. if line.shipping_event_quantity(event_type) < line_qty:
  97. return False
  98. return True
  99. # Payment stuff
  100. # -------------
  101. def calculate_payment_event_subtotal(self, event_type, lines,
  102. line_quantities):
  103. """
  104. Calculate the total charge for the passed event type, lines and line
  105. quantities.
  106. This takes into account the previous prices that have been charged for
  107. this event.
  108. Note that shipping is not including in this subtotal. You need to
  109. subclass and extend this method if you want to include shipping costs.
  110. """
  111. total = D('0.00')
  112. for line, qty_to_consume in zip(lines, line_quantities):
  113. # This part is quite fiddly. We need to skip the prices that have
  114. # already been settled. This involves keeping a load of counters.
  115. # Count how many of this line have already been involved in an
  116. # event of this type.
  117. qty_to_skip = line.payment_event_quantity(event_type)
  118. # Test if request is sensible
  119. if qty_to_skip + qty_to_consume > line.quantity:
  120. raise exceptions.InvalidPaymentEvent
  121. # Consume prices in order of ID (this is the default but it's
  122. # better to be explicit)
  123. qty_consumed = 0
  124. for price in line.prices.all().order_by('id'):
  125. if qty_consumed == qty_to_consume:
  126. # We've accounted for the asked-for quantity: we're done
  127. break
  128. qty_available = price.quantity - qty_to_skip
  129. if qty_available <= 0:
  130. # Skip the whole quantity of this price instance
  131. qty_to_skip -= price.quantity
  132. else:
  133. # Need to account for some of this price instance and
  134. # track how many we needed to skip and how many we settled
  135. # for.
  136. qty_to_include = min(
  137. qty_to_consume - qty_consumed, qty_available)
  138. total += qty_to_include * price.price_incl_tax
  139. # There can't be any left to skip if we've included some in
  140. # our total
  141. qty_to_skip = 0
  142. qty_consumed += qty_to_include
  143. return total
  144. # Stock
  145. # -----
  146. def are_stock_allocations_available(self, lines, line_quantities):
  147. """
  148. Check whether stock records still have enough stock to honour the
  149. requested allocations.
  150. """
  151. for line, qty in zip(lines, line_quantities):
  152. record = line.stockrecord
  153. if not record:
  154. return False
  155. if not record.is_allocation_consumption_possible(qty):
  156. return False
  157. return True
  158. def consume_stock_allocations(self, order, lines, line_quantities):
  159. """
  160. Consume the stock allocations for the passed lines
  161. """
  162. for line, qty in zip(lines, line_quantities):
  163. if line.stockrecord:
  164. line.stockrecord.consume_allocation(qty)
  165. def cancel_stock_allocations(self, order, lines, line_quantities):
  166. """
  167. Cancel the stock allocations for the passed lines
  168. """
  169. for line, qty in zip(lines, line_quantities):
  170. if line.stockrecord:
  171. line.stockrecord.cancel_allocation(qty)
  172. # Model instance creation
  173. # -----------------------
  174. def create_shipping_event(self, order, event_type, lines, line_quantities,
  175. **kwargs):
  176. reference = kwargs.get('reference', '')
  177. event = order.shipping_events.create(
  178. event_type=event_type, notes=reference)
  179. try:
  180. for line, quantity in zip(lines, line_quantities):
  181. event.line_quantities.create(
  182. line=line, quantity=quantity)
  183. except exceptions.InvalidShippingEvent:
  184. event.delete()
  185. raise
  186. return event
  187. def create_payment_event(self, order, event_type, amount, lines=None,
  188. line_quantities=None, **kwargs):
  189. reference = kwargs.get('reference', "")
  190. event = order.payment_events.create(
  191. event_type=event_type, amount=amount, reference=reference)
  192. if lines and line_quantities:
  193. for line, quantity in zip(lines, line_quantities):
  194. event.line_quantities.create(
  195. line=line, quantity=quantity)
  196. return event
  197. def create_communication_event(self, order, event_type):
  198. return order.communication_events.create(event_type=event_type)
  199. def create_note(self, order, message, note_type='System'):
  200. return order.notes.create(
  201. message=message, note_type=note_type, user=self.user)