# -*- 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, with_statement
import six
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import format_lazy
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from parler.managers import TranslatableQuerySet
from parler.models import TranslatableModel, TranslatedFields
from shuup.core.excs import ImpossibleProductModeException
from shuup.core.fields import InternalIdentifierField, MeasurementField
from shuup.core.signals import post_clean, pre_clean
from shuup.core.specs.product_kind import DefaultProductKindSpec, get_product_kind_choices
from shuup.core.taxing import TaxableItem
from shuup.core.utils.slugs import generate_multilanguage_slugs
from shuup.utils.analog import LogEntryKind, define_log_model
from shuup.utils.django_compat import force_text
from ._attributes import AppliedAttribute, AttributableMixin, Attribute
from ._product_media import ProductMediaKind
from ._product_packages import ProductPackageLink
from ._product_variation import (
ProductVariationResult,
ProductVariationVariable,
get_all_available_combinations,
get_combination_hash_from_variable_mapping,
)
# TODO (3.0): This should be extandable
class ProductMode(Enum):
NORMAL = 0
PACKAGE_PARENT = 1
SIMPLE_VARIATION_PARENT = 2
VARIABLE_VARIATION_PARENT = 3
VARIATION_CHILD = 4
SUBSCRIPTION = 5 # This is like package_parent and all the functionality is same under the hood.
class Labels:
NORMAL = _("normal")
PACKAGE_PARENT = _("package parent")
SIMPLE_VARIATION_PARENT = _("variation parent (simple)")
VARIABLE_VARIATION_PARENT = _("variation parent (variable)")
VARIATION_CHILD = _("variation child")
SUBSCRIPTION = _("subscription")
class ProductVisibility(Enum):
VISIBLE_TO_ALL = 1
VISIBLE_TO_LOGGED_IN = 2
VISIBLE_TO_GROUPS = 3
class Labels:
VISIBLE_TO_ALL = _("visible to all")
VISIBLE_TO_LOGGED_IN = _("visible to logged in")
VISIBLE_TO_GROUPS = _("visible to groups")
# Deprecated. Used in old migrations
class StockBehavior(Enum):
UNSTOCKED = 0
STOCKED = 1
class Labels:
STOCKED = _("stocked")
UNSTOCKED = _("unstocked")
class ProductCrossSellType(Enum):
RECOMMENDED = 1
RELATED = 2
COMPUTED = 3
BOUGHT_WITH = 4
class Labels:
RECOMMENDED = _("recommended")
RELATED = _("related")
COMPUTED = _("computed")
BOUGHT_WITH = _("bought with")
class ShippingMode(Enum):
NOT_SHIPPED = 0
SHIPPED = 1
class Labels:
NOT_SHIPPED = _("not shipped (non-deliverable)")
SHIPPED = _("shipped (deliverable)")
class ProductVerificationMode(Enum):
NO_VERIFICATION_REQUIRED = 0
ADMIN_VERIFICATION_REQUIRED = 1
THIRD_PARTY_VERIFICATION_REQUIRED = 2
class Labels:
NO_VERIFICATION_REQUIRED = _("no verification required")
ADMIN_VERIFICATION_REQUIRED = _("admin verification required")
THIRD_PARTY_VERIFICATION_REQUIRED = _("third party verification required")
@python_2_unicode_compatible
class ProductType(TranslatableModel):
identifier = InternalIdentifierField(unique=True)
translations = TranslatedFields(
name=models.CharField(
max_length=64,
verbose_name=_("name"),
help_text=_(
"Enter a descriptive name for your product type. "
"Products and attributes for products of this type can be found under this name."
),
),
)
attributes = models.ManyToManyField(
"Attribute",
blank=True,
related_name="product_types",
verbose_name=_("attributes"),
help_text=_(
"Select attributes that go with your product type. To change available attributes search for `Attributes`."
),
)
class Meta:
verbose_name = _("product type")
verbose_name_plural = _("product types")
def __str__(self):
return force_text(self.safe_translation_getter("name") or self.identifier)
class ProductQuerySet(TranslatableQuerySet):
def _visible(self, shop, customer, language=None, invisible_modes=[ProductMode.VARIATION_CHILD]):
root = self.language(language) if language else self
qs = root.all().filter(shop_products__shop=shop)
if customer and customer.is_all_seeing:
if invisible_modes:
qs = qs.exclude(mode__in=invisible_modes)
else:
from ._product_shops import ShopProductVisibility
qs = qs.exclude(
Q(shop_products__visibility=ShopProductVisibility.NOT_VISIBLE)
| Q(shop_products__available_until__lte=now())
)
if invisible_modes:
qs = qs.exclude(mode__in=invisible_modes)
if customer and not customer.is_anonymous:
visible_to_logged_in_q = Q(
shop_products__visibility_limit__in=(
ProductVisibility.VISIBLE_TO_ALL,
ProductVisibility.VISIBLE_TO_LOGGED_IN,
)
)
visible_to_my_groups_q = Q(
shop_products__visibility_limit=ProductVisibility.VISIBLE_TO_GROUPS,
shop_products__visibility_groups__in=customer.groups.all(),
)
qs = qs.filter(visible_to_logged_in_q | visible_to_my_groups_q)
else:
qs = qs.filter(shop_products__visibility_limit=ProductVisibility.VISIBLE_TO_ALL)
qs = qs.select_related(*Product.COMMON_SELECT_RELATED).prefetch_related(*Product.COMMON_PREFETCH_RELATED)
return qs.exclude(deleted=True).exclude(type__isnull=True).distinct()
def _get_qs(self, shop, customer, language, visibility_type):
qs = self._visible(shop=shop, customer=customer, language=language)
if customer and customer.is_all_seeing:
return qs
else:
from ._product_shops import ShopProductVisibility
return qs.filter(
shop_products__shop=shop,
shop_products__visibility__in=(visibility_type, ShopProductVisibility.ALWAYS_VISIBLE),
)
def listed(self, shop, customer=None, language=None):
from ._product_shops import ShopProductVisibility
return self._get_qs(shop, customer, language, ShopProductVisibility.LISTED)
def searchable(self, shop, customer=None, language=None):
from ._product_shops import ShopProductVisibility
return self._get_qs(shop, customer, language, ShopProductVisibility.SEARCHABLE)
def visible(self, shop, customer=None, language=None):
return self._visible(shop, customer=customer, language=language, invisible_modes=[])
def all_except_deleted(self, language=None, shop=None):
qs = (self.language(language) if language else self).exclude(deleted=True).exclude(type__isnull=True)
if shop:
qs = qs.filter(shop_products__shop=shop)
qs = qs.select_related(*Product.COMMON_SELECT_RELATED).prefetch_related(*Product.COMMON_PREFETCH_RELATED)
return qs
@python_2_unicode_compatible
class Product(TaxableItem, AttributableMixin, TranslatableModel):
COMMON_SELECT_RELATED = ("sales_unit", "type", "primary_image", "tax_class", "manufacturer")
COMMON_PREFETCH_RELATED = ("translations",)
# Metadata
created_on = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name=_("created on"))
modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("modified on"))
deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_("deleted"))
# Behavior
kind = models.IntegerField(default=DefaultProductKindSpec.value, choices=get_product_kind_choices(), db_index=True)
mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_("mode"))
variation_parent = models.ForeignKey(
"self",
null=True,
blank=True,
related_name="variation_children",
on_delete=models.PROTECT,
verbose_name=_("variation parent"),
)
shipping_mode = EnumIntegerField(
ShippingMode,
default=ShippingMode.SHIPPED,
verbose_name=_("shipping mode"),
help_text=_("Set to `shipped` if the product requires shipment."),
)
sales_unit = models.ForeignKey(
"SalesUnit",
verbose_name=_("sales unit"),
blank=True,
null=True,
on_delete=models.PROTECT,
help_text=_(
"Select a sales unit for your product. "
"This is shown in your store front and is used to determine whether the product can be purchased using "
"fractional amounts. To change settings search for `Sales Units`."
),
)
tax_class = models.ForeignKey(
"TaxClass",
verbose_name=_("tax class"),
on_delete=models.PROTECT,
help_text=_(
"Select a tax class for your product. "
"The tax class is used to determine which taxes to apply to your product. "
"Define tax classes by searching for `Tax Classes`. "
"To define the rules by which taxes are applied search for `Tax Rules`."
),
)
# Identification
type = models.ForeignKey(
"ProductType",
related_name="products",
on_delete=models.SET_NULL,
db_index=True,
verbose_name=_("product type"),
null=True,
help_text=_(
"Select a product type for your product. "
"These allow you to configure custom attributes to help with classification and analysis."
),
)
sku = models.CharField(
db_index=True,
max_length=128,
verbose_name=_("SKU"),
unique=True,
help_text=_(
"Enter a SKU (Stock Keeping Unit) number for your product. "
"This is a product identification code that helps you track products through your inventory "
"and analyze their movement. People often use the product's barcode number, "
"but you can set up any numerical system you want to keep track of products."
),
)
gtin = models.CharField(
blank=True,
max_length=40,
verbose_name=_("GTIN"),
help_text=_(
"You can enter a Global Trade Item Number. "
"This is typically a 14 digit identification number for all of your trade items. "
"It can often be found by the barcode."
),
)
barcode = models.CharField(
blank=True,
max_length=40,
verbose_name=_("barcode"),
help_text=_(
"You can enter the barcode number for your product. "
"This is useful for inventory/stock tracking and analysis."
),
)
accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_("bookkeeping account"))
profit_center = models.CharField(max_length=32, verbose_name=_("profit center"), blank=True)
cost_center = models.CharField(max_length=32, verbose_name=_("cost center"), blank=True)
# Physical dimensions
width = MeasurementField(
unit=settings.SHUUP_LENGTH_UNIT,
verbose_name=format_lazy(_("width ({})"), settings.SHUUP_LENGTH_UNIT),
help_text=_(
"Set the measured width of your product or product packaging. "
"This will provide customers with the product size and help with calculating shipping costs."
),
)
height = MeasurementField(
unit=settings.SHUUP_LENGTH_UNIT,
verbose_name=format_lazy(_("height ({})"), settings.SHUUP_LENGTH_UNIT),
help_text=_(
"Set the measured height of your product or product packaging. "
"This will provide customers with the product size and help with calculating shipping costs."
),
)
depth = MeasurementField(
unit=settings.SHUUP_LENGTH_UNIT,
verbose_name=format_lazy(_("depth ({})"), settings.SHUUP_LENGTH_UNIT),
help_text=_(
"Set the measured depth or length of your product or product packaging. "
"This will provide customers with the product size and help with calculating shipping costs."
),
)
net_weight = MeasurementField(
unit=settings.SHUUP_MASS_UNIT,
verbose_name=format_lazy(_("net weight ({})"), settings.SHUUP_MASS_UNIT),
help_text=_(
"Set the measured weight of your product WITHOUT its packaging. "
"This will provide customers with the actual product's weight."
),
)
gross_weight = MeasurementField(
unit=settings.SHUUP_MASS_UNIT,
verbose_name=format_lazy(_("gross weight ({})"), settings.SHUUP_MASS_UNIT),
help_text=_(
"Set the measured gross weight of your product WITH its packaging. "
"This will help with calculating shipping costs."
),
)
# Misc.
manufacturer = models.ForeignKey(
"Manufacturer",
blank=True,
null=True,
verbose_name=_("manufacturer"),
on_delete=models.SET_NULL,
help_text=_("Select a manufacturer for your product. To define these, search for `Manufacturers`."),
)
primary_image = models.ForeignKey(
"ProductMedia",
null=True,
blank=True,
related_name="primary_image_for_products",
on_delete=models.SET_NULL,
verbose_name=_("primary image"),
)
translations = TranslatedFields(
name=models.CharField(
max_length=256,
verbose_name=_("name"),
db_index=True,
help_text=_("Enter a descriptive name for your product. This will be its title in your store front."),
),
description=models.TextField(
blank=True,
verbose_name=_("description"),
help_text=_(
"To make your product stand out, give it an awesome description. "
"This is what will help your shoppers learn about your products. "
"It will also help shoppers find them in the store and on the web."
),
),
short_description=models.CharField(
max_length=150,
blank=True,
verbose_name=_("short description"),
help_text=_(
"Enter a short description for your product. The short description will "
"be used to get the attention of your customer with a small, but "
"precise description of your product. It also helps with getting more "
"traffic via search engines."
),
),
slug=models.SlugField(
verbose_name=_("slug"),
max_length=255,
blank=True,
null=True,
help_text=_(
"Enter a URL slug for your product. Slug is user- and search engine-friendly short text "
"used in a URL to identify and describe a resource. In this case it will determine "
"what your product page URL in the browser address bar will look like. "
"A default will be created using the product name."
),
),
keywords=models.TextField(
blank=True,
verbose_name=_("keywords"),
help_text=_(
"You can enter keywords that describe your product. "
"This will help your shoppers learn about your products. "
"It will also help shoppers find them in the store and on the web."
),
),
variation_name=models.CharField(
max_length=128,
blank=True,
verbose_name=_("variation name"),
help_text=_(
"You can enter a name for the variation of your product. "
"This could be for example different colors, sizes or versions. "
"To manage variations, at the top of the the individual product page, "
"click `Actions` -> `Manage Variations`."
),
),
)
objects = ProductQuerySet.as_manager()
class Meta:
ordering = ("-id",)
verbose_name = _("product")
verbose_name_plural = _("products")
def __str__(self):
try:
return "%s" % self.name
except ObjectDoesNotExist:
return self.sku
[docs] def get_shop_instance(self, shop, allow_cache=False):
"""
:type shop: shuup.core.models.Shop
:rtype: shuup.core.models.ShopProduct
"""
from shuup.core.utils import context_cache
key, val = context_cache.get_cached_value(
identifier="shop_product", item=self, context={"shop": shop}, allow_cache=allow_cache
)
if val is not None:
return val
shop_inst = self.shop_products.get(shop_id=shop.id)
shop_inst.product = self
shop_inst.shop = shop
context_cache.set_cached_value(key, shop_inst)
return shop_inst
[docs] def get_priced_children(self, context, quantity=1):
"""
Get child products with price infos sorted by price.
:rtype: list[(Product,PriceInfo)]
:return:
List of products and their price infos sorted from cheapest to
most expensive.
"""
from shuup.core.models import ShopProduct
priced_children = []
shop_product_query = Q(
shop=context.shop,
product_id__in=self.variation_children.visible(shop=context.shop, customer=context.customer).values_list(
"id", flat=True
),
)
for shop_product in ShopProduct.objects.filter(shop_product_query):
if shop_product.is_orderable(supplier=None, customer=context.customer, quantity=1):
child = shop_product.product
priced_children.append((child, child.get_price_info(context, quantity=quantity)))
return sorted(priced_children, key=(lambda x: x[1].price))
[docs] def get_cheapest_child_price(self, context, quantity=1):
price_info = self.get_cheapest_child_price_info(context, quantity)
if price_info:
return price_info.price
[docs] def get_child_price_range(self, context, quantity=1):
"""
Get the prices for cheapest and the most expensive child.
The attribute used for sorting is `PriceInfo.price`.
Return (`None`, `None`) if `self.variation_children` do not exist.
This is because we cannot return anything sensible.
:type context: shuup.core.pricing.PricingContextable
:type quantity: int
:return: a tuple of prices.
:rtype: (shuup.core.pricing.Price, shuup.core.pricing.Price)
"""
items = []
for child in self.variation_children.visible(shop=context.shop, customer=context.customer):
items.append(child.get_price_info(context, quantity=quantity))
if not items:
return (None, None)
infos = sorted(items, key=lambda x: x.price)
return (infos[0].price, infos[-1].price)
[docs] def get_cheapest_child_price_info(self, context, quantity=1):
"""
Get the `PriceInfo` of the cheapest variation child.
The attribute used for sorting is `PriceInfo.price`.
Return `None` if `self.variation_children` do not exist.
This is because we cannot return anything sensible.
:type context: shuup.core.pricing.PricingContextable
:rtype: shuup.core.pricing.PriceInfo
"""
items = []
for child in self.variation_children.visible(shop=context.shop, customer=context.customer):
items.append(child.get_price_info(context, quantity=quantity))
if not items:
return None
return sorted(items, key=lambda x: x.price)[0]
[docs] def get_price_info(self, context, quantity=1):
"""
Get `PriceInfo` object for the product in given context.
Returned `PriceInfo` object contains calculated `price` and
`base_price`. The calculation of prices is handled in the
current pricing module.
:type context: shuup.core.pricing.PricingContextable
:rtype: shuup.core.pricing.PriceInfo
"""
from shuup.core.pricing import get_price_info
return get_price_info(product=self, context=context, quantity=quantity)
[docs] def get_price(self, context, quantity=1):
"""
Get price of the product within given context.
.. note::
When the current pricing module implements pricing steps, it
is possible that ``p.get_price(ctx) * 123`` is not equal to
``p.get_price(ctx, quantity=123)``, since there could be
quantity discounts in effect, but usually they are equal.
:type context: shuup.core.pricing.PricingContextable
:rtype: shuup.core.pricing.Price
"""
return self.get_price_info(context, quantity).price
[docs] def get_base_price(self, context, quantity=1):
"""
Get base price of the product within given context.
Base price differs from the (effective) price when there are
discounts in effect.
:type context: shuup.core.pricing.PricingContextable
:rtype: shuup.core.pricing.Price
"""
return self.get_price_info(context, quantity=quantity).base_price
[docs] def get_available_attribute_queryset(self):
if self.type_id:
return self.type.attributes.visible()
else:
return Attribute.objects.none()
[docs] def get_available_variation_results(self):
"""
Get a dict of `combination_hash` to product ID of variable variation results.
:return: Mapping of combination hashes to product IDs.
:rtype: dict[str, int]
"""
return dict(
ProductVariationResult.objects.filter(product=self)
.filter(status=1)
.values_list("combination_hash", "result_id")
)
[docs] def get_all_available_combinations(self):
"""
Generate all available combinations of variation variables.
If the product is not a variable variation parent, the iterator is empty.
Because of possible combinatorial explosion this is a generator function.
(For example 6 variables with 5 options each, would explode to 15,625 combinations.)
:return: Iterable of combination information dicts.
:rtype: Iterable[dict]
"""
return get_all_available_combinations(self)
[docs] def clear_variation(self):
"""
Fully remove variation information.
Make this product a non-variation parent.
"""
self.simplify_variation()
for child in self.variation_children.all():
if child.variation_parent_id == self.pk:
child.unlink_from_parent()
self.verify_mode()
self.save()
[docs] def simplify_variation(self):
"""
Remove variation variables from a given variation parent, turning it
into a simple variation (or a normal product, if it has no children).
:param product: Variation parent, that shouldn't be variable any more.
:type product: shuup.core.models.Product
"""
ProductVariationVariable.objects.filter(product=self).delete()
ProductVariationResult.objects.filter(product=self).delete()
self.verify_mode()
self.save()
@staticmethod
def _get_slug_name(self, translation=None):
if self.deleted:
return None
return getattr(translation, "name", self.sku)
[docs] def save(self, *args, **kwargs):
self.clean()
if self.net_weight and self.net_weight > 0:
self.gross_weight = max(self.net_weight, self.gross_weight)
rv = super(Product, self).save(*args, **kwargs)
generate_multilanguage_slugs(self, self._get_slug_name)
return rv
[docs] def clean(self):
pre_clean.send(type(self), instance=self)
super(Product, self).clean()
post_clean.send(type(self), instance=self)
[docs] def delete(self, using=None):
raise NotImplementedError("Error! Not implemented: `Product` -> `delete()`. Use `soft_delete()` for products.")
[docs] def soft_delete(self, user=None):
if not self.deleted:
self.deleted = True
self.add_log_entry("Success! Deleted (soft).", kind=LogEntryKind.DELETION, user=user)
# Bypassing local `save()` on purpose.
super(Product, self).save(update_fields=("deleted",))
[docs] def verify_mode(self):
if ProductPackageLink.objects.filter(parent=self).exists():
self.mode = ProductMode.PACKAGE_PARENT
self.external_url = None
self.variation_children.clear()
elif ProductVariationVariable.objects.filter(product=self).exists():
self.mode = ProductMode.VARIABLE_VARIATION_PARENT
elif self.variation_children.filter(deleted=False).exists():
if ProductVariationResult.objects.filter(product=self).exists():
self.mode = ProductMode.VARIABLE_VARIATION_PARENT
else:
self.mode = ProductMode.SIMPLE_VARIATION_PARENT
self.external_url = None
ProductPackageLink.objects.filter(parent=self).delete()
elif self.variation_parent:
self.mode = ProductMode.VARIATION_CHILD
ProductPackageLink.objects.filter(parent=self).delete()
self.variation_children.clear()
self.external_url = None
else:
self.mode = ProductMode.NORMAL
[docs] def unlink_from_parent(self):
if self.variation_parent:
parent = self.variation_parent
self.variation_parent = None
self.save()
parent.verify_mode()
self.verify_mode()
self.save()
ProductVariationResult.objects.filter(result=self).delete()
return True
[docs] def link_to_parent(self, parent, variables=None, combination_hash=None):
"""
:param parent: The parent to link to.
:type parent: Product
:param variables: Optional dict of {variable identifier: value identifier} for a complex variable linkage.
:type variables: dict|None
:param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually
exclusive with `variables`.
:type combination_hash: str|None
"""
if combination_hash:
if variables:
raise ValueError("Error! `combination_hash` and `variables` are mutually exclusive.")
variables = True # Simplifies the below invariant checks
self._raise_if_cant_link_to_parent(parent, variables)
self.unlink_from_parent()
self.variation_parent = parent
self.verify_mode()
self.save()
if not parent.is_variation_parent():
parent.verify_mode()
parent.save()
if variables:
if not combination_hash: # No precalculated hash, need to figure that out
combination_hash = get_combination_hash_from_variable_mapping(parent, variables=variables)
pvr = ProductVariationResult.objects.create(product=parent, combination_hash=combination_hash, result=self)
if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT:
parent.verify_mode()
parent.save()
return pvr
else:
return True
def _raise_if_cant_link_to_parent(self, parent, variables):
"""
Validates relation possibility for `self.link_to_parent()`.
:param parent: parent product of self.
:type parent: Product
:param variables:
:type variables: dict|None
"""
if parent.is_variation_child():
raise ImpossibleProductModeException(
_("Multilevel parentage hierarchies aren't supported (parent is a child already)."), code="multilevel"
)
if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables:
raise ImpossibleProductModeException(
_("Parent is a variable variation parent, yet variables were not passed."), code="no_variables"
)
if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables:
raise ImpossibleProductModeException(
"Error! Parent is a simple variation parent, yet variables were passed.", code="extra_variables"
)
if self.mode == ProductMode.SIMPLE_VARIATION_PARENT:
raise ImpossibleProductModeException(
_("Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)."),
code="multilevel",
)
if self.mode == ProductMode.VARIABLE_VARIATION_PARENT:
raise ImpossibleProductModeException(
_("Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)."),
code="multilevel",
)
[docs] def make_package(self, package_def):
if self.mode != ProductMode.NORMAL:
raise ImpossibleProductModeException(
_("Product is currently not a normal product, and can't be turned into a package."), code="abnormal"
)
for child_product, quantity in six.iteritems(package_def):
if child_product.pk == self.pk:
raise ImpossibleProductModeException(_("Package can't contain itself."), code="content")
# :type child_product: Product
if child_product.is_variation_parent():
raise ImpossibleProductModeException(
_("Variation parents can't belong in the package."), code="abnormal"
)
if child_product.is_container():
raise ImpossibleProductModeException(_("Packages can't be nested."), code="multilevel")
if quantity <= 0:
raise ImpossibleProductModeException(_("Quantity %s is invalid.") % quantity, code="quantity")
ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity)
self.verify_mode()
[docs] def get_package_child_to_quantity_map(self):
if self.is_container():
product_id_to_quantity = dict(
ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity")
)
products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys()))
return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)}
return {}
[docs] def is_variation_parent(self):
return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT)
[docs] def is_variation_child(self):
return self.mode == ProductMode.VARIATION_CHILD
[docs] def get_variation_siblings(self):
return Product.objects.filter(variation_parent=self.variation_parent).exclude(pk=self.pk)
[docs] def is_package_parent(self):
return self.mode == ProductMode.PACKAGE_PARENT
[docs] def is_subscription_parent(self):
return self.mode == ProductMode.SUBSCRIPTION
[docs] def is_package_child(self):
return ProductPackageLink.objects.filter(child=self).exists()
[docs] def get_all_package_parents(self):
return Product.objects.filter(
pk__in=(ProductPackageLink.objects.filter(child=self).values_list("parent", flat=True))
)
[docs] def get_all_package_children(self):
return Product.objects.filter(
pk__in=(ProductPackageLink.objects.filter(parent=self).values_list("child", flat=True))
)
[docs] def is_container(self):
return self.is_package_parent() or self.is_subscription_parent()
ProductLogEntry = define_log_model(Product)
class ProductCrossSell(models.Model):
product1 = models.ForeignKey(
Product, related_name="cross_sell_1", on_delete=models.CASCADE, verbose_name=_("primary product")
)
product2 = models.ForeignKey(
Product, related_name="cross_sell_2", on_delete=models.CASCADE, verbose_name=_("secondary product")
)
weight = models.IntegerField(default=0, verbose_name=_("weight"))
type = EnumIntegerField(ProductCrossSellType, verbose_name=_("type"))
class Meta:
verbose_name = _("cross sell link")
verbose_name_plural = _("cross sell links")
class ProductAttribute(AppliedAttribute):
_applied_fk_field = "product"
product = models.ForeignKey(Product, related_name="attributes", on_delete=models.CASCADE, verbose_name=_("product"))
translations = TranslatedFields(
translated_string_value=models.TextField(blank=True, verbose_name=_("translated value"))
)
class Meta:
abstract = False
verbose_name = _("product attribute")
verbose_name_plural = _("product attributes")