Пример #1
0
class ShipmentProduct(models.Model):
    shipment = models.ForeignKey(Shipment,
                                 related_name='products',
                                 on_delete=models.PROTECT)
    product = models.ForeignKey("Product", related_name='shipments')
    quantity = QuantityField()
    unit_volume = MeasurementField(
        unit="m3"
    )  # volume is m^3, not mm^3, because mm^3 are tiny. like ants.
    unit_weight = MeasurementField(unit="g")

    class Meta:
        verbose_name = _('sent product')
        verbose_name_plural = _('sent products')

    def __str__(self):  # pragma: no cover
        return "%(quantity)s of '%(product)s' in Shipment #%(shipment_pk)s" % {
            'product': self.product,
            'quantity': self.quantity,
            'shipment_pk': self.shipment_id,
        }

    def cache_values(self):
        prod = self.product
        self.unit_volume = (prod.width * prod.height *
                            prod.depth) / CUBIC_MM_TO_CUBIC_METERS_DIVISOR
        self.unit_weight = prod.gross_weight
Пример #2
0
class Shipment(models.Model):
    order = models.ForeignKey("Order", related_name='shipments', on_delete=models.PROTECT)
    supplier = models.ForeignKey("Supplier", related_name='shipments', on_delete=models.PROTECT)
    created_on = models.DateTimeField(auto_now_add=True)
    status = EnumIntegerField(ShipmentStatus, default=ShipmentStatus.NOT_SENT)
    tracking_code = models.CharField(max_length=64, blank=True, verbose_name=_('tracking code'))
    description = models.CharField(max_length=255, blank=True)
    volume = MeasurementField(unit="m3")
    weight = MeasurementField(unit="kg")
    # TODO: documents = models.ManyToManyField(FilerFile)

    class Meta:
        verbose_name = _('shipment')
        verbose_name_plural = _('shipments')

    def __repr__(self):  # pragma: no cover
        return "<Shipment %s for order %s (tracking %r, created %s)>" % (
            self.pk, self.order_id, self.tracking_code, self.created_on
        )

    def cache_values(self):
        """
        (Re)cache `.volume` and `.weight` for this Shipment from the ShipmentProducts within.
        """
        total_volume = 0
        total_weight = 0
        for quantity, volume, weight in self.products.values_list("quantity", "unit_volume", "unit_weight"):
            total_volume += quantity * volume
            total_weight += quantity * weight
        self.volume = total_volume
        self.weight = total_weight

    @property
    def total_products(self):
        return (self.products.aggregate(quantity=models.Sum("quantity"))["quantity"] or 0)
Пример #3
0
class WeightBasedPriceRange(TranslatableModel):
    component = models.ForeignKey("WeightBasedPricingBehaviorComponent",
                                  related_name="ranges",
                                  on_delete=models.CASCADE)
    min_value = MeasurementField(unit="g",
                                 verbose_name=_("min weight"),
                                 blank=True,
                                 null=True)
    max_value = MeasurementField(unit="g",
                                 verbose_name=_("max weight"),
                                 blank=True,
                                 null=True)
    price_value = MoneyValueField()
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100, blank=True, verbose_name=_("description")), )

    def matches_to_value(self, value):
        return _is_in_range(value, self.min_value, self.max_value)
Пример #4
0
def test_measurement_field_doesnt_know_bananas():
    with pytest.raises(ImproperlyConfigured):
        scale = MeasurementField(unit="banana")
