| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- from decimal import Decimal as D
-
- from django.conf import settings
- from django.contrib.sites.models import Site
- from django.db import transaction
- from django.utils.translation import gettext_lazy as _
-
- from oscar.apps.order.signals import order_placed
- from oscar.core.loading import get_class, get_model
-
- from . import exceptions
-
- Order = get_model('order', 'Order')
- Line = get_model('order', 'Line')
- OrderDiscount = get_model('order', 'OrderDiscount')
- CommunicationEvent = get_model('order', 'CommunicationEvent')
- CommunicationEventType = get_model('communication', 'CommunicationEventType')
- Dispatcher = get_class('communication.utils', 'Dispatcher')
- Surcharge = get_model('order', 'Surcharge')
-
-
- class OrderNumberGenerator(object):
- """
- Simple object for generating order numbers.
-
- We need this as the order number is often required for payment
- which takes place before the order model has been created.
- """
-
- def order_number(self, basket):
- """
- Return an order number for a given basket
- """
- return 100000 + basket.id
-
-
- class OrderCreator(object):
- """
- Places the order by writing out the various models
- """
-
- def place_order(self, basket, total, # noqa (too complex (12))
- shipping_method, shipping_charge, user=None,
- shipping_address=None, billing_address=None,
- order_number=None, status=None, request=None, surcharges=None, **kwargs):
- """
- Placing an order involves creating all the relevant models based on the
- basket and session data.
- """
- if basket.is_empty:
- raise ValueError(_("Empty baskets cannot be submitted"))
- if not order_number:
- generator = OrderNumberGenerator()
- order_number = generator.order_number(basket)
- if not status and hasattr(settings, 'OSCAR_INITIAL_ORDER_STATUS'):
- status = getattr(settings, 'OSCAR_INITIAL_ORDER_STATUS')
-
- if Order._default_manager.filter(number=order_number).exists():
- raise ValueError(_("There is already an order with number %s")
- % order_number)
-
- with transaction.atomic():
-
- kwargs['surcharges'] = surcharges
- # Ok - everything seems to be in order, let's place the order
- order = self.create_order_model(
- user, basket, shipping_address, shipping_method, shipping_charge,
- billing_address, total, order_number, status, request, **kwargs)
- for line in basket.all_lines():
- self.create_line_models(order, line)
- self.update_stock_records(line)
-
- for voucher in basket.vouchers.select_for_update():
- if not voucher.is_active(): # basket ignores inactive vouchers
- basket.vouchers.remove(voucher)
- else:
- available_to_user, msg = voucher.is_available_to_user(user=user)
- if not available_to_user:
- raise ValueError(msg)
-
- # Record any discounts associated with this order
- for application in basket.offer_applications:
- # Trigger any deferred benefits from offers and capture the
- # resulting message
- application['message'] \
- = application['offer'].apply_deferred_benefit(basket, order,
- application)
- # Record offer application results
- if application['result'].affects_shipping:
- # Skip zero shipping discounts
- shipping_discount = shipping_method.discount(basket)
- if shipping_discount <= D('0.00'):
- continue
- # If a shipping offer, we need to grab the actual discount off
- # the shipping method instance, which should be wrapped in an
- # OfferDiscount instance.
- application['discount'] = shipping_discount
- self.create_discount_model(order, application)
- self.record_discount(application)
-
- for voucher in basket.vouchers.all():
- self.record_voucher_usage(order, voucher, user)
-
- # Send signal for analytics to pick up
- order_placed.send(sender=self, order=order, user=user)
-
- return order
-
- def create_order_model(self, user, basket, shipping_address,
- shipping_method, shipping_charge, billing_address,
- total, order_number, status, request=None, surcharges=None, **extra_order_fields):
- """Create an order model."""
- order_data = {'basket': basket,
- 'number': order_number,
- 'currency': total.currency,
- 'total_incl_tax': total.incl_tax,
- 'total_excl_tax': total.excl_tax,
- 'shipping_incl_tax': shipping_charge.incl_tax,
- 'shipping_excl_tax': shipping_charge.excl_tax,
- 'shipping_method': shipping_method.name,
- 'shipping_code': shipping_method.code}
- if shipping_address:
- order_data['shipping_address'] = shipping_address
- if billing_address:
- order_data['billing_address'] = billing_address
- if user and user.is_authenticated:
- order_data['user_id'] = user.id
- if status:
- order_data['status'] = status
- if extra_order_fields:
- order_data.update(extra_order_fields)
- if 'site' not in order_data:
- order_data['site'] = Site._default_manager.get_current(request)
- order = Order(**order_data)
- order.save()
- if surcharges is not None:
- for charge in surcharges:
- Surcharge.objects.create(
- order=order,
- name=charge.surcharge.name,
- code=charge.surcharge.code,
- excl_tax=charge.price.excl_tax,
- incl_tax=charge.price.incl_tax
- )
- return order
-
- def create_line_models(self, order, basket_line, extra_line_fields=None):
- """
- Create the batch line model.
-
- You can set extra fields by passing a dictionary as the
- extra_line_fields value
- """
- product = basket_line.product
- stockrecord = basket_line.stockrecord
- if not stockrecord:
- raise exceptions.UnableToPlaceOrder(
- "Basket line #%d has no stockrecord" % basket_line.id)
- partner = stockrecord.partner
- line_data = {
- 'order': order,
- # Partner details
- 'partner': partner,
- 'partner_name': partner.name,
- 'partner_sku': stockrecord.partner_sku,
- 'stockrecord': stockrecord,
- # Product details
- 'product': product,
- 'title': product.get_title(),
- 'upc': product.upc,
- 'quantity': basket_line.quantity,
- # Price details
- 'line_price_excl_tax':
- basket_line.line_price_excl_tax_incl_discounts,
- 'line_price_incl_tax':
- basket_line.line_price_incl_tax_incl_discounts,
- 'line_price_before_discounts_excl_tax':
- basket_line.line_price_excl_tax,
- 'line_price_before_discounts_incl_tax':
- basket_line.line_price_incl_tax,
- # Reporting details
- 'unit_price_incl_tax': basket_line.unit_price_incl_tax,
- 'unit_price_excl_tax': basket_line.unit_price_excl_tax,
- }
- extra_line_fields = extra_line_fields or {}
- if hasattr(settings, 'OSCAR_INITIAL_LINE_STATUS'):
- if not (extra_line_fields and 'status' in extra_line_fields):
- extra_line_fields['status'] = getattr(
- settings, 'OSCAR_INITIAL_LINE_STATUS')
- if extra_line_fields:
- line_data.update(extra_line_fields)
-
- order_line = Line._default_manager.create(**line_data)
- self.create_line_price_models(order, order_line, basket_line)
- self.create_line_attributes(order, order_line, basket_line)
- self.create_additional_line_models(order, order_line, basket_line)
-
- return order_line
-
- def update_stock_records(self, line):
- """
- Update any relevant stock records for this order line
- """
- if line.product.get_product_class().track_stock:
- line.stockrecord.allocate(line.quantity)
-
- def create_additional_line_models(self, order, order_line, basket_line):
- """
- Empty method designed to be overridden.
-
- Some applications require additional information about lines, this
- method provides a clean place to create additional models that
- relate to a given line.
- """
- pass
-
- def create_line_price_models(self, order, order_line, basket_line):
- """
- Creates the batch line price models
- """
- breakdown = basket_line.get_price_breakdown()
- for price_incl_tax, price_excl_tax, quantity in breakdown:
- order_line.prices.create(
- order=order,
- quantity=quantity,
- price_incl_tax=price_incl_tax,
- price_excl_tax=price_excl_tax)
-
- def create_line_attributes(self, order, order_line, basket_line):
- """
- Creates the batch line attributes.
- """
- for attr in basket_line.attributes.all():
- order_line.attributes.create(
- option=attr.option,
- type=attr.option.code,
- value=attr.value)
-
- def create_discount_model(self, order, discount):
-
- """
- Create an order discount model for each offer application attached to
- the basket.
- """
- order_discount = OrderDiscount(
- order=order,
- message=discount['message'] or '',
- offer_id=discount['offer'].id,
- frequency=discount['freq'],
- amount=discount['discount'])
- result = discount['result']
- if result.affects_shipping:
- order_discount.category = OrderDiscount.SHIPPING
- elif result.affects_post_order:
- order_discount.category = OrderDiscount.DEFERRED
- voucher = discount.get('voucher', None)
- if voucher:
- order_discount.voucher_id = voucher.id
- order_discount.voucher_code = voucher.code
- order_discount.save()
-
- def record_discount(self, discount):
- discount['offer'].record_usage(discount)
- if 'voucher' in discount and discount['voucher']:
- discount['voucher'].record_discount(discount)
-
- def record_voucher_usage(self, order, voucher, user):
- """
- Updates the models that care about this voucher.
- """
- voucher.record_usage(order, user)
-
-
- class OrderDispatcher:
- """
- Dispatcher to send concrete order related emails.
- """
-
- # Event codes
- ORDER_PLACED_EVENT_CODE = 'ORDER_PLACED'
-
- def __init__(self, logger=None, mail_connection=None):
- self.dispatcher = Dispatcher(logger=logger, mail_connection=mail_connection)
-
- def dispatch_order_messages(self, order, messages, event_code, attachments=None, **kwargs):
- """
- Dispatch order-related messages to the customer.
- """
- self.dispatcher.logger.info("Order #%s - sending %s messages", order.number, event_code)
- if order.is_anonymous:
- email = kwargs.get('email_address', order.guest_email)
- dispatched_messages = self.dispatcher.dispatch_anonymous_messages(email, messages, attachments)
- else:
- dispatched_messages = self.dispatcher.dispatch_user_messages(order.user, messages, attachments)
-
- try:
- event_type = CommunicationEventType.objects.get(code=event_code)
- except CommunicationEventType.DoesNotExist:
- event_type = None
-
- self.create_communication_event(order, event_type, dispatched_messages)
-
- def create_communication_event(self, order, event_type, dispatched_messages):
- """
- Create order communications event for audit.
- """
- if dispatched_messages and event_type is not None:
- CommunicationEvent._default_manager.create(order=order, event_type=event_type)
-
- def send_order_placed_email_for_user(self, order, extra_context, attachments=None):
- event_code = self.ORDER_PLACED_EVENT_CODE
- messages = self.dispatcher.get_messages(event_code, extra_context)
- self.dispatch_order_messages(order, messages, event_code, attachments=attachments)
|