Source code for shuup.core.models._product_variation

# -*- 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 hashlib
import itertools
import six
from collections import defaultdict
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from parler.models import TranslatableModel, TranslatedFields

from shuup.core.fields import InternalIdentifierField
from shuup.utils.django_compat import force_bytes, force_text
from shuup.utils.models import SortableMixin


class ProductVariationLinkStatus(Enum):
    INVISIBLE = 0
    VISIBLE = 1

    class Labels:
        INVISIBLE = _("invisible")
        VISIBLE = _("visible")


@python_2_unicode_compatible
class ProductVariationVariable(TranslatableModel, SortableMixin):
    product = models.ForeignKey(
        "Product", related_name="variation_variables", on_delete=models.CASCADE, verbose_name=_("product")
    )
    identifier = InternalIdentifierField(unique=False)
    translations = TranslatedFields(
        name=models.CharField(max_length=128, verbose_name=_("name")),
    )

    class Meta:
        verbose_name = _("variation variable")
        verbose_name_plural = _("variation variables")
        unique_together = (
            (
                "product",
                "identifier",
            ),
        )
        ordering = ("ordering",)

    def __str__(self):
        return force_text(self.safe_translation_getter("name") or self.identifier or repr(self))


@python_2_unicode_compatible
class ProductVariationVariableValue(TranslatableModel, SortableMixin):
    variable = models.ForeignKey(
        ProductVariationVariable, related_name="values", on_delete=models.CASCADE, verbose_name=_("variation variable")
    )
    identifier = InternalIdentifierField(unique=False)

    translations = TranslatedFields(
        value=models.CharField(max_length=128, verbose_name=_("value")),
    )

    class Meta:
        verbose_name = _("variation value")
        verbose_name_plural = _("variation values")
        unique_together = (
            (
                "variable",
                "identifier",
            ),
        )
        ordering = ("ordering",)

    def __str__(self):
        return force_text(self.safe_translation_getter("value") or self.identifier or repr(self))


class ProductVariationResult(models.Model):
    product = models.ForeignKey(
        "Product", related_name="variation_result_supers", on_delete=models.CASCADE, verbose_name=_("product")
    )
    combination_hash = models.CharField(max_length=40, unique=True, db_index=True, verbose_name=_("combination hash"))
    result = models.ForeignKey(
        "Product", related_name="variation_result_subs", on_delete=models.CASCADE, verbose_name=_("result")
    )
    status = EnumIntegerField(
        ProductVariationLinkStatus, db_index=True, default=ProductVariationLinkStatus.VISIBLE, verbose_name=_("status")
    )

    @classmethod
[docs] def resolve(cls, parent_product, combination): pvr = cls.objects.filter( product=parent_product, combination_hash=hash_combination(combination), status=ProductVariationLinkStatus.VISIBLE, ).first() if pvr: return pvr.result
class Meta: verbose_name = _("variation result") verbose_name_plural = _("variation results") def hash_combination(combination): """ Calculate the combination hash for a given mapping of variable PKs to value PKs. :param combination: Combination dict {pvv_pk: pvvv_pk} :type combination: dict[int, int] :return: Hash string :rtype: str """ bits = [] for variable, value in six.iteritems(combination): if isinstance(variable, six.integer_types) and isinstance(value, six.integer_types): bits.append("%d=%d" % (variable, value)) else: bits.append("%d=%d" % (variable.pk, value.pk)) bits.sort() raw_combination = ",".join(bits) hashed_combination = hashlib.sha1(force_bytes(raw_combination)).hexdigest() return hashed_combination def get_combination_hash_from_variable_mapping(parent, variables): """ Create a combination hash from a mapping of variable identifiers to value identifiers. If variables and values with the given identifier do not exist, they are created on the go. :param parent: Parent product :type parent: shuup.core.models.Product :param variables: Dict of {variable identifier: value identifier} for complex variable linkage :type variables: dict :return: Combination hash :rtype: str """ mapping = {} for variable_identifier, value_identifier in variables.items(): variable, _ = ProductVariationVariable.objects.get_or_create( product=parent, identifier=force_text(variable_identifier) ) value, _ = ProductVariationVariableValue.objects.get_or_create( variable=variable, identifier=force_text(value_identifier) ) mapping[variable] = value return hash_combination(mapping) def get_all_available_combinations(product): results = product.get_available_variation_results() values_by_variable = defaultdict(list) values = ( ProductVariationVariableValue.objects.filter(variable__product=product) .prefetch_related("translations", "variable", "variable__translations") .order_by("ordering") ) for val in values: values_by_variable[val.variable].append(val) if not values_by_variable: return variables_list, value_sets_list = zip(*values_by_variable.items()) for value_set_combo in itertools.product(*value_sets_list): variable_to_value = dict(zip(variables_list, value_set_combo)) sorted_variable_to_value = sorted(variable_to_value.items(), key=lambda varval: varval[0].ordering) text_description = ", ".join(sorted("%s: %s" % (var, val) for (var, val) in sorted_variable_to_value)) sku_part = "-".join(slugify(force_text(val))[:6] for (var, val) in sorted_variable_to_value) hash = hash_combination(variable_to_value) yield { "variable_to_value": variable_to_value, "hash": hash, "text_description": text_description, "sku_part": sku_part, "result_product_pk": results.get(hash), }