Пример #5
0
class Product(AttributableMixin, TranslatableModel):

    COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class")

    # Metadata
    created_on = models.DateTimeField(auto_now_add=True, editable=False)
    modified_on = models.DateTimeField(auto_now=True, editable=False)
    deleted = models.BooleanField(default=False, editable=False, db_index=True)

    # Behavior
    mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL)
    variation_parent = models.ForeignKey(
        "self", null=True, blank=True, related_name='variation_children',
        on_delete=models.PROTECT,
        verbose_name=_('variation parent'))
    stock_behavior = EnumIntegerField(StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock'))
    shipping_mode = EnumIntegerField(ShippingMode, default=ShippingMode.NOT_SHIPPED, verbose_name=_('shipping mode'))
    sales_unit = models.ForeignKey("SalesUnit", verbose_name=_('unit'), blank=True, null=True)
    tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'))

    # Identification
    type = models.ForeignKey(
        "ProductType", related_name='products',
        on_delete=models.PROTECT, db_index=True,
        verbose_name=_('product type'))
    sku = models.CharField(db_index=True, max_length=128, verbose_name=_('SKU'), unique=True)
    gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_('Global Trade Item Number'))
    barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode'))
    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)
    # Category is duplicated here because not all products necessarily belong in Shops (i.e. have
    # ShopProduct instances), but they should nevertheless be searchable by category in other
    # places, such as administration UIs.
    category = models.ForeignKey(
        "Category", related_name='primary_products', blank=True, null=True,
        verbose_name=_('primary category'),
        help_text=_("only used for administration and reporting"))

    # Physical dimensions
    width = MeasurementField(unit="mm", verbose_name=_('width (mm)'))
    height = MeasurementField(unit="mm", verbose_name=_('height (mm)'))
    depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)'))
    net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)'))
    gross_weight = MeasurementField(unit="g", verbose_name=_('gross weight (g)'))

    # Misc.
    purchase_price = MoneyField(verbose_name=_('purchase price'))
    suggested_retail_price = MoneyField(verbose_name=_('suggested retail price'))
    manufacturer = models.ForeignKey("Manufacturer", blank=True, null=True, verbose_name=_('manufacturer'))
    primary_image = models.ForeignKey(
        "ProductMedia", null=True, blank=True,
        related_name="primary_image_for_products",
        on_delete=models.SET_NULL)

    translations = TranslatedFields(
        name=models.CharField(max_length=256, verbose_name=_('name')),
        description=models.TextField(blank=True, verbose_name=_('description')),
        slug=models.SlugField(verbose_name=_('slug'), max_length=255, null=True),
        keywords=models.TextField(blank=True, verbose_name=_('keywords')),
        status_text=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('status text'),
            help_text=_(
                'This text will be shown alongside the product in the shop.'
                ' (Ex.: "Available in a month")')),
        variation_name=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('variation name'))
    )

    objects = ProductQuerySet.as_manager()

    class Meta:
        ordering = ('-id',)
        verbose_name = _('product')
        verbose_name_plural = _('products')

    def __str__(self):
        try:
            return u"%s" % self.name
        except ObjectDoesNotExist:
            return self.sku

    def get_shop_instance(self, shop):
        """
        :type shop: shoop.core.models.shops.Shop
        :rtype: shoop.core.models.product_shops.ShopProduct
        """
        shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {})
        cached = shop_inst_cache.get(shop)
        if cached:
            return cached

        shop_inst = self.shop_products.filter(shop=shop).first()
        if shop_inst:
            shop_inst._product_cache = self
            shop_inst._shop_cache = shop
            shop_inst_cache[shop] = shop_inst

        return shop_inst

    def get_cheapest_child_price(self, context, quantity=1):
        return sorted(
            c.get_price(context, quantity=quantity)
            for c in self.variation_children.all()
        )[0]

    def get_price(self, context, quantity=1):
        """
        :type context: shoop.core.contexts.PriceTaxContext
        :rtype: shoop.core.pricing.Price
        """
        from shoop.core.pricing import get_pricing_module
        module = get_pricing_module()
        pricing_context = module.get_context(context)
        return module.get_price(pricing_context, product_id=self.pk, quantity=quantity)

    def get_base_price(self):
        from shoop.core.pricing import get_pricing_module
        module = get_pricing_module()
        return module.get_base_price(product_id=self.pk)

    def get_taxed_price(self, context, quantity=1):
        """
        :type context: shoop.core.contexts.PriceTaxContext
        :rtype: shoop.core.pricing.TaxedPrice
        """
        from shoop.core import taxing
        module = taxing.get_tax_module()
        return module.determine_product_tax(context, self)

    def get_available_attribute_queryset(self):
        if self.type_id:
            return self.type.attributes.visible()
        else:
            return Attribute.objects.none()

    @staticmethod
    def _get_slug_name(self):
        if self.deleted:
            return None
        return (self.safe_translation_getter("name") or self.sku)

    def save(self, *args, **kwargs):
        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

    def delete(self, using=None):
        raise NotImplementedError("Not implemented: Use `soft_delete()` for products.")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user)
            # Bypassing local `save()` on purpose.
            super(Product, self).save(update_fields=("deleted",))

    def verify_mode(self):
        if ProductPackageLink.objects.filter(parent=self).count():
            self.mode = ProductMode.PACKAGE_PARENT
            self.external_url = None
            self.variation_children.clear()
        elif self.variation_children.count():
            if ProductVariationResult.objects.filter(product=self).count():
                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 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

    def link_to_parent(self, parent, variables=None):
        if parent.mode == ProductMode.VARIATION_CHILD:
            raise ValueError("Multilevel parentage hierarchies aren't supported (parent is a child already)")
        if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables:
            raise ValueError("Parent is a variable variation parent, yet variables were not passed to `link_to_parent`")
        if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables:
            raise ValueError("Parent is a simple variation parent, yet variables were passed to `link_to_parent`")
        if self.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            raise ValueError(
                "Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)"
            )
        if self.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            raise ValueError(
                "Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)"
            )

        self.unlink_from_parent()
        self.variation_parent = parent
        self.verify_mode()
        self.save()
        if parent.mode not in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT):
            parent.verify_mode()
            parent.save()

        if variables:
            mapping = {}
            for variable_identifier, value_identifier in variables.items():
                variable_identifier, _ = ProductVariationVariable.objects.get_or_create(
                    product=parent, identifier=variable_identifier
                )
                value_identifier, _ = ProductVariationVariableValue.objects.get_or_create(
                    variable=variable_identifier, identifier=value_identifier
                )
                mapping[variable_identifier] = value_identifier
            pvr = ProductVariationResult.objects.create(
                product=parent,
                combination_hash=hash_combination(mapping),
                result=self
            )
            if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT:
                parent.verify_mode()
                parent.save()
            return pvr
        else:
            return True

    def make_package(self, package_def):
        if self.mode != ProductMode.NORMAL:
            raise ValueError("Product is currently not a normal product, can't turn into package")

        for child_product, quantity in six.iteritems(package_def):
            if child_product.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT):
                raise ValueError("Variation parents can not belong into a package")
            if child_product.mode == ProductMode.PACKAGE_PARENT:
                raise ValueError("Can't nest packages")

            if quantity <= 0:
                raise ValueError("Quantity %s is invalid" % quantity)
            ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity)
        self.verify_mode()

    def get_package_child_to_quantity_map(self):
        if self.mode == ProductMode.PACKAGE_PARENT:
            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 {}
