# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2017, Shoop Commerce Ltd. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals
import warnings
from decimal import Decimal
from django.utils.encoding import force_text
from shuup.core.models import Order, OrderLine, OrderLineType
from shuup.core.order_creator.signals import order_creator_finished
from shuup.core.shortcuts import update_order_line_from_product
from shuup.core.utils import context_cache
from shuup.core.utils.users import real_user_or_none
from shuup.utils.deprecation import RemovedFromShuupWarning
from shuup.utils.numbers import bankers_round
from ._source_modifier import get_order_source_modifier_modules
class OrderProcessor(object):
def source_line_to_order_lines(self, order, source_line):
"""
Convert a source line into one or more order lines.
Normally each source line will yield just one order line, but
package products will yield a parent line and its child lines.
:type order: shuup.core.models.Order
:param order: The order
:type source_line: shuup.core.order_creator.SourceLine
:param source_line: The SourceLine
:rtype: Iterable[OrderLine]
"""
order_line = OrderLine(order=order)
product = source_line.product
quantity = Decimal(source_line.quantity)
if product:
order_line.product = product
if product.sales_unit:
quantized_quantity = bankers_round(quantity, product.sales_unit.decimals)
if quantized_quantity != quantity:
raise ValueError("Sales unit decimal conversion causes precision loss!")
else:
order_line.product = None
def text(value):
return force_text(value) if value is not None else ""
order_line.quantity = quantity
order_line.supplier = source_line.supplier
order_line.sku = text(source_line.sku)
order_line.text = (text(source_line.text))[:192]
if source_line.base_unit_price:
order_line.base_unit_price = source_line.base_unit_price
if source_line.discount_amount:
order_line.discount_amount = source_line.discount_amount
order_line.type = (source_line.type if source_line.type is not None
else OrderLineType.OTHER)
order_line.accounting_identifier = text(source_line.accounting_identifier)
order_line.require_verification = bool(source_line.require_verification)
order_line.verified = (not order_line.require_verification)
order_line.source_line = source_line
order_line.parent_source_line = source_line.parent_line
self._check_orderability(order_line)
yield order_line
for child_order_line in self.create_package_children(order_line):
yield child_order_line
def create_package_children(self, order_line):
order = order_line.order
parent_product = order_line.product
# :type parent_product: shuup.core.models.Product
if not (parent_product and parent_product.is_package_parent()):
return
child_to_quantity = parent_product.get_package_child_to_quantity_map()
for (child_product, child_quantity) in child_to_quantity.items():
child_order_line = OrderLine(order=order, parent_line=order_line)
update_order_line_from_product(
pricing_context=None, # Will use zero price
order_line=child_order_line,
product=child_product,
quantity=(order_line.quantity * child_quantity),
)
# Package children are free
assert child_order_line.base_unit_price.value == 0
child_order_line.source_line = None
child_order_line.parent_source_line = order_line.source_line
child_order_line.supplier = order_line.supplier
self._check_orderability(child_order_line)
yield child_order_line
def _check_orderability(self, order_line):
if not order_line.product:
return
if not order_line.supplier:
raise ValueError("Order line has no supplier")
order = order_line.order
shop_product = order_line.product.get_shop_instance(order.shop)
shop_product.raise_if_not_orderable(
supplier=order_line.supplier,
quantity=order_line.quantity,
customer=order.customer
)
def process_saved_order_line(self, order, order_line):
"""
Called in sequence for all order lines to be saved into the order. These have all been saved, so they have PKs.
:type order: Order
:type order_line: OrderLine
"""
pass
def add_lines_into_order(self, order, lines):
# Map source lines to order lines for parentage linking
order_line_by_source = {
id(order_line.source_line): order_line
for order_line in lines
}
# Set line ordering, parentage and save the lines
for index, order_line in enumerate(lines):
order_line.order = order
order_line.ordering = index
parent_src_line = order_line.parent_source_line
if parent_src_line:
parent_order_line = order_line_by_source[id(parent_src_line)]
assert parent_order_line.pk, "Parent line should be saved"
order_line.parent_line = parent_order_line
order_line.save()
self.add_line_taxes(lines)
# And one last pass to call the subclass hook.
for order_line in lines:
self.process_saved_order_line(order=order, order_line=order_line)
def add_line_taxes(self, lines):
for line in lines:
if not line.source_line:
continue # Cannot have taxes, since not in source
for (index, line_tax) in enumerate(line.source_line.taxes, 1):
line.taxes.create(
tax=line_tax.tax,
name=line_tax.name,
amount_value=line_tax.amount.value,
base_amount_value=line_tax.base_amount.value,
ordering=index,
)
def get_source_order_lines(self, source, order):
"""
:type source: shuup.core.order_creator.OrderSource
:type order: shuup.core.models.Order
:rtype: list[OrderLine]
"""
lines = []
source.update_from_order(order)
# Since we just updated `order_provision`, we need to uncache
# the processed lines.
source.uncache()
for line in source.get_final_lines(with_taxes=True):
lines.extend(self.source_line_to_order_lines(order, line))
return lines
def get_source_base_data(self, order_source):
"""
:type order_source: shuup.core.order_creator.OrderSource
"""
return dict(
shop=order_source.shop,
currency=order_source.currency,
prices_include_tax=order_source.prices_include_tax,
shipping_address=(order_source.shipping_address.to_immutable() if order_source.shipping_address else None),
billing_address=(order_source.billing_address.to_immutable() if order_source.billing_address else None),
customer=(order_source.customer or None),
orderer=(order_source.orderer or None),
creator=real_user_or_none(order_source.creator),
shipping_method=order_source.shipping_method,
payment_method=order_source.payment_method,
customer_comment=(order_source.customer_comment if order_source.customer_comment else ""),
marketing_permission=bool(order_source.marketing_permission),
language=order_source.language,
ip_address=order_source.ip_address,
order_date=order_source.order_date,
status=order_source.status,
payment_data=order_source.payment_data,
shipping_data=order_source.shipping_data,
extra_data=order_source.extra_data
)
def finalize_creation(self, order, order_source):
order_source.verify_orderability()
self.process_order_before_lines(source=order_source, order=order)
lines = self.get_source_order_lines(source=order_source, order=order)
self.add_lines_into_order(order, lines)
if any(line.require_verification for line in order.lines.all()):
order.require_verification = True
order.all_verified = False
else:
order.all_verified = True
order.cache_prices()
order.save()
self._update_customer_info_if_needed(order)
self._assign_code_usages(order_source, order)
order.save()
self.process_order_after_lines(source=order_source, order=order)
# Then do all the caching one more time!
order.cache_prices()
order.save()
return order
def _update_customer_info_if_needed(self, order):
if not order.customer:
return
changed_fields = []
if not order.customer.name and order.billing_address:
order.customer.name = order.billing_address.name
changed_fields.append("name")
for address_kind in ["billing_address", "shipping_address"]:
order_addr = getattr(order, address_kind, None)
if not order_addr:
continue
customer_address_field = "default_%s" % address_kind
if not getattr(order.customer, customer_address_field, None):
new_customer_address = order_addr.to_mutable()
new_customer_address.save()
setattr(order.customer, customer_address_field, new_customer_address)
changed_fields.append(customer_address_field)
if order.customer.marketing_permission != order.marketing_permission:
order.customer.marketing_permission = order.marketing_permission
changed_fields.append("marketing_permission")
if changed_fields:
order.customer.save()
def _assign_code_usages(self, order_source, order):
order.codes = order_source.codes
for code in order_source.codes:
self._assign_code_usage(order_source, order, code)
def _assign_code_usage(self, order_source, order, code):
for module in get_order_source_modifier_modules():
if module.can_use_code(order_source, code):
module.use_code(order, code)
break
def process_order_before_lines(self, source, order):
# Subclass hook
pass
def process_order_after_lines(self, source, order):
# Subclass hook
pass
class OrderCreator(OrderProcessor):
def __init__(self, request=None):
"""
Initialize order creator.
:type request: django.http.HttpRequest|None
:param request:
Optional request object for backward compatibility. Passing
non-None value is DEPRECATED.
"""
if request is not None:
warnings.warn(
"Initializing OrderCreator with a request is deprecated",
RemovedFromShuupWarning, stacklevel=2)
[docs] def create_order(self, order_source):
data = self.get_source_base_data(order_source)
order = Order(**data)
order.save()
order = self.finalize_creation(order, order_source)
order_creator_finished.send(sender=type(self), order=order, source=order_source)
# reset product prices
for line in order.lines.exclude(product_id=None):
context_cache.bump_cache_for_product(line.product, shop=order.shop)
return order