Source code for shuup.admin.modules.orders.views.edit

# -*- 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 decimal
import json

from babel.numbers import format_currency, format_decimal
from django.conf import settings
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Sum
from django.http.response import HttpResponse, JsonResponse
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _
from django.views.generic import View
from django_countries import countries

from shuup.admin.modules.orders.json_order_creator import JsonOrderCreator
from shuup.admin.signals import object_created
from shuup.admin.toolbar import Toolbar
from shuup.admin.utils.urls import get_model_url
from shuup.admin.utils.views import CreateOrUpdateView
from shuup.core.models import (
    AnonymousContact, CompanyContact, Contact, Order, OrderLineType,
    PaymentMethod, PersonContact, Product, ShippingMethod, Shop, ShopStatus
)
from shuup.core.pricing import get_pricing_module
from shuup.utils.i18n import (
    format_money, format_percent, get_current_babel_locale,
    get_locally_formatted_datetime
)


[docs]def create_order_from_state(state, **kwargs): joc = JsonOrderCreator() order = joc.create_order_from_state(state, **kwargs) if not order: raise ValidationError(list(joc.errors)) return order
[docs]def update_order_from_state(state, order_to_update, **kwargs): joc = JsonOrderCreator() order = joc.update_order_from_state(state, order_to_update, **kwargs) if not order: raise ValidationError(list(joc.errors)) return order
[docs]def create_source_from_state(state, **kwargs): joc = JsonOrderCreator() source = joc.create_source_from_state(state, **kwargs) if not source: raise ValidationError(list(joc.errors)) return source
[docs]def encode_address(address, tax_number=""): if not address: return {"tax_number": tax_number} address_dict = json.loads(serializers.serialize("json", [address]))[0].get("fields") if not address_dict.get("tax_number", ""): address_dict["tax_number"] = tax_number return address_dict
[docs]def encode_shop(shop): return { "id": shop.pk, "name": force_text(shop), "currency": shop.currency, "pricesIncludeTaxes": shop.prices_include_tax }
[docs]def encode_method(method): basic_data = {"id": method.pk, "name": force_text(method)} return basic_data
[docs]def encode_line(line): if line.base_unit_price.amount.value != 0: discount_percent = ( line.discount_amount.amount.value / (line.base_unit_price.amount.value * line.quantity) ) else: discount_percent = 0 return { "sku": line.sku, "text": line.text, "quantity": format_decimal(line.quantity, locale=get_current_babel_locale()), "unitPrice": format_money(line.base_unit_price.amount), "discountedUnitPrice": format_money(line.discounted_unit_price.amount), "discountAmount": format_money(line.discount_amount.amount), "discountPercent": format_percent(discount_percent, 2), "taxlessTotal": format_money(line.taxless_price.amount), "taxPercentage": format_percent(line.tax_rate, 2), "taxfulTotal": format_money(line.taxful_price.amount) }
[docs]def get_line_data_for_edit(shop, line): total_price = line.taxful_price.value if shop.prices_include_tax else line.taxless_price.value base_data = { "id": line.id, "type": "other" if line.quantity else "text", "text": line.text, "quantity": line.quantity, "sku": line.sku, "baseUnitPrice": line.base_unit_price.value, "unitPrice": total_price / line.quantity if line.quantity else 0, "unitPriceIncludesTax": shop.prices_include_tax, "errors": "", "step": "" } if line.product: shop_product = line.product.get_shop_instance(shop) supplier = shop_product.suppliers.first() stock_status = supplier.get_stock_status(line.product.pk) if supplier else None base_data.update({ "type": "product", "product": { "id": line.product.pk, "text": line.product.name }, "step": shop_product.purchase_multiple, "logicalCount": stock_status.logical_count if stock_status else 0, "physicalCount": stock_status.physical_count if stock_status else 0, "salesDecimals": line.product.sales_unit.decimals if line.product.sales_unit else 0, "salesUnit": line.product.sales_unit.symbol if line.product.sales_unit else "" }) return base_data
[docs]def get_price_info(shop, customer, product, quantity): """ Get price info of given product for given context parameters. :type shop: shuup.core.models.Shop :type customer: shuup.core.models.Contact :type product: shuup.core.models.Product :type quantity: numbers.Number """ pricing_mod = get_pricing_module() pricing_ctx = pricing_mod.get_context_from_data( shop=shop, customer=(customer or AnonymousContact()), ) return product.get_price_info(pricing_ctx, quantity=quantity)
[docs]class OrderEditView(CreateOrUpdateView): model = Order template_name = "shuup/admin/orders/create.jinja" context_object_name = "order" title = _("Create Order") fields = []
[docs] def get_context_data(self, **kwargs): context = super(OrderEditView, self).get_context_data(**kwargs) context["config"] = self.get_config() return context
[docs] def get_toolbar(self): return Toolbar([])
[docs] def get_config(self): order = self.object shops = [encode_shop(shop) for shop in Shop.objects.filter(status=ShopStatus.ENABLED)] customer_id = self.request.GET.get("contact_id") shipping_methods = ShippingMethod.objects.enabled() payment_methods = PaymentMethod.objects.enabled() return { "shops": shops, "countryDefault": settings.SHUUP_ADDRESS_HOME_COUNTRY, "countries": [{"id": code, "name": name} for code, name in list(countries)], "shippingMethods": [encode_method(sm) for sm in shipping_methods], "paymentMethods": [encode_method(pm) for pm in payment_methods], "orderId": order.pk, "orderData": self.get_initial_order_data(), "customerData": self.get_customer_data(customer_id) if customer_id else None }
[docs] def get_initial_order_data(self): order = self.object if not order.pk: return {} return { "shop": encode_shop(order.shop), "lines": [ get_line_data_for_edit(order.shop, line) for line in order.lines.filter( type__in=[OrderLineType.PRODUCT, OrderLineType.OTHER], parent_line_id=None ) ], "shippingMethodId": (encode_method(order.shipping_method) if order.shipping_method else None), "paymentMethodId": (encode_method(order.payment_method) if order.payment_method else None), "customer": { "id": order.customer.id if order.customer else "", "name": order.customer.name if order.customer else "", "isCompany": bool(isinstance(order.customer, CompanyContact)), "billingAddress": encode_address(order.billing_address, order.tax_number), "shippingAddress": encode_address(order.shipping_address, order.tax_number) } }
[docs] def get_customer_data(self, customer_id): customer = Contact.objects.filter(pk=customer_id).first() if not customer: return JsonResponse( {"success": False, "errorMessage": _("Contact %s does not exist.") % customer_id}, status=400 ) tax_number = getattr(customer, "tax_number", "") return { "id": customer.id, "name": customer.name, "isCompany": bool(isinstance(customer, CompanyContact)), "billingAddress": encode_address(customer.default_billing_address, tax_number), "shippingAddress": encode_address(customer.default_shipping_address, tax_number) }
[docs] def dispatch(self, request, *args, **kwargs): if request.GET.get("command"): return self.dispatch_command(request) return super(OrderEditView, self).dispatch(request, *args, **kwargs)
[docs] def dispatch_command(self, request): handler = getattr(self, "handle_%s" % request.GET.get("command"), None) if not callable(handler): return JsonResponse({"error": "unknown command %s" % request.GET.get("command")}, status=400) retval = handler(request) if not isinstance(retval, HttpResponse): retval = JsonResponse(retval) return retval
[docs] def handle_product_data(self, request): product_id = request.GET["id"] shop_id = request.GET["shop_id"] customer_id = request.GET.get("customer_id") quantity = decimal.Decimal(request.GET.get("quantity", 1)) product = Product.objects.filter(pk=product_id).first() if not product: return {"errorText": _("Product %s does not exist.") % product_id} shop = Shop.objects.get(pk=shop_id) try: shop_product = product.get_shop_instance(shop) except ObjectDoesNotExist: return { "errorText": _("Product %(product)s is not available in the %(shop)s shop.") % {"product": product.name, "shop": shop.name} } min_quantity = shop_product.minimum_purchase_quantity # Make quantity to be at least minimum quantity quantity = (min_quantity if quantity < min_quantity else quantity) customer = Contact.objects.filter(pk=customer_id).first() if customer_id else None price_info = get_price_info(shop, customer, product, quantity) supplier = shop_product.suppliers.first() # TODO: Allow setting a supplier? stock_status = supplier.get_stock_status(product.pk) if supplier else None return { "id": product.id, "sku": product.sku, "name": product.name, "quantity": quantity, "logicalCount": stock_status.logical_count if stock_status else 0, "physicalCount": stock_status.physical_count if stock_status else 0, "salesDecimals": product.sales_unit.decimals if product.sales_unit else 0, "salesUnit": product.sales_unit.symbol if product.sales_unit else "", "purchaseMultiple": shop_product.purchase_multiple, "taxClass": { "id": product.tax_class.id, "name": force_text(product.tax_class), }, "baseUnitPrice": { "value": price_info.base_unit_price.value, "includesTax": price_info.base_unit_price.includes_tax }, "unitPrice": { "value": price_info.discounted_unit_price.value, "includesTax": price_info.base_unit_price.includes_tax }, "product": { "text": product.name, "id": product.id, "url": get_model_url(product) } }
[docs] def handle_customer_data(self, request): customer_id = request.GET["id"] return self.get_customer_data(customer_id)
[docs] def handle_customer_exists(self, request): field = request.GET["field"] value = request.GET["value"] if field in Contact._meta.get_all_field_names(): contact_model = Contact elif field in CompanyContact._meta.get_all_field_names(): contact_model = CompanyContact elif field in PersonContact._meta.get_all_field_names(): contact_model = PersonContact else: return {"error": "Invalid field name"} customer = contact_model.objects.filter(**{field: value}).first() if customer: return {"id": customer.pk, "name": force_text(customer)} else: return {}
[docs] def handle_customer_details(self, request): customer_id = request.GET["id"] customer = Contact.objects.get(pk=customer_id) companies = [] if isinstance(customer, PersonContact): companies = sorted(customer.company_memberships.all(), key=(lambda x: force_text(x))) recent_orders = customer.customer_orders.valid().order_by('-id')[:10] order_summary = [] for dt in customer.customer_orders.valid().datetimes('order_date', 'year'): summary = customer.customer_orders.filter(order_date__year=dt.year).aggregate( total=Sum('taxful_total_price_value') ) order_summary.append({ 'year': dt.year, 'total': format_currency( summary['total'], currency=recent_orders[0].currency, locale=get_current_babel_locale() ) }) return { "customer_info": { "name": customer.full_name, "phone_no": customer.phone, "email": customer.email, "tax_number": getattr(customer, "tax_number", ""), "companies": [force_text(company) for company in companies] if len(companies) else None, "groups": [force_text(group) for group in customer.groups.all()], "merchant_notes": customer.merchant_notes }, "order_summary": order_summary, "recent_orders": [ { "order_date": get_locally_formatted_datetime(order.order_date), "total": format_money(order.taxful_total_price), "status": order.get_status_display(), "payment_status": force_text(order.payment_status.label), "shipment_status": force_text(order.shipping_status.label) } for order in recent_orders ] }
@transaction.atomic def _handle_source_data(self, request): self.object = self.get_object() state = json.loads(request.body.decode("utf-8"))["state"] source = create_source_from_state( state, creator=request.user, ip_address=request.META.get("REMOTE_ADDR"), order_to_update=self.object if self.object.pk else None ) # Calculate final lines for confirmation source.calculate_taxes(force_recalculate=True) return { "taxfulTotal": format_money(source.taxful_total_price.amount), "taxlessTotal": format_money(source.taxless_total_price.amount), "totalDiscountAmount": format_money(source.total_discount.amount), "orderLines": [encode_line(line) for line in source.get_final_lines(with_taxes=True)], "billingAddress": source.billing_address.as_string_list() if source.billing_address else None, "shippingAddress": source.shipping_address.as_string_list() if source.shipping_address else None, } @transaction.atomic def _handle_finalize(self, request): state = json.loads(request.body.decode("utf-8"))["state"] self.object = self.get_object() if self.object.pk: # Edit order = update_order_from_state( state, self.object, modified_by=request.user ) assert self.object.pk == order.pk messages.success(request, _("Order %(identifier)s updated.") % vars(order)) else: # Create order = create_order_from_state( state, creator=request.user, ip_address=request.META.get("REMOTE_ADDR"), ) object_created.send(sender=Order, object=order) messages.success(request, _("Order %(identifier)s created.") % vars(order)) return JsonResponse({ "success": True, "orderIdentifier": order.identifier, "url": reverse("shuup_admin:order.detail", kwargs={"pk": order.pk}) })
[docs] def handle_source_data(self, request): return _handle_or_return_error(self._handle_source_data, request, _("Could not proceed with order:"))
[docs] def handle_finalize(self, request): return _handle_or_return_error(self._handle_finalize, request, _("Could not finalize order:"))
[docs]class UpdateAdminCommentView(View): """ Update order's admin comment """
[docs] def post(self, request, *args, **kwargs): order = Order.objects.get(pk=kwargs["pk"]) comment = request.POST["comment"] order.admin_comment = comment order.save() return JsonResponse({ "comment": order.admin_comment, })
def _handle_or_return_error(func, request, error_message): try: return func(request) except Exception as exc: if isinstance(exc, ValidationError): error_message += "\n" + "\n".join(force_text(err) for err in exc.messages) else: error_message += " {}".format(exc) return JsonResponse({"success": False, "errorMessage": error_message}, status=400)