Пример #6
0
class Product(TaxableItem, AttributableMixin, TranslatableModel):
    COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class")

    # Metadata
    created_on = models.DateTimeField(auto_now_add=True,
                                      editable=False,
                                      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
    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'))
    stock_behavior = EnumIntegerField(StockBehavior,
                                      default=StockBehavior.UNSTOCKED,
                                      verbose_name=_('stock'))
    shipping_mode = EnumIntegerField(ShippingMode,
                                     default=ShippingMode.NOT_SHIPPED,
                                     verbose_name=_('shipping mode'))
    sales_unit = models.ForeignKey("SalesUnit",
                                   verbose_name=_('unit'),
                                   blank=True,
                                   null=True,
                                   on_delete=models.PROTECT)
    tax_class = models.ForeignKey("TaxClass",
                                  verbose_name=_('tax class'),
                                  on_delete=models.PROTECT)

    # Identification
    type = models.ForeignKey("ProductType",
                             related_name='products',
                             on_delete=models.PROTECT,
                             db_index=True,
                             verbose_name=_('product type'))
    sku = models.CharField(db_index=True,
                           max_length=128,
                           verbose_name=_('SKU'),
                           unique=True)
    gtin = models.CharField(blank=True,
                            max_length=40,
                            verbose_name=_('GTIN'),
                            help_text=_('Global Trade Item Number'))
    barcode = models.CharField(blank=True,
                               max_length=40,
                               verbose_name=_('barcode'))
    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)
    # Category is duplicated here because not all products necessarily belong in Shops (i.e. have
    # ShopProduct instances), but they should nevertheless be searchable by category in other
    # places, such as administration UIs.
    category = models.ForeignKey(
        "Category",
        related_name='primary_products',
        blank=True,
        null=True,
        verbose_name=_('primary category'),
        help_text=_("only used for administration and reporting"),
        on_delete=models.PROTECT)

    # Physical dimensions
    width = MeasurementField(unit="mm", verbose_name=_('width (mm)'))
    height = MeasurementField(unit="mm", verbose_name=_('height (mm)'))
    depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)'))
    net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)'))
    gross_weight = MeasurementField(unit="g",
                                    verbose_name=_('gross weight (g)'))

    # Misc.
    manufacturer = models.ForeignKey("Manufacturer",
                                     blank=True,
                                     null=True,
                                     verbose_name=_('manufacturer'),
                                     on_delete=models.PROTECT)
    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')),
        description=models.TextField(blank=True,
                                     verbose_name=_('description')),
        slug=models.SlugField(verbose_name=_('slug'),
                              max_length=255,
                              null=True),
        keywords=models.TextField(blank=True, verbose_name=_('keywords')),
        status_text=models.CharField(
            max_length=128,
            blank=True,
            verbose_name=_('status text'),
            help_text=_(
                'This text will be shown alongside the product in the shop.'
                ' (Ex.: "Available in a month")')),
        variation_name=models.CharField(max_length=128,
                                        blank=True,
                                        verbose_name=_('variation name')))

    objects = ProductQuerySet.as_manager()

    class Meta:
        ordering = ('-id', )
        verbose_name = _('product')
        verbose_name_plural = _('products')

    def __str__(self):
        try:
            return u"%s" % self.name
        except ObjectDoesNotExist:
            return self.sku

    def get_shop_instance(self, shop):
        """
        :type shop: shoop.core.models.Shop
        :rtype: shoop.core.models.ShopProduct
        """
        shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {})
        cached = shop_inst_cache.get(shop)
        if cached:
            return cached

        shop_inst = self.shop_products.get(shop=shop)
        shop_inst._product_cache = self
        shop_inst._shop_cache = shop
        shop_inst_cache[shop] = shop_inst

        return shop_inst

    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

    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: shoop.core.pricing.PricingContextable
        :type quantity: int
        :return: a tuple of prices
        :rtype: (shoop.core.pricing.Price, shoop.core.pricing.Price)
        """
        items = [
            c.get_price_info(context, quantity=quantity)
            for c in self.variation_children.all()
        ]
        if not items:
            return (None, None)

        infos = sorted(items, key=lambda x: x.price)
        return (infos[0].price, infos[-1].price)

    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: shoop.core.pricing.PricingContextable
        :rtype: shoop.core.pricing.PriceInfo
        """
        items = [
            c.get_price_info(context, quantity=quantity)
            for c in self.variation_children.all()
        ]
        if not items:
            return None

        return sorted(items, key=lambda x: x.price)[0]

    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: shoop.core.pricing.PricingContextable
        :rtype: shoop.core.pricing.PriceInfo
        """
        from shoop.core.pricing import get_price_info
        return get_price_info(product=self, context=context, quantity=quantity)

    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: shoop.core.pricing.PricingContextable
        :rtype: shoop.core.pricing.Price
        """
        return self.get_price_info(context, quantity).price

    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: shoop.core.pricing.PricingContextable
        :rtype: shoop.core.pricing.Price
        """
        return self.get_price_info(context, quantity=quantity).base_price

    def get_available_attribute_queryset(self):
        if self.type_id:
            return self.type.attributes.visible()
        else:
            return Attribute.objects.none()

    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"))

    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 explodes to 15,625 combinations.)

        :return: Iterable of combination information dicts.
        :rtype: Iterable[dict]
        """
        return get_all_available_combinations(self)

    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()

    def simplify_variation(self):
        """
        Remove variation variables from the given variation parent, turning it
        into a simple variation (or a normal product, if it has no children).

        :param product: Variation parent to not be variable any longer.
        :type product: shoop.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):
        if self.deleted:
            return None
        return (self.safe_translation_getter("name") or self.sku)

    def save(self, *args, **kwargs):
        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

    def delete(self, using=None):
        raise NotImplementedError(
            "Not implemented: Use `soft_delete()` for products.")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.",
                               kind=LogEntryKind.DELETION,
                               user=user)
            # Bypassing local `save()` on purpose.
            super(Product, self).save(update_fields=("deleted", ))

    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.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 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

    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 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(
                    "`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(
                "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")

    def make_package(self, package_def):
        if self.mode != ProductMode.NORMAL:
            raise ImpossibleProductModeException(_(
                "Product is currently not a normal product, can't turn into package"
            ),
                                                 code="abnormal")

        for child_product, quantity in six.iteritems(package_def):
            # :type child_product: Product
            if child_product.is_variation_parent():
                raise ImpossibleProductModeException(
                    _("Variation parents can not belong into a package"),
                    code="abnormal")
            if child_product.is_package_parent():
                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()

    def get_package_child_to_quantity_map(self):
        if self.is_package_parent():
            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 {}

    def is_variation_parent(self):
        return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT,
                             ProductMode.VARIABLE_VARIATION_PARENT)

    def is_variation_child(self):
        return (self.mode == ProductMode.VARIATION_CHILD)

    def get_variation_siblings(self):
        return Product.objects.filter(
            variation_parent=self.variation_parent).exclude(pk=self.pk)

    def is_package_parent(self):
        return (self.mode == ProductMode.PACKAGE_PARENT)

    def is_package_child(self):
        return ProductPackageLink.objects.filter(child=self).exists()

    def get_all_package_parents(self):
        return Product.objects.filter(
            pk__in=(ProductPackageLink.objects.filter(
                child=self).values_list("parent", flat=True)))

    def get_all_package_children(self):
        return Product.objects.filter(
            pk__in=(ProductPackageLink.objects.filter(
                parent=self).values_list("child", flat=True)))

    def get_public_media(self):
        return self.media.filter(enabled=True, public=True)
Пример #7
0
class Shipment(models.Model):
    order = models.ForeignKey("Order",
                              related_name='shipments',
                              on_delete=models.PROTECT,
                              verbose_name=_("order"))
    supplier = models.ForeignKey("Supplier",
                                 related_name='shipments',
                                 on_delete=models.PROTECT,
                                 verbose_name=_("supplier"))
    created_on = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_("created on"))
    status = EnumIntegerField(ShipmentStatus,
                              default=ShipmentStatus.NOT_SENT,
                              verbose_name=_("status"))
    tracking_code = models.CharField(max_length=64,
                                     blank=True,
                                     verbose_name=_("tracking code"))
    description = models.CharField(max_length=255,
                                   blank=True,
                                   verbose_name=_("description"))
    volume = MeasurementField(unit="m3", verbose_name=_("volume"))
    weight = MeasurementField(unit="kg", verbose_name=_("weight"))
    identifier = InternalIdentifierField(unique=True)

    # TODO: documents = models.ManyToManyField(FilerFile)

    class Meta:
        verbose_name = _('shipment')
        verbose_name_plural = _('shipments')

    def __init__(self, *args, **kwargs):
        super(Shipment, self).__init__(*args, **kwargs)
        if not self.identifier:
            if self.order and self.order.pk:
                prefix = '%s/%s/' % (self.order.pk,
                                     self.order.shipments.count())
            else:
                prefix = ''
            self.identifier = prefix + get_random_string(32)

    def __repr__(self):  # pragma: no cover
        return "<Shipment %s for order %s (tracking %r, created %s)>" % (
            self.pk, self.order_id, self.tracking_code, self.created_on)

    def save(self, *args, **kwargs):
        super(Shipment, self).save(*args, **kwargs)
        for product_id in self.products.values_list("product_id", flat=True):
            self.supplier.module.update_stock(product_id=product_id)

    def cache_values(self):
        """
        (Re)cache `.volume` and `.weight` for this Shipment from the ShipmentProducts within.
        """
        total_volume = 0
        total_weight = 0
        for quantity, volume, weight in self.products.values_list(
                "quantity", "unit_volume", "unit_weight"):
            total_volume += quantity * volume
            total_weight += quantity * weight
        self.volume = total_volume
        self.weight = total_weight / GRAMS_TO_KILOGRAMS_DIVISOR

    @property
    def total_products(self):
        return (self.products.aggregate(
            quantity=models.Sum("quantity"))["quantity"] or 0)