Exemplo n.º 1
0
class ShipmentProduct(models.Model):
    shipment = models.ForeignKey(Shipment,
                                 related_name='products',
                                 on_delete=models.PROTECT,
                                 verbose_name=_("shipment"))
    product = models.ForeignKey("Product",
                                related_name='shipments',
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    quantity = QuantityField(verbose_name=_("quantity"))

    # volume is m^3, not mm^3, because mm^3 are tiny. like ants.
    unit_volume = MeasurementField(unit="m3", verbose_name=_("unit volume"))
    unit_weight = MeasurementField(unit="g", verbose_name=_("unit weight"))

    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
Exemplo n.º 2
0
class WeightBasedPriceRange(TranslatableModel):
    component = models.ForeignKey("WeightBasedPricingBehaviorComponent",
                                  related_name="ranges",
                                  on_delete=models.CASCADE)
    min_value = MeasurementField(
        unit="g",
        verbose_name=_("min weight (g)"),
        blank=True,
        null=True,
        help_text=_("The minimum weight, in grams, for this price to apply."))
    max_value = MeasurementField(
        unit="g",
        verbose_name=_("max weight (g)"),
        blank=True,
        null=True,
        help_text=_(
            "The maximum weight, in grams, before this price no longer applies."
        ))
    price_value = MoneyValueField(help_text=_(
        "The cost to apply to this service when the weight criteria is met."))
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("description"),
        help_text=_(
            "The order line text to display when this behavior is applied.")),
                                    )

    def matches_to_value(self, value):
        return _is_in_range(value, self.min_value, self.max_value)
Exemplo n.º 3
0
class ShipmentProduct(ShuupModel):
    shipment = models.ForeignKey(Shipment,
                                 related_name='products',
                                 on_delete=models.PROTECT,
                                 verbose_name=_("shipment"))
    product = models.ForeignKey("Product",
                                related_name='shipments',
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    quantity = QuantityField(verbose_name=_("quantity"))

    unit_volume = MeasurementField(unit=get_shuup_volume_unit(),
                                   verbose_name=_("unit volume ({})".format(
                                       get_shuup_volume_unit())))
    unit_weight = MeasurementField(unit=settings.SHUUP_MASS_UNIT,
                                   verbose_name=_("unit weight ({})".format(
                                       settings.SHUUP_MASS_UNIT)))

    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
        self.unit_weight = prod.gross_weight
Exemplo n.º 4
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)
Exemplo n.º 5
0
class ShippingTableItem(models.Model):
    table = models.ForeignKey(ShippingTable, verbose_name=_("table"))
    region = models.ForeignKey(ShippingRegion, verbose_name=_("region"))

    start_weight = MeasurementField(unit="g", verbose_name=_('start weight (g)'))
    end_weight = MeasurementField(unit="g", verbose_name=_('end weight (g)'))

    price = MoneyValueField(verbose_name=_("price"))
    delivery_time = models.PositiveSmallIntegerField(verbose_name=_("delivery time (days)"))

    class Meta:
        verbose_name = _("shipping price table")
        verbose_name_plural = _("shipping price tables")

    def __str__(self):
        return "ID {0} {1} {2} - {3}->{4}: {5}-{6}".format(self.id,
                                                           self.table,
                                                           self.region,
                                                           self.start_weight,
                                                           self.end_weight,
                                                           self.price,
                                                           self.delivery_time)
Exemplo n.º 6
0
def test_measurement_field_doesnt_know_bananas():
    with pytest.raises(ImproperlyConfigured):
        scale = MeasurementField(unit="banana")
