Source code for shuup.core.basket.commands

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. 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 six
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _

from shuup.core.excs import ProductNotOrderableProblem
from shuup.core.models import (
    AnonymousContact,
    CompanyContact,
    PersonContact,
    Product,
    ProductMode,
    ProductVariationResult,
    ShopProduct,
)
from shuup.core.order_creator import is_code_usable
from shuup.utils.django_compat import is_authenticated
from shuup.utils.importing import cached_load
from shuup.utils.numbers import parse_decimal_string


# TODO: Refactor handle_add, it's too complex
[docs]def handle_add( # noqa (C901) request, basket, product_id, quantity=1, unit_type="internal", supplier_id=None, **kwargs ): """ Handle adding a product to the basket. :param product_id: product ID to add (or if `child_product_id` is truey, the parent ID). :param quantity: quantity of products to add. :param child_product_id: child product ID to add (if truey). :param supplier_id: The supplier ID for the new line. If None, the first supplier is used. """ product_id = int(product_id) product = get_object_or_404(Product, pk=product_id) if product.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT): raise ValidationError("Error! Invalid product.", code="invalid_product") try: shop_product = product.get_shop_instance(shop=request.shop) except ShopProduct.DoesNotExist: raise ValidationError("Error! Product is not available in this shop.", code="product_not_available_in_shop") if supplier_id: supplier = shop_product.suppliers.enabled(shop=shop_product.shop).filter(pk=supplier_id).first() else: supplier = shop_product.get_supplier(basket.customer, quantity, basket.shipping_address) if not supplier: raise ValidationError("Error! Invalid supplier.", code="invalid_supplier") try: quantity = parse_decimal_string(quantity) if unit_type == "display": quantity = shop_product.unit.from_display(quantity) if not product.sales_unit.allow_fractions: if quantity % 1 != 0: msg = _("Error! The quantity `%f` is not allowed. " "Please use an integer value.") % quantity raise ValidationError(msg, code="invalid_quantity") quantity = int(quantity) except (ValueError, decimal.InvalidOperation): raise ValidationError(_("Error! The quantity `%s` is not valid.") % quantity, code="invalid_quantity") if quantity <= 0: raise ValidationError( _("Error! The quantity `%s` is not valid, " "should be bigger than zero.") % quantity, code="invalid_quantity", ) product_ids_and_quantities = basket.get_product_ids_and_quantities() already_in_basket_qty = product_ids_and_quantities.get(product.id, 0) shop_product.raise_if_not_orderable( supplier=supplier, quantity=(already_in_basket_qty + quantity), customer=basket.customer ) # If the product is a package parent, also check child products if product.is_package_parent(): for child_product, child_quantity in six.iteritems(product.get_package_child_to_quantity_map()): already_in_basket_qty = product_ids_and_quantities.get(child_product.id, 0) total_child_quantity = quantity * child_quantity try: sp = child_product.get_shop_instance(shop=request.shop) except ShopProduct.DoesNotExist: raise ProductNotOrderableProblem( "Error! Product %s is not available in shop %s." % (child_product, request.shop) ) sp.raise_if_not_orderable( supplier=supplier, quantity=(already_in_basket_qty + total_child_quantity), customer=basket.customer ) # TODO: Hook/extension point # if product.form: # return { # "error": u"Form required", # "return": reverse_GET("product-form", kwargs={"pk": product.pk}, GET={"n": quantity}) # } add_product_kwargs = { "product": product, "quantity": quantity, "supplier": supplier, "shop": request.shop, "force_new_line": kwargs.get("force_new_line", False), "extra": kwargs.get("extra"), "parent_line": kwargs.get("parent_line"), } line = basket.add_product(**add_product_kwargs) return {"ok": basket.smart_product_count, "line_id": line.line_id, "added": quantity}
[docs]def handle_add_var(request, basket, product_id, quantity=1, unit_type="internal", **kwargs): """ Handle adding a complex variable product into the basket by resolving the combination variables. This actually uses `kwargs`, expecting `var_XXX=YYY` to exist there, where `XXX` is the PK of a ProductVariationVariable and YYY is the PK of a ProductVariationVariableValue. Confused yet? :param quantity: Quantity of the resolved variation to add. :param kwargs: Expected to contain `var_*` values, see above. """ # Resolve the combination... vars = dict((int(k.split("_")[-1]), int(v)) for (k, v) in six.iteritems(kwargs) if k.startswith("var_")) var_product = ProductVariationResult.resolve(product_id, combination=vars) if not var_product: raise ValidationError(_("Error! This variation is not available."), code="invalid_variation_combination") # and hand it off to handle_add like we're used to return handle_add( request=request, basket=basket, product_id=var_product.pk, quantity=quantity, unit_type=unit_type, **kwargs )
[docs]def handle_del(request, basket, line_id, **kwargs): """ Handle deleting a distinct order line from the basket given its unique line ID. :param line_id: The line ID to delete. :return: """ return {"ok": basket.delete_line(line_id)}
[docs]def handle_clear(request, basket, **kwargs): """ Handle fully clearing the basket. """ basket.clear_all() return {"ok": True}
[docs]def handle_add_campaign_code(request, basket, code): if not code: return {"ok": False} if is_code_usable(basket, code): return {"ok": basket.add_code(code)} return {"ok": False}
[docs]def handle_remove_campaign_code(request, basket, code): if not code: return {"ok": False} return {"ok": basket.remove_code(code)}
[docs]def handle_clear_campaign_codes(request, basket): return {"ok": basket.clear_codes()}
[docs]def handle_set_customer(request, basket, customer, orderer=None): # noqa (C901) if isinstance(customer, AnonymousContact): basket.orderer = AnonymousContact() else: if not customer.is_active: raise ValidationError(_("Customer is not active."), code="invalid_customer") if customer.pk: customer_shops = customer.shops.all() if customer_shops and basket.shop not in customer_shops: raise ValidationError( _("Shop does not have all the necessary permissions for this customer."), code="invalid_customer_shop", ) if is_authenticated(request.user): request_contact = PersonContact.objects.filter(user=request.user).first() or AnonymousContact() else: request_contact = AnonymousContact() is_superuser = getattr(request.user, "is_superuser", False) is_staff = getattr(request.user, "is_staff", False) and request.user in basket.shop.staff_members.all() if isinstance(customer, PersonContact): # to set a customer different from the current one # he must be a super user or at least staff # but allow to set a customer when the current one is not authenticated if customer != request_contact and is_authenticated(request.user): if not (is_superuser or is_staff): raise ValidationError( _("You don't have the required permission to assign this customer."), code="no_permission" ) basket.orderer = customer elif isinstance(customer, CompanyContact): if not orderer: raise ValidationError( _("You must specify the order, in which customer is a company."), code="invalid_orderer" ) # make sure the company is saved in db valid_customer = customer and customer.pk if not valid_customer: raise ValidationError(_("Invalid customer."), code="invalid_customer") company_members = customer.members.all() if orderer not in company_members: raise ValidationError(_("Orderer is not a member of the company."), code="orderer_not_company_member") elif not (is_superuser or is_staff) and request_contact not in company_members: raise ValidationError(_("You are not a member of the company."), code="not_company_member") basket.orderer = orderer basket.customer = customer return {"ok": True}
[docs]def handle_update(request, basket, **kwargs): """ Handle updating a basket, i.e. deleting some lines or updating quantities. This dispatches further to whatever is declared by the `SHUUP_BASKET_UPDATE_METHODS_SPEC` configuration entry. """ methods = cached_load("SHUUP_BASKET_UPDATE_METHODS_SPEC")(request=request, basket=basket) prefix_method_dict = methods.get_prefix_to_method_map() basket_changed = False # If any POST items match a prefix defined in prefix_method_dict, call the appropriate model method. for key, value in six.iteritems(kwargs): for prefix, method in six.iteritems(prefix_method_dict): if key.startswith(prefix): line_id = key[len(prefix) :] line = basket.find_line_by_line_id(line_id) field_changed = method(key=key, value=value, line=line) basket_changed = basket_changed or field_changed break if basket_changed: # pragma: no branch basket.clean_empty_lines() basket.dirty = True basket.uncache()