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')
[docs]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.
"""
[docs] def order_number(self, basket):
"""
Return an order number for a given basket
"""
return 100000 + basket.id
[docs]class OrderCreator(object):
"""
Places the order by writing out the various models
"""
[docs] 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
[docs] 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
[docs] 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
[docs] 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)
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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)
[docs] def record_voucher_usage(self, order, voucher, user):
"""
Updates the models that care about this voucher.
"""
voucher.record_usage(order, user)
[docs]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)
[docs] 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)
[docs] 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)