Source code for shuup.campaigns.models.basket_line_effects

# 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 django.db import models
from django.utils.translation import ugettext_lazy as _
from uuid import uuid4

from shuup.core.fields import MoneyValueField, QuantityField
from shuup.core.models import Category, OrderLineType, PolymorphicShuupModel, Product, ShopProduct
from shuup.core.order_creator._source import LineSource


[docs]class BasketLineEffect(PolymorphicShuupModel): identifier = None model = None admin_form_class = None campaign = models.ForeignKey( on_delete=models.CASCADE, to="BasketCampaign", related_name="line_effects", verbose_name=_("campaign") )
[docs] def get_discount_lines(self, order_source, original_lines, supplier): """ Applies the effect based on given `order_source` :return: amount of discount to accumulate for the product :rtype: Iterable[shuup.core.order_creator.SourceLine] """ raise NotImplementedError("Error! Not implemented: `BasketLineEffect` -> `get_discount_lines()`")
[docs]class FreeProductLine(BasketLineEffect): identifier = "free_product_line_effect" model = Product name = _("Free Product(s)") quantity = QuantityField(default=1, verbose_name=_("quantity")) products = models.ManyToManyField(Product, verbose_name=_("product")) @property def description(self): return _("Select product(s) to give free.") @property def values(self): return self.products @values.setter def values(self, values): self.products = values
[docs] def get_discount_lines(self, order_source, original_lines, supplier): lines = [] shop = order_source.shop for product in self.products.all(): try: shop_product = product.get_shop_instance(shop, allow_cache=True) except ShopProduct.DoesNotExist: continue if not supplier: supplier = shop_product.get_supplier( order_source.customer, self.quantity, order_source.shipping_address ) if not shop_product.is_orderable( supplier=supplier, customer=order_source.customer, quantity=self.quantity, allow_cache=False ): continue line_data = dict( line_id="free_product_%s" % uuid4().hex, type=OrderLineType.PRODUCT, quantity=self.quantity, shop=shop, text=("%s (%s)" % (product.name, self.campaign.public_name)), base_unit_price=shop.create_price(0), product=product, sku=product.sku, supplier=supplier, line_source=LineSource.DISCOUNT_MODULE, ) lines.append(order_source.create_line(**line_data)) return lines
[docs]class DiscountFromProduct(BasketLineEffect): identifier = "discount_from_product_line_effect" model = Product name = _("Discount from Product") per_line_discount = models.BooleanField( default=True, verbose_name=_("per line discount"), help_text=_("Disable this if you want to give discount for each matched product."), ) discount_amount = MoneyValueField( default=None, blank=True, null=True, verbose_name=_("discount amount"), help_text=_("Flat amount of discount.") ) products = models.ManyToManyField(Product, verbose_name=_("product")) @property def description(self): return _("Select discount amount and products.")
[docs] def get_discount_lines(self, order_source, original_lines, supplier): product_ids = self.products.values_list("pk", flat=True) campaign = self.campaign if not supplier: supplier = getattr(campaign, "supplier", None) for line in original_lines: if supplier and line.supplier != supplier: continue if not line.type == OrderLineType.PRODUCT: continue if line.product.pk not in product_ids: continue base_price = line.base_unit_price.value * line.quantity amnt = (self.discount_amount * line.quantity) if not self.per_line_discount else self.discount_amount # we use min() to limit the amount of discount to the products price discount_price = order_source.create_price(min(base_price, amnt)) if not line.discount_amount or line.discount_amount < discount_price: line.discount_amount = discount_price # check for minimum price, if set, and change the discount amount _limit_discount_amount_by_min_price(line, order_source) return []
[docs]class DiscountFromCategoryProducts(BasketLineEffect): identifier = "discount_from_category_products_line_effect" model = Category name = _("Discount from Category products") discount_amount = MoneyValueField( default=None, blank=True, null=True, verbose_name=_("discount amount"), help_text=_("Flat amount of discount.") ) discount_percentage = models.DecimalField( max_digits=6, decimal_places=5, blank=True, null=True, verbose_name=_("discount percentage"), help_text=_("The discount percentage for this campaign."), ) category = models.ForeignKey(on_delete=models.CASCADE, to=Category, verbose_name=_("category")) @property def description(self): return _( "Select discount amount and category. " "Please note that the discount will be given to all matching products in basket." )
[docs] def get_discount_lines(self, order_source, original_lines, supplier): # noqa (C901) if not (self.discount_percentage or self.discount_amount): return [] campaign = self.campaign if not supplier: supplier = getattr(campaign, "supplier", None) product_ids = self.category.shop_products.values_list("product_id", flat=True) for line in original_lines: # Use original lines since we don't want to discount free product lines if supplier and line.supplier != supplier: continue if not line.type == OrderLineType.PRODUCT: continue if line.product.variation_parent: if line.product.variation_parent.pk not in product_ids and line.product.pk not in product_ids: continue else: if line.product.pk not in product_ids: continue amount = order_source.zero_price.value base_price = line.base_unit_price.value * line.quantity if self.discount_amount: amount = self.discount_amount * line.quantity elif self.discount_percentage: amount = base_price * self.discount_percentage # we use min() to limit the amount of discount to base price # also in percentage, since one can configure 150% of discount discount_price = order_source.create_price(min(base_price, amount)) if not line.discount_amount or line.discount_amount < discount_price: line.discount_amount = discount_price # check for minimum price, if set, and change the discount amount _limit_discount_amount_by_min_price(line, order_source) return []
def _limit_discount_amount_by_min_price(line, order_source): """ Changes the Order Line discount amount if the discount amount exceeds the minimium total price set by `minimum_price` constraint in `ShopProduct`. :param shuup.core.order_creator.SourceLine line: the line to limit the discount :param shuup.core.order_source.OrderSource order_source: the order source """ # make sure the discount respects the minimum price of the product, if set try: shop_product = line.product.get_shop_instance(order_source.shop, allow_cache=True) if shop_product.minimum_price: min_total = shop_product.minimum_price.value * line.quantity base_price = line.base_unit_price.value * line.quantity # check if the discount makes the line less than the minimum total if (base_price - line.discount_amount.value) < min_total: line.discount_amount = order_source.create_price(base_price - min_total) except ShopProduct.DoesNotExist: pass