# -*- coding: utf-8 -*-
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals
from copy import deepcopy
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils.translation import ugettext as _
from shoop.core.models import (
CompanyContact, Contact, MutableAddress, OrderLineType, OrderStatus,
PaymentMethod, PersonContact, Product, ShippingMethod, Shop
)
from shoop.core.order_creator import OrderCreator, OrderModifier, OrderSource
from shoop.utils.analog import LogEntryKind
from shoop.utils.numbers import parse_decimal_string
[docs]class JsonOrderCreator(object):
def __init__(self):
self._errors = []
@staticmethod
[docs] def safe_get_first(model, **lookup):
# A little helper function to clean up the code below.
return model.objects.filter(**lookup).first()
[docs] def add_error(self, error):
self._errors.append(error)
@property
def is_valid(self):
return not self._errors
@property
def errors(self):
return tuple(self._errors)
def _process_line_quantity_and_price(self, source, sline, sl_kwargs):
quantity_val = sline.pop("quantity", None)
try:
sl_kwargs["quantity"] = parse_decimal_string(quantity_val)
except Exception as exc:
msg = _("The quantity '%(quantity)s' (for line %(text)s) is invalid (%(error)s)") % {
"text": sl_kwargs["text"],
"quantity": quantity_val,
"error": exc,
}
self.add_error(ValidationError(msg, code="invalid_quantity"))
return False
is_product = bool(sline.get("type") == "product")
price_val = sline.pop("baseUnitPrice", None) if is_product else sline.pop("unitPrice", None)
try:
sl_kwargs["base_unit_price"] = source.create_price(parse_decimal_string(price_val))
except Exception as exc:
msg = _("The price '%(price)s' (for line %(text)s) is invalid (%(error)s)") % {
"text": sl_kwargs["text"],
"price": price_val,
"error": exc
}
self.add_error(ValidationError(msg, code="invalid_price"))
return False
discount_val = sline.pop("discountAmount", parse_decimal_string(str("0.00")))
try:
sl_kwargs["discount_amount"] = source.create_price(parse_decimal_string(discount_val))
except Exception as exc:
msg = _("The discount '%(discount)s' (for line %(text)s is invalid (%(error)s)") % {
"discount": discount_val,
"text": sl_kwargs["text"],
"error": exc
}
self.add_error(ValidationError(msg, code="invalid_discount"))
return True
def _process_product_line(self, source, sline, sl_kwargs):
product_info = sline.pop("product", None)
if not product_info:
self.add_error(ValidationError(_("Product line does not have a product set."), code="no_product"))
return False
product = self.safe_get_first(Product, pk=product_info["id"])
if not product:
self.add_error(ValidationError(_("Product %s does not exist.") % product_info["id"], code="no_product"))
return False
try:
shop_product = product.get_shop_instance(source.shop)
except ObjectDoesNotExist:
self.add_error(ValidationError((_("Product %(product)s is not available in the %(shop)s shop.") % {
"product": product,
"shop": source.shop
}), code="no_shop_product"))
return False
supplier = shop_product.suppliers.first() # TODO: Allow setting a supplier?
is_orderable = True
for message in shop_product.get_orderability_errors(
supplier=supplier, quantity=sl_kwargs["quantity"], customer=source.customer):
self.add_error(ValidationError((_("Product %(product)s is not orderable: %(error)s") % {
"product": product,
"error": str(message.args[0])
}), code=str(message.args[1])))
is_orderable = False
if not is_orderable:
return False
sl_kwargs["product"] = product
sl_kwargs["supplier"] = supplier
sl_kwargs["type"] = OrderLineType.PRODUCT
sl_kwargs["sku"] = product.sku
sl_kwargs["text"] = product.name
return True
def _add_json_line_to_source(self, source, sline):
valid = True
type = sline.get("type")
sl_kwargs = dict(
line_id=sline.pop("id"),
sku=sline.pop("sku", None),
text=sline.pop("text", None),
shop=source.shop,
type=OrderLineType.OTHER # Overridden in the `product` branch
)
if type != "text":
if not self._process_line_quantity_and_price(source, sline, sl_kwargs):
valid = False
if type == "product":
if not self._process_product_line(source, sline, sl_kwargs):
valid = False
if valid:
source.add_line(**sl_kwargs)
def _process_lines(self, source, state):
state_lines = state.pop("lines", [])
if not state_lines:
self.add_error(ValidationError(_("Please add lines to the order."), code="no_lines"))
for sline in state_lines:
try:
self._add_json_line_to_source(source, sline)
except Exception as exc: # pragma: no cover
self.add_error(exc)
def _create_contact_from_address(self, billing_address, is_company):
name = billing_address.get("name", None)
if not name:
self.add_error(
ValidationError(_("Name is required for customer"), code="no_name")
)
return
phone = billing_address.get("phone", "")
email = billing_address.get("email", "")
fields = {"name": name, "phone": phone, "email": email}
if is_company:
tax_number = billing_address.get("tax_number", None)
if not tax_number:
self.add_error(
ValidationError(_("Tax number is not set for company."), code="no_tax_number")
)
return
fields.update({"tax_number": tax_number})
customer = CompanyContact(**fields)
else:
customer = PersonContact(**fields)
return customer
def _initialize_source_from_state(self, state, creator, ip_address, save, order_to_update=None):
shop_data = state.pop("shop", None).get("selected", {})
shop = self.safe_get_first(Shop, pk=shop_data.pop("id", None))
if not shop:
self.add_error(ValidationError(_("Please choose a valid shop."), code="no_shop"))
return None
source = OrderSource(shop=shop)
if order_to_update:
source.update_from_order(order_to_update)
customer_data = state.pop("customer", None)
billing_address_data = customer_data.pop("billingAddress", {})
shipping_address_data = (
billing_address_data
if customer_data.pop("shipToBillingAddress", False)
else customer_data.pop("shippingAddress", {}))
is_company = customer_data.pop("isCompany", False)
save_address = customer_data.pop("saveAddress", False)
customer = self._get_customer(customer_data, billing_address_data, is_company, save)
if not customer:
return
billing_address = MutableAddress.from_data(billing_address_data)
shipping_address = MutableAddress.from_data(shipping_address_data)
if save:
billing_address.save()
shipping_address.save()
if save and save_address:
customer.default_billing_address = billing_address
customer.default_shipping_address = shipping_address
customer.save()
methods_data = state.pop("methods", None) or {}
shipping_method = methods_data.pop("shippingMethod")
if not shipping_method:
self.add_error(ValidationError(_("Please select shipping method."), code="no_shipping_method"))
payment_method = methods_data.pop("paymentMethod")
if not payment_method:
self.add_error(ValidationError(_("Please select payment method."), code="no_payment_method"))
if self.errors:
return
source.update(
creator=creator,
ip_address=ip_address,
customer=customer,
billing_address=billing_address,
shipping_address=shipping_address,
status=OrderStatus.objects.get_default_initial(),
shipping_method=self.safe_get_first(ShippingMethod, pk=shipping_method.get("id")),
payment_method=self.safe_get_first(PaymentMethod, pk=payment_method.get("id")),
)
return source
def _get_customer(self, customer_data, billing_address_data, is_company, save):
customer = self.safe_get_first(Contact, pk=customer_data.get("id")) if customer_data else None
if not customer:
customer = self._create_contact_from_address(billing_address_data, is_company)
if not customer:
return
if save:
customer.save()
return customer
def _postprocess_order(self, order, state):
comment = (state.pop("comment", None) or "")
if comment:
order.add_log_entry(comment, kind=LogEntryKind.NOTE, user=order.creator)
[docs] def create_source_from_state(self, state, creator=None, ip_address=None, save=False, order_to_update=None):
"""
Create an order source from a state dict unserialized from JSON.
:param state: State dictionary
:type state: dict
:param creator: Creator user
:type creator: django.contrib.auth.models.User|None
:param save: Flag whether order customer and addresses is saved to database
:type save: boolean
:param order_to_update: Order object to edit
:type order_to_update: shoop.core.models.Order|None
:return: The created order source, or None if something failed along the way
:rtype: OrderSource|None
"""
if not self.is_valid: # pragma: no cover
raise ValueError("Create a new JsonOrderCreator for each order.")
# We'll be mutating the state to make it easier to track we've done everything,
# so it's nice to deepcopy things first.
state = deepcopy(state)
# First, initialize an OrderSource.
source = self._initialize_source_from_state(
state, creator=creator, ip_address=ip_address, save=save, order_to_update=order_to_update)
if not source:
return None
# Then, copy some lines into it.
self._process_lines(source, state)
if not self.is_valid: # If we encountered any errors thus far, don't bother going forward
return None
for error in source.get_validation_errors():
self.add_error(error)
if not self.is_valid:
return None
if order_to_update:
for code in order_to_update.codes:
source.add_code(code)
return source
[docs] def create_order_from_state(self, state, creator=None, ip_address=None):
"""
Create an order from a state dict unserialized from JSON.
:param state: State dictionary
:type state: dict
:param creator: Creator user
:type creator: django.contrib.auth.models.User|None
:param ip_address: Remote IP address (IPv4 or IPv6)
:type ip_address: str
:return: The created order, or None if something failed along the way
:rtype: Order|None
"""
source = self.create_source_from_state(
state, creator=creator, ip_address=ip_address, save=True)
# Then create an OrderCreator and try to get things done!
creator = OrderCreator()
try:
order = creator.create_order(order_source=source)
self._postprocess_order(order, state)
return order
except Exception as exc: # pragma: no cover
self.add_error(exc)
return
[docs] def update_order_from_state(self, state, order_to_update, modified_by=None):
"""
Update an order from a state dict unserialized from JSON.
:param state: State dictionary
:type state: dict
:param order_to_update: Order object to edit
:type order_to_update: shoop.core.models.Order
:return: The created order, or None if something failed along the way
:rtype: Order|None
"""
source = self.create_source_from_state(state, order_to_update=order_to_update, save=True)
if source:
source.modified_by = modified_by
modifier = OrderModifier()
try:
order = modifier.update_order_from_source(order_source=source, order=order_to_update)
self._postprocess_order(order, state)
return order
except Exception as exc:
self.add_error(exc)
return