Exemplo n.º 7
0
class ShippingTableBehaviorComponent(ServiceBehaviorComponent):
    add_delivery_time_days = models.PositiveSmallIntegerField(verbose_name=_("additional delivery time"),
                                                              default=0,
                                                              help_text=_("Extra number of days to add."))

    add_price = MoneyValueField(verbose_name=_("additional price"),
                                default=Decimal(),
                                help_text=_("Extra amount to add."))

    use_cubic_weight = models.BooleanField(verbose_name=_("Use Cubic Weight"),
                                           default=False,
                                           help_text=_("Enable this to calculate the cubic weight and use "
                                                       "the heaviest measurement (real weight or cubic weight)."))

    cubic_weight_factor = models.DecimalField(verbose_name=_("Cubic Weight Factor (cm³)"),
                                              decimal_places=2, max_digits=10,
                                              default=Decimal(6000),
                                              help_text=_("Google it if you don't know what you're doing."))

    cubic_weight_exemption = MeasurementField(unit="kg",
                                              verbose_name=_("Cubic Weight exemption value (kg)"),
                                              decimal_places=3, max_digits=8,
                                              default=Decimal(),
                                              help_text=_("The Cubic Weight will be considered if the "
                                                          "sum of all products real weights "
                                                          "is higher then this value."))

    max_package_width = MeasurementField(unit="mm",
                                         verbose_name=_("Max package width (mm)"),
                                         decimal_places=2, max_digits=7,
                                         default=Decimal(),
                                         help_text=_("This is only used for Cubic Weight method "
                                                     "since the order/basket will be splitted into packages "
                                                     "for volume calculation."))

    max_package_height = MeasurementField(unit="mm",
                                          verbose_name=_("Max package height (mm)"),
                                          decimal_places=2, max_digits=7,
                                          default=Decimal(),
                                          help_text=_("This is only used for Cubic Weight method "
                                                      "since the order/basket will be splitted into packages "
                                                      "for volume calculation."))

    max_package_length = MeasurementField(unit="mm",
                                          verbose_name=_("Max package length (mm)"),
                                          decimal_places=2, max_digits=7,
                                          default=Decimal(),
                                          help_text=_("This is only used for Cubic Weight method "
                                                      "since the order/basket will be splitted into packages "
                                                      "for volume calculation."))

    max_package_edges_sum = MeasurementField(unit="mm",
                                             verbose_name=_("Max package edge sum (mm)"),
                                             decimal_places=2, max_digits=7,
                                             default=Decimal(),
                                             help_text=_("The max sum of width, height and length of the package. "
                                                         "This is only used for Cubic Weight method "
                                                         "since the order/basket will be splitted into packages "
                                                         "for volume calculation."))

    max_package_weight = MeasurementField(unit="kg",
                                          verbose_name=_("Max package weight (kg)"),
                                          decimal_places=3, max_digits=8,
                                          default=Decimal(),
                                          help_text=_("This is only used for Cubic Weight method "
                                                      "since the order/basket will be splitted into packages "
                                                      "for volume calculation."))

    class Meta:
        abstract = True

    def get_source_weight(self, source):
        """
        Calculates the source weight (in kg) based on behavior component configuration.
        """
        weight = source.total_gross_weight * G_TO_KG  # transform g in kg

        if self.use_cubic_weight and weight > self.cubic_weight_exemption:
            # create the packager
            packager = SimplePackager()

            # add the constraints, if configured
            if self.max_package_height and self.max_package_length and \
                    self.max_package_width and self.max_package_edges_sum:

                packager.add_constraint(SimplePackageDimensionConstraint(
                    self.max_package_width,
                    self.max_package_length,
                    self.max_package_height,
                    self.max_package_edges_sum
                ))

            if self.max_package_weight:
                packager.add_constraint(WeightPackageConstraint(self.max_package_weight * KG_TO_G))

            # split products into packages
            packages = packager.pack_source(source)

            # check if some package was created
            if packages:
                total_weight = 0

                for package in packages:

                    if package.weight > self.cubic_weight_exemption:
                        total_weight = total_weight + (package.volume / self.cubic_weight_factor * G_TO_KG)
                    else:
                        total_weight = total_weight + package.weight * G_TO_KG

                weight = total_weight

        return weight

    def get_available_table_items(self, source):
        """
        Fetches the available table items
        """
        now_dt = now()
        weight = self.get_source_weight(source)

        # 1) source total weight must be in a range
        # 2) enabled tables
        # 3) enabled table carriers
        # 4) valid shops
        # 5) valid date range tables
        # 6) order by priority
        # 7) distinct rows

        qs = ShippingTableItem.objects.select_related('table').filter(
            end_weight__gte=weight,
            start_weight__lte=weight,
            table__enabled=True,
            table__carrier__enabled=True,
            table__shops__in=[source.shop]
        ).filter(
            Q(Q(table__start_date__lte=now_dt) | Q(table__start_date=None)),
            Q(Q(table__end_date__gte=now_dt) | Q(table__end_date=None))
        ).order_by('-region__priority').distinct()

        return qs

    def get_first_available_item(self, source):
        table_items = self.get_available_table_items(source)

        for table_item in table_items:
            # check if the table exclude region is compatible
            # with the source.. if True, check next one
            invalid_region = False
            for excluded_region in table_item.table.excluded_regions.all():
                if excluded_region.is_compatible_with(source):
                    invalid_region = True
                    break
            if invalid_region:
                continue

            # a valid table item was found! get out of here
            if table_item.region.is_compatible_with(source):
                return table_item

    def get_unavailability_reasons(self, service, source):
        table_item = self.get_first_available_item(source)

        if not table_item:
            return [ValidationError(_("No table found"))]
        return ()

    def get_costs(self, service, source):
        table_item = self.get_first_available_item(source)

        if table_item:
            return [ServiceCost(source.create_price(table_item.price + self.add_price))]

        return ()

    def get_delivery_time(self, service, source):
        table_item = self.get_first_available_item(source)

        if table_item:
            return DurationRange(
                timedelta(days=(table_item.delivery_time + self.add_delivery_time_days))
            )

        return None
