Source code for shuup.core.models._products

# -*- 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
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 get_public_media(self): return self.media.filter(enabled=True, public=True).exclude(kind=ProductMediaKind.IMAGE)
[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")