import decimal
import six
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_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 as exc:
raise ValidationError(
"Error! Product is not available in this shop.",
code="product_not_available_in_shop",
) from exc
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 isinstance(quantity, decimal.Decimal):
if quantity != quantity.to_integral_value():
msg = _(f"Error! The quantity {quantity} is not allowed. Please use an integer value.")
raise ValidationError(msg, code="invalid_quantity")
quantity = int(quantity)
else:
if quantity != int(quantity):
msg = _(f"Error! The quantity {quantity} is not allowed. Please use an integer value.")
raise ValidationError(msg, code="invalid_quantity")
quantity = int(quantity)
except (ValueError, decimal.InvalidOperation) as exc:
raise ValidationError(
_(f"Error! The quantity {quantity} is not valid."),
code="invalid_quantity",
) from exc
if quantity <= 0:
raise ValidationError(
_(f"Error! The quantity {quantity} is not valid, should be bigger than zero."),
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.pk, 0)
total_child_quantity = quantity * child_quantity
try:
sp = child_product.get_shop_instance(shop=request.shop)
except ShopProduct.DoesNotExist as exc:
raise ProductNotOrderableProblem(
f"Error! Product {child_product} is not available in shop {request.shop}."
) from exc
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 = {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()