Exemplo n.º 8
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,
                                      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
    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'),
        help_text=_(
            "Set to stocked if inventory should be managed within Shuup."))
    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. Sales units are defined in Products - 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. "
          "Tax classes are defined in Settings - Tax Classes. "
          "The rules by which taxes are applied are defined in Settings - Tax Rules."
          ))

    # Identification
    type = models.ForeignKey(
        "ProductType",
        related_name='products',
        on_delete=models.PROTECT,
        db_index=True,
        verbose_name=_('product type'),
        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 it through your inventory. "
          "People often use the number by the barcode on the product, "
          "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="mm",
        verbose_name=_('width (mm)'),
        help_text=_(
            "Set the measured width of your product or product packaging. "
            "This will provide customers with your product size and help with calculating shipping costs."
        ))
    height = MeasurementField(
        unit="mm",
        verbose_name=_('height (mm)'),
        help_text=_(
            "Set the measured height of your product or product packaging. "
            "This will provide customers with your product size and help with calculating shipping costs."
        ))
    depth = MeasurementField(
        unit="mm",
        verbose_name=_('depth (mm)'),
        help_text=
        _("Set the measured depth or length of your product or product packaging. "
          "This will provide customers with your product size and help with calculating shipping costs."
          ))
    net_weight = MeasurementField(
        unit="g",
        verbose_name=_('net weight (g)'),
        help_text=_(
            "Set the measured weight of your product WITHOUT its packaging. "
            "This will provide customers with your product weight."))
    gross_weight = MeasurementField(
        unit="g",
        verbose_name=_('gross weight (g)'),
        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.PROTECT,
        help_text=
        _("Select a manufacturer for your product. These are defined in Products Settings - 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'),
            help_text=
            _("Enter a descriptive name for your product. This will be its title in your store."
              )),
        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."
              )),
        slug=models.SlugField(
            verbose_name=_('slug'),
            max_length=255,
            blank=True,
            null=True,
            help_text=_(
                "Enter a URL Slug for your product. This is what your product page URL will be. "
                "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."
            )),
        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. '
                'It is useful for informing customers of special stock numbers or preorders. '
                '(Ex.: "Available in a month")')),
        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 or versions.")))

    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, allow_cache=False):
        """
        :type shop: shuup.core.models.Shop
        :rtype: shuup.core.models.ShopProduct
        """

        # FIXME: Temporary removed the cache to prevent parler issues
        # Uncomment this as soon as https://github.com/shuup/shuup/issues/1323 is fixed
        # and Django Parler version is bumped with the fix

        # 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)
        # context_cache.set_cached_value(key, shop_inst)
        return shop_inst

    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 = []
        for child in self.variation_children.all():
            try:
                shop_product = child.get_shop_instance(context.shop)
            except ShopProduct.DoesNotExist:
                continue

            if shop_product.is_orderable(supplier=None,
                                         customer=context.customer,
                                         quantity=1):
                priced_children.append(
                    (child, child.get_price_info(context, quantity=quantity)))

        return sorted(priced_children, key=(lambda x: x[1].price))

    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: shuup.core.pricing.PricingContextable
        :type quantity: int
        :return: a tuple of prices
        :rtype: (shuup.core.pricing.Price, shuup.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: shuup.core.pricing.PricingContextable
        :rtype: shuup.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: 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)

    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

    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

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

    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

    def clean(self):
        pre_clean.send(type(self), instance=self)
        super(Product, self).clean()
        post_clean.send(type(self), instance=self)

    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):
            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 not belong into a 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()

    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 {}

    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_subscription_parent(self):
        return (self.mode == ProductMode.SUBSCRIPTION)

    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).exclude(kind=ProductMediaKind.IMAGE)

    def is_stocked(self):
        return (self.stock_behavior == StockBehavior.STOCKED)

    def is_container(self):
        return (self.is_package_parent() or self.is_subscription_parent())
Exemplo n.º 9
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)

    objects = ShipmentManager()

    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 delete(self, using=None):
        raise NotImplementedError(
            "Not implemented: Use `soft_delete()` for shipments.")

    @atomic
    def soft_delete(self, user=None):
        if self.status == ShipmentStatus.DELETED:
            return
        self.status = ShipmentStatus.DELETED
        self.save(update_fields=["status"])
        for product_id in self.products.values_list("product_id", flat=True):
            self.supplier.module.update_stock(product_id=product_id)
        self.order.update_shipping_status()
        shipment_deleted.send(sender=type(self), shipment=self)

    def is_deleted(self):
        return bool(self.status == ShipmentStatus.DELETED)

    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)
Exemplo n.º 10
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, blank=True, 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: shuup.core.models.Shop
        :rtype: shuup.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_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.
        """
        priced_children = (
            (child, child.get_price_info(context, quantity=quantity))
            for child in self.variation_children.all())
        return sorted(priced_children, key=(lambda x: x[1].price))

    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: shuup.core.pricing.PricingContextable
        :type quantity: int
        :return: a tuple of prices
        :rtype: (shuup.core.pricing.Price, shuup.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: shuup.core.pricing.PricingContextable
        :rtype: shuup.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: 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)

    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

    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

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

    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).exclude(kind=ProductMediaKind.IMAGE)

    def is_stocked(self):
        return (self.stock_behavior == StockBehavior.STOCKED)
Exemplo n.º 11
0
class Shipment(ShuupModel):
    order = models.ForeignKey("Order",
                              blank=True,
                              null=True,
                              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=get_shuup_volume_unit(),
                              verbose_name=_("volume ({})".format(
                                  get_shuup_volume_unit())))
    weight = MeasurementField(unit=settings.SHUUP_MASS_UNIT,
                              verbose_name=_("weight ({})".format(
                                  settings.SHUUP_MASS_UNIT)))
    identifier = InternalIdentifierField(unique=True)
    type = EnumIntegerField(ShipmentType,
                            default=ShipmentType.OUT,
                            verbose_name=_("type"))
    # TODO: documents = models.ManyToManyField(FilerFile)

    objects = ShipmentManager()

    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 (tracking %r, created %s)>" % (
            self.pk, 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 delete(self, using=None):
        raise NotImplementedError(
            "Error! Not implemented: `Shipment` -> `delete()`. Use `soft_delete()` instead."
        )

    @atomic
    def soft_delete(self, user=None):
        if self.status == ShipmentStatus.DELETED:
            return
        self.status = ShipmentStatus.DELETED
        self.save(update_fields=["status"])
        for product_id in self.products.values_list("product_id", flat=True):
            self.supplier.module.update_stock(product_id=product_id)
        if self.order:
            self.order.update_shipping_status()
        shipment_deleted.send(sender=type(self), shipment=self)

    def is_deleted(self):
        return bool(self.status == ShipmentStatus.DELETED)

    def cache_values(self):
        """
        (Re)cache `.volume` and `.weight` for this Shipment from within the ShipmentProducts.
        """
        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)

    def set_received(self, purchase_prices=None, created_by=None):
        """
        Mark the shipment as received.

        In case shipment is incoming, add stock adjustment for each
        shipment product in this shipment.

        :param purchase_prices: a dict mapping product ids to purchase prices
        :type purchase_prices: dict[shuup.shop.models.Product, decimal.Decimal]
        :param created_by: user who set this shipment received
        :type created_by: settings.AUTH_USER_MODEL
        """
        self.status = ShipmentStatus.RECEIVED
        self.save()
        if self.type == ShipmentType.IN:
            for product_id, quantity in self.products.values_list(
                    "product_id", "quantity"):
                purchase_price = (purchase_prices.get(product_id, None)
                                  if purchase_prices else None)
                self.supplier.module.adjust_stock(product_id=product_id,
                                                  delta=quantity,
                                                  purchase_price=purchase_price
                                                  or 0,
                                                  created_by=created_by)
Exemplo n.º 12
0
def test_measurement_field():
    field = MeasurementField(unit="mm3")
    assert field.unit == "mm3"
    assert field.default == 0
    assert field.max_digits == FORMATTED_DECIMAL_FIELD_MAX_DIGITS
    assert field.decimal_places == FORMATTED_DECIMAL_FIELD_DECIMAL_PLACES
Exemplo n.º 13
0
class CorreiosBehaviorComponent(ServiceBehaviorComponent):
    CORREIOS_SERVICOS_CHOICES = (
        (CorreiosServico.PAC, '({0}) PAC'.format(CorreiosServico.PAC)),
        (CorreiosServico.SEDEX, '({0}) Sedex'.format(CorreiosServico.SEDEX)),
        (CorreiosServico.SEDEX_10,
         '({0}) Sedex 10'.format(CorreiosServico.SEDEX_10)),
        (CorreiosServico.SEDEX_A_COBRAR,
         '({0}) Sedex a cobrar'.format(CorreiosServico.SEDEX_A_COBRAR)),
        (CorreiosServico.SEDEX_HOJE,
         '({0}) Sedex Hoje'.format(CorreiosServico.SEDEX_HOJE)),
        (CorreiosServico.ESEDEX,
         '({0}) eSedex'.format(CorreiosServico.ESEDEX)),
    )

    name = _("Serviço dos Correios")
    help_text = _("Configurações do serviço")

    cod_servico = models.CharField(
        "Código do serviço",
        max_length=10,
        help_text="Código atribuído automaticamente ao criar o serviço.",
        choices=CORREIOS_SERVICOS_CHOICES)

    cod_servico_contrato = models.CharField(
        "Código do serviço em contrato",
        null=True,
        blank=True,
        max_length=10,
        help_text="Informe o código do serviço em contrato, se existir. "
        "Quando há um contrato com os Correios, "
        "é necessário informar este código para que "
        "ele seja utilizado no cálculo dos preços e prazos.")

    cep_origem = models.CharField(
        "CEP de origem",
        max_length=8,
        default='99999999',
        help_text="CEP de origem da encomenda. Apenas números, sem hífen.")

    cod_empresa = models.CharField(
        "Código da empresa",
        max_length=30,
        blank=True,
        null=True,
        help_text="Seu código administrativo junto à ECT, se existir. "
        "O código está disponível no corpo do "
        "contrato firmado com os Correios.")

    senha = models.CharField(
        "Senha",
        max_length=30,
        blank=True,
        null=True,
        help_text="Senha para acesso ao serviço, associada ao seu "
        "código administrativo, se existir. "
        "A senha inicial corresponde aos 8 primeiros dígitos "
        "do CNPJ informado no contrato.")

    mao_propria = models.BooleanField(
        "Mão própria?",
        default=False,
        help_text="Indica se a encomenda será entregue "
        "com o serviço adicional mão própria.")

    valor_declarado = models.BooleanField(
        "Valor declarado?",
        default=False,
        help_text="Indica se a encomenda será entregue "
        "com o serviço adicional valor declarado.")

    aviso_recebimento = models.BooleanField(
        "Aviso de recebimento?",
        default=False,
        help_text="Indica se a encomenda será entregue "
        "com o serviço adicional aviso de recebimento.")

    additional_delivery_time = models.PositiveIntegerField(
        "Prazo adicional",
        blank=True,
        default=0,
        validators=[MinValueValidator(0)],
        help_text="Indica quantos dias devem ser somados "
        "ao prazo original retornado pelo serviço dos Correios. "
        "O prazo será somado no prazo de cada encomenda diferente.")

    additional_price = models.DecimalField(
        "Preço adicional",
        blank=True,
        max_digits=9,
        decimal_places=2,
        default=Decimal(),
        validators=[MinValueValidator(Decimal(0))],
        help_text="Indica o valor, em reais, a ser somado "
        "ao preço original retornado pelo serviço dos Correios. "
        "O preço será somado no valor de cada encomenda diferente.")

    max_weight = MeasurementField(
        verbose_name="Peso máximo da embalagem (kg)",
        unit="kg",
        blank=True,
        validators=[MinValueValidator(Decimal(0))],
        default=Decimal(),
        help_text="Indica o peso máximo admitido para esta modalidade.")

    min_length = MeasurementField(verbose_name="Comprimento mínimo (mm)",
                                  unit="mm",
                                  default=110,
                                  validators=[MinValueValidator(Decimal(0))],
                                  help_text="Indica o comprimento mínimo "
                                  "para caixas e pacotes.")

    max_length = MeasurementField(verbose_name="Comprimento máximo (mm)",
                                  unit="mm",
                                  default=1050,
                                  validators=[MinValueValidator(Decimal(0))],
                                  help_text="Indica o comprimento máximo "
                                  "para caixas e pacotes.")

    min_width = MeasurementField(verbose_name="Largura mínima (mm)",
                                 unit="mm",
                                 default=160,
                                 validators=[MinValueValidator(Decimal(0))],
                                 help_text="Indica a largura mínima "
                                 "para caixas e pacotes.")

    max_width = MeasurementField(verbose_name="Largura máxima",
                                 unit="mm",
                                 default=1050,
                                 validators=[MinValueValidator(Decimal(0))],
                                 help_text="Indica a largura máxima "
                                 "para caixas e pacotes.")

    min_height = MeasurementField(verbose_name="Altura mínima (mm)",
                                  unit="mm",
                                  default=20,
                                  validators=[MinValueValidator(Decimal(0))],
                                  help_text="Indica a altura mínima "
                                  "para caixas e pacotes.")

    max_height = MeasurementField(verbose_name="Altura máxima (mm)",
                                  unit="mm",
                                  default=1050,
                                  validators=[MinValueValidator(Decimal(0))],
                                  help_text="Indica a altura máxima "
                                  "para caixas e pacotes.")

    max_edges_sum = MeasurementField(
        verbose_name="Soma máxima das dimensões (L + A + C) (mm)",
        unit="mm",
        default=2000,
        validators=[MinValueValidator(Decimal(0))],
        help_text="Indica a soma máxima das dimensões de "
        "altura + largura + comprimento "
        "para caixas e pacotes.")

    def get_unavailability_reasons(self, service, source):
        """
        :type service: Service
        :type source: shuup.core.order_creator.OrderSource
        :rtype: Iterable[ValidationError]
        """

        errors = []
        packages = self._pack_source(source)

        if packages:
            try:
                results = self._get_correios_results(source, packages)

                if results:
                    for result in results:
                        if result.erro != 0:
                            logger.warn("{0}: {1}".format(
                                result.erro, result.msg_erro))
                            errors.append(
                                ValidationError(
                                    "Alguns itens não poderão ser "
                                    "entregues pelos Correios.",
                                    code=result.erro))
            except CorreiosWSServerTimeoutException:
                errors.append(
                    ValidationError(
                        "Não foi possível contatar os serviços dos Correios."))

        else:
            errors.append(
                ValidationError(
                    "Alguns itens não puderam ser empacotados nos requisitos dos Correios."
                ))

        return errors

    def get_costs(self, service, source):
        """
        Return costs for for this object. This should be implemented
        in subclass. This method is used to calculate price for
        ``ShippingMethod`` and ``PaymentMethod`` objects.

        :type service: Service
        :type source: shuup.core.order_creator.OrderSource
        :rtype: Iterable[ServiceCost]
        """
        packages = self._pack_source(source)

        try:
            results = self._get_correios_results(source, packages)
            total_price = Decimal()

            for result in results:
                if result.erro == 0:
                    total_price = total_price + result.valor
                else:
                    total_price = 0
                    logger.critical("CorreiosWS: Erro {0} ao calcular "
                                    "preço e prazo para {2}: {1}".format(
                                        result.erro, result.msg_erro, source))
                    break

            if total_price > 0:
                yield ServiceCost(
                    source.create_price(total_price + self.additional_price))

        except CorreiosWSServerTimeoutException:
            pass

    def get_delivery_time(self, service, source):
        """
        :type service: Service
        :type source: shuup.core.order_creator.OrderSource
        :rtype: shuup.utils.dates.DurationRange|None
        """

        packages = self._pack_source(source)

        try:
            results = self._get_correios_results(source, packages)
        except CorreiosWSServerTimeoutException:
            return None

        max_days = 1
        min_days = 0

        for result in results:
            if result.erro == 0:
                max_days = max(max_days, result.prazo_entrega)
                min_days = result.prazo_entrega if not min_days else min(
                    min_days, result.prazo_entrega)

            else:
                logger.critical("CorreiosWS: Erro {0} ao calcular "
                                "preço e prazo para {2}: {1}".format(
                                    result.erro, result.msg_erro, source))
                return None

        return DurationRange.from_days(
            min_days + self.additional_delivery_time,
            max_days + self.additional_delivery_time)

    def _pack_source(self, source):
        """
        Empacota itens do pedido
        :rtype: Iterable[shuup_order_packager.package.AbstractPackage|None]
        :return: Lista de pacotes ou None se for impossível empacotar pedido
        """
        packager = cached_load("CORREIOS_PRODUCTS_PACKAGER_CLASS")()
        packager.add_constraint(
            SimplePackageDimensionConstraint(self.max_width, self.max_length,
                                             self.max_height,
                                             self.max_edges_sum))

        packager.add_constraint(
            WeightPackageConstraint(self.max_weight * KG_TO_G))
        return packager.pack_source(source)

    def _get_correios_results(self, source, packages):
        """
        Obtém uma lista dos resultados obtidos dos correios para determinado pedido
        :type source: shuup.core.order_creator.OrderSource
        :rtype: list of shuup_correios.correios.CorreiosWS.CorreiosWSServiceResult
        """
        results = []

        cep_origem = "".join([d for d in self.cep_origem if d.isdigit()])
        pedido_total = source.total_price_of_products.value
        shipping_address = source.shipping_address

        if not shipping_address:
            shipping_address = source.billing_address

        if not shipping_address:
            return []

        cep_destino = "".join(
            [d for d in shipping_address.postal_code if d.isdigit()])

        for package in packages:
            results.append(
                CorreiosWS.get_preco_prazo(
                    cep_destino, cep_origem, self.cod_servico_contrato
                    if self.cod_servico_contrato else self.cod_servico,
                    package, self.cod_empresa, self.senha, self.mao_propria,
                    pedido_total if self.valor_declarado else 0.0,
                    self.aviso_recebimento, self.min_width, self.min_length,
                    self.min_height))

        return results