コード例 #1
0
class StockCount(models.Model):
    alert_limit = QuantityField(default=0, editable=False, verbose_name=_("alert limit"))
    product = models.ForeignKey(
        "shuup.Product", related_name="+", editable=False, on_delete=models.CASCADE, verbose_name=_("product"))
    supplier = models.ForeignKey(
        "shuup.Supplier", editable=False, on_delete=models.CASCADE, verbose_name=_("supplier"))
    logical_count = QuantityField(default=0, editable=False, verbose_name=_("logical count"))
    physical_count = QuantityField(default=0, editable=False, verbose_name=_("physical count"))
    stock_value_value = MoneyValueField(default=0)
    stock_value = PriceProperty("stock_value_value", "currency", "includes_tax")
    stock_unit_price = PriceProperty("stock_unit_price_value", "currency", "includes_tax")

    class Meta:
        unique_together = [("product", "supplier")]

    @property
    def currency(self):
        return SHUUP_HOME_CURRENCY

    @property
    def includes_tax(self):
        return False

    @property
    def stock_unit_price_value(self):
        return (self.stock_value_value / self.logical_count if self.logical_count else 0)
コード例 #2
0
ファイル: models.py プロジェクト: yurkobb/shuup
class StockCount(models.Model):
    alert_limit = QuantityField(default=0, editable=False, verbose_name=_("alert limit"))
    stock_managed = models.BooleanField(
        verbose_name=_("stock managed"),
        default=True,
        help_text=_("Use this to override the supplier default stock behavior per product."),
    )
    product = models.ForeignKey(
        "shuup.Product",
        related_name="simple_supplier_stock_count",
        editable=False,
        on_delete=models.CASCADE,
        verbose_name=_("product"),
    )
    supplier = models.ForeignKey("shuup.Supplier", editable=False, on_delete=models.CASCADE, verbose_name=_("supplier"))
    logical_count = QuantityField(default=0, editable=False, verbose_name=_("logical count"))
    physical_count = QuantityField(default=0, editable=False, verbose_name=_("physical count"))
    stock_value_value = MoneyValueField(default=0)
    stock_value = PriceProperty("stock_value_value", "currency", "includes_tax")
    stock_unit_price = PriceProperty("stock_unit_price_value", "currency", "includes_tax")

    class Meta:
        unique_together = [("product", "supplier")]

    @cached_property
    def currency(self):
        return _get_currency()

    @cached_property
    def includes_tax(self):
        return _get_prices_include_tax()

    @property
    def stock_unit_price_value(self):
        return self.stock_value_value / self.logical_count if self.logical_count else 0
コード例 #3
0
class StockAdjustment(models.Model):
    product = models.ForeignKey("shuup.Product",
                                related_name="+",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    supplier = models.ForeignKey("shuup.Supplier",
                                 on_delete=models.CASCADE,
                                 verbose_name=_("supplier"))
    created_on = models.DateTimeField(auto_now_add=True,
                                      editable=False,
                                      db_index=True,
                                      verbose_name=_("created on"))
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                   blank=True,
                                   null=True,
                                   on_delete=models.PROTECT,
                                   verbose_name=_("created by"))
    delta = QuantityField(default=0, verbose_name=_("delta"))
    purchase_price_value = MoneyValueField(default=0)
    purchase_price = PriceProperty("purchase_price_value", "currency",
                                   "includes_tax")
    type = EnumIntegerField(StockAdjustmentType,
                            db_index=True,
                            default=StockAdjustmentType.INVENTORY,
                            verbose_name=_("type"))

    @cached_property
    def currency(self):
        return _get_currency()

    @cached_property
    def includes_tax(self):
        return _get_prices_include_tax()
コード例 #4
0
class SupplierPrice(MoneyPropped, models.Model):
    shop = models.ForeignKey(on_delete=models.CASCADE, to="shuup.Shop")
    supplier = models.ForeignKey(on_delete=models.CASCADE, to="shuup.Supplier")
    product = models.ForeignKey(on_delete=models.CASCADE, to="shuup.Product")
    amount_value = MoneyValueField()
    amount = PriceProperty("amount_value", "shop.currency",
                           "shop.prices_include_tax")
コード例 #5
0
ファイル: test_properties.py プロジェクト: yashodhank/shuup
    class Market(object):
        price = PriceProperty('value', 'currency', 'includes_tax')

        def __init__(self):
            self.value = 123
            self.currency = 'GBP'
            self.includes_tax = True
コード例 #6
0
ファイル: basket_conditions.py プロジェクト: wsmoyer/shuup
class BasketTotalAmountCondition(MoneyPropped, BasketCondition):
    identifier = "basket_amount_condition"
    name = _("Basket total value")

    amount = PriceProperty("amount_value", "campaign.shop.currency",
                           "campaign.shop.prices_include_tax")
    amount_value = MoneyValueField(default=None,
                                   blank=True,
                                   null=True,
                                   verbose_name=_("basket total amount"))

    def matches(self, basket, lines):
        campaign = self.campaign.first()
        total_of_products = get_total_price_of_products(basket, campaign)
        return (total_of_products.value >= self.amount_value)

    @property
    def description(self):
        return _(
            "Limit the campaign to match when it has at least the total value entered here worth of products."
        )

    @property
    def value(self):
        return self.amount_value

    @value.setter
    def value(self, value):
        self.amount_value = value
コード例 #7
0
class CgpPrice(MoneyPropped, models.Model):
    product = models.ForeignKey("shuup.Product",
                                related_name="+",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    shop = models.ForeignKey("shuup.Shop",
                             db_index=True,
                             on_delete=models.CASCADE,
                             verbose_name=_("shop"))
    group = models.ForeignKey("shuup.ContactGroup",
                              db_index=True,
                              on_delete=models.CASCADE,
                              verbose_name=_("contact group"))
    price = PriceProperty("price_value", "shop.currency",
                          "shop.prices_include_tax")
    price_value = MoneyValueField(verbose_name=_("price"))

    class Meta:
        unique_together = (('product', 'shop', 'group'), )
        verbose_name = _(u"product price")
        verbose_name_plural = _(u"product prices")

    def __repr__(self):
        return "<CgpPrice (p%s,s%s,g%s): price %s" % (
            self.product_id,
            self.shop_id,
            self.group_id,
            self.price,
        )

    def save(self, *args, **kwargs):
        super(CgpPrice, self).save(*args, **kwargs)
        bump_cache_for_product(self.product, self.shop)
コード例 #8
0
class BasketTotalUndiscountedProductAmountCondition(MoneyPropped,
                                                    BasketCondition):
    identifier = "basket_amount_condition_undiscounted"
    name = _("Undiscounted basket total value")

    amount = PriceProperty("amount_value", "campaign.shop.currency",
                           "campaign.shop.prices_include_tax")
    amount_value = MoneyValueField(default=None,
                                   blank=True,
                                   null=True,
                                   verbose_name=_("basket total amount"))

    def matches(self, basket, lines):
        from shuup.campaigns.models import CatalogCampaign

        campaign = self.campaign.first()
        total_of_products = get_total_price_of_products(basket, campaign)
        product_lines = basket.get_product_lines()

        if hasattr(campaign, "supplier") and campaign.supplier:
            product_lines = [
                line for line in product_lines
                if line.supplier == campaign.supplier
            ]

        total_undiscounted_price_value = total_of_products.value
        shop = basket.shop
        context = PricingContext(shop, basket.customer)

        for line in product_lines:
            if CatalogCampaign.get_matching(
                    context, line.product.get_shop_instance(shop)):
                total_undiscounted_price_value -= line.price.value
        return total_undiscounted_price_value >= self.amount_value

    @property
    def description(self):
        return _(
            "Limit the campaign to match when it has at least the total value "
            "entered here worth of products which doesn't have already discounts."
        )

    @property
    def value(self):
        return self.amount_value

    @value.setter
    def value(self, value):
        self.amount_value = value
コード例 #9
0
ファイル: _product_catalog.py プロジェクト: rrosajp/shuup
class ProductCatalogPrice(MoneyPropped, models.Model):
    """
    Index the prices of products.
    There can be multiple prices, the best price will be selected.
    """

    id = models.BigAutoField(primary_key=True)
    product = models.ForeignKey("shuup.Product",
                                related_name="catalog_prices",
                                on_delete=models.CASCADE,
                                editable=False)
    shop = models.ForeignKey("shuup.Shop",
                             related_name="catalog_prices",
                             on_delete=models.CASCADE,
                             editable=False)
    supplier = models.ForeignKey("shuup.Supplier",
                                 related_name="catalog_prices",
                                 on_delete=models.CASCADE,
                                 editable=False)
    price = PriceProperty("price_value", "shop.currency",
                          "shop.prices_include_tax")
    price_value = MoneyValueField(editable=False,
                                  verbose_name=_("price"),
                                  help_text=_("The indexed product price"))
    is_available = models.BooleanField(
        verbose_name=_("is available"),
        default=False,
        db_index=True,
        editable=False,
        help_text=
        _("Whether the product is available for purchasing. This status is managed by the supplier module."
          ),
    )
    catalog_rule = models.ForeignKey(
        ProductCatalogPriceRule,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        verbose_name=_("Catalog rule"),
        help_text=_("The rule in which this price is available."),
    )

    def __str__(self):
        return f"{self.product} ({self.shop}, {self.supplier}) = {self.price}"

    class Meta:
        unique_together = ("product", "shop", "supplier", "catalog_rule")
コード例 #10
0
class CgpDiscount(MoneyPropped, CgpBase):
    discount_amount = PriceProperty("discount_amount_value", "shop.currency",
                                    "shop.prices_include_tax")
    discount_amount_value = MoneyValueField(verbose_name=_("discount amount"))

    class Meta(CgpBase.Meta):
        abstract = False
        verbose_name = _(u"product discount")
        verbose_name_plural = _(u"product discounts")

    def __repr__(self):
        return "<CgpDiscount (p%s,s%s,g%s): discount %s" % (
            self.product_id, self.shop_id, self.group_id, self.discount_amount)

    def save(self, *args, **kwargs):
        super(CgpDiscount, self).save(*args, **kwargs)

        # check if there is a shop product before bumping the cache
        if self.product.shop_products.filter(shop_id=self.shop.id).exists():
            bump_cache_for_product(self.product, self.shop)
コード例 #11
0
ファイル: _product_catalog.py プロジェクト: rrosajp/shuup
class ProductCatalogDiscountedPrice(MoneyPropped, models.Model):
    """
    Index the discounted prices of products.
    There can be multiple discounted prices, the best discounted price will be selected.
    """

    id = models.BigAutoField(primary_key=True)
    product = models.ForeignKey("shuup.Product",
                                related_name="catalog_discounted_prices",
                                on_delete=models.CASCADE,
                                editable=False)
    shop = models.ForeignKey("shuup.Shop",
                             related_name="catalog_discounted_prices",
                             on_delete=models.CASCADE,
                             editable=False)
    supplier = models.ForeignKey("shuup.Supplier",
                                 related_name="catalog_discounted_prices",
                                 on_delete=models.CASCADE,
                                 editable=False)
    discounted_price = PriceProperty("discounted_price_value", "shop.currency",
                                     "shop.prices_include_tax")
    discounted_price_value = MoneyValueField(
        editable=False,
        verbose_name=_("discounted price"),
        help_text=_("The indexed discounted product price."),
        null=True,
    )
    catalog_rule = models.ForeignKey(
        ProductCatalogDiscountedPriceRule,
        on_delete=models.CASCADE,
        verbose_name=_("Catalog rule"),
        help_text=_("The rule in which this discounted price is available."),
    )

    def __str__(self):
        return f"{self.product} ({self.shop}, {self.supplier}) = {self.discounted_price}"

    class Meta:
        unique_together = ("product", "shop", "supplier", "catalog_rule")
コード例 #12
0
class ShopProduct(MoneyPropped, models.Model):
    shop = models.ForeignKey("Shop",
                             related_name="shop_products",
                             on_delete=models.CASCADE,
                             verbose_name=_("shop"))
    product = UnsavedForeignKey("Product",
                                related_name="shop_products",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    suppliers = models.ManyToManyField(
        "Supplier",
        related_name="shop_products",
        blank=True,
        verbose_name=_("suppliers"),
        help_text=
        _("List your suppliers here. Suppliers can be found in Product Settings - Suppliers."
          ))

    visibility = EnumIntegerField(
        ShopProductVisibility,
        default=ShopProductVisibility.ALWAYS_VISIBLE,
        db_index=True,
        verbose_name=_("visibility"),
        help_text=mark_safe_lazy(
            _("Select if you want your product to be seen and found by customers. "
              "<p>Not visible: Product will not be shown in your store front or found in search.</p>"
              "<p>Searchable: Product will be shown in search but not listed on any category page.</p>"
              "<p>Listed: Product will be shown on category pages but not shown in search results.</p>"
              "<p>Always Visible: Product will be shown in your store front and found in search.</p>"
              )))
    purchasable = models.BooleanField(default=True,
                                      db_index=True,
                                      verbose_name=_("purchasable"))
    visibility_limit = EnumIntegerField(
        ProductVisibility,
        db_index=True,
        default=ProductVisibility.VISIBLE_TO_ALL,
        verbose_name=_('visibility limitations'),
        help_text=
        _("Select whether you want your product to have special limitations on its visibility in your store. "
          "You can make products visible to all, visible to only logged in users, or visible only to certain "
          "customer groups."))
    visibility_groups = models.ManyToManyField(
        "ContactGroup",
        related_name='visible_products',
        verbose_name=_('visible for groups'),
        blank=True,
        help_text=
        _(u"Select the groups you would like to make your product visible for. "
          u"These groups are defined in Contacts Settings - Contact Groups."))
    backorder_maximum = QuantityField(
        default=0,
        blank=True,
        null=True,
        verbose_name=_('backorder maximum'),
        help_text=_(
            "The number of units that can be purchased after the product is out of stock. "
            "Set to blank for product to be purchasable without limits."))
    purchase_multiple = QuantityField(
        default=0,
        verbose_name=_('purchase multiple'),
        help_text=_(
            "Set this if the product needs to be purchased in multiples. "
            "For example, if the purchase multiple is set to 2, then customers are required to order the product "
            "in multiples of 2."))
    minimum_purchase_quantity = QuantityField(
        default=1,
        verbose_name=_('minimum purchase'),
        help_text=_(
            "Set a minimum number of products needed to be ordered for the purchase. "
            "This is useful for setting bulk orders and B2B purchases."))
    limit_shipping_methods = models.BooleanField(
        default=False,
        verbose_name=_("limited for shipping methods"),
        help_text=_(
            "Check this if you want to limit your product to use only select payment methods. "
            "You can select the payment method(s) in the field below."))
    limit_payment_methods = models.BooleanField(
        default=False,
        verbose_name=_("limited for payment methods"),
        help_text=_(
            "Check this if you want to limit your product to use only select payment methods. "
            "You can select the payment method(s) in the field below."))
    shipping_methods = models.ManyToManyField(
        "ShippingMethod",
        related_name='shipping_products',
        verbose_name=_('shipping methods'),
        blank=True,
        help_text=_(
            "Select the shipping methods you would like to limit the product to using. "
            "These are defined in Settings - Shipping Methods."))
    payment_methods = models.ManyToManyField(
        "PaymentMethod",
        related_name='payment_products',
        verbose_name=_('payment methods'),
        blank=True,
        help_text=_(
            "Select the payment methods you would like to limit the product to using. "
            "These are defined in Settings - Payment Methods."))
    primary_category = models.ForeignKey(
        "Category",
        related_name='primary_shop_products',
        verbose_name=_('primary category'),
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        help_text=_(
            "Choose the primary category for your product. "
            "This will be the main category for classification in the system. "
            "Your product can be found under this category in your store. "
            "Categories are defined in Products Settings - Categories."))
    categories = models.ManyToManyField(
        "Category",
        related_name='shop_products',
        verbose_name=_('categories'),
        blank=True,
        help_text=_(
            "Add secondary categories for your product. "
            "These are other categories that your product fits under and that it can be found by in your store."
        ))
    shop_primary_image = models.ForeignKey(
        "ProductMedia",
        null=True,
        blank=True,
        related_name="primary_image_for_shop_products",
        on_delete=models.SET_NULL,
        verbose_name=_("primary image"),
        help_text=
        _("Click this to set this image as the primary display image for your product."
          ))

    # the default price of this product in the shop
    default_price = PriceProperty('default_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    default_price_value = MoneyValueField(
        verbose_name=_("default price"),
        null=True,
        blank=True,
        help_text=_(
            "This is the default individual base unit (or multi-pack) price of the product. "
            "All discounts or coupons will be based off of this price."))

    minimum_price = PriceProperty('minimum_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    minimum_price_value = MoneyValueField(
        verbose_name=_("minimum price"),
        null=True,
        blank=True,
        help_text=
        _("This is the default price that the product cannot go under in your store, "
          "despite coupons or discounts being applied. "
          "This is useful to make sure your product price stays above cost."))

    display_unit = models.ForeignKey(
        DisplayUnit,
        null=True,
        blank=True,
        verbose_name=_("display unit"),
        help_text=_("Unit for displaying quantities of this product"))

    class Meta:
        unique_together = ((
            "shop",
            "product",
        ), )

    def save(self, *args, **kwargs):
        self.clean()
        super(ShopProduct, self).save(*args, **kwargs)
        for supplier in self.suppliers.all():
            supplier.module.update_stock(product_id=self.product.id)

    def clean(self):
        super(ShopProduct, self).clean()
        if self.display_unit:
            if self.display_unit.internal_unit != self.product.sales_unit:
                raise ValidationError({
                    'display_unit':
                    _("Invalid display unit: Internal unit of "
                      "the selected display unit does not match "
                      "with the sales unit of the product")
                })

    def is_list_visible(self):
        """
        Return True if this product should be visible in listings in general,
        without taking into account any other visibility limitations.
        :rtype: bool
        """
        if self.product.deleted:
            return False
        if not self.listed:
            return False
        if self.product.is_variation_child():
            return False
        return True

    @property
    def primary_image(self):
        if self.shop_primary_image_id:
            return self.shop_primary_image
        else:
            return self.product.primary_image

    @property
    def searchable(self):
        return self.visibility in (ShopProductVisibility.SEARCHABLE,
                                   ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def listed(self):
        return self.visibility in (ShopProductVisibility.LISTED,
                                   ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def visible(self):
        return not (self.visibility == ShopProductVisibility.NOT_VISIBLE)

    @property
    def public_primary_image(self):
        primary_image = self.primary_image
        return primary_image if primary_image and primary_image.public else None

    def get_visibility_errors(self, customer):
        if self.product.deleted:
            yield ValidationError(_('This product has been deleted.'),
                                  code="product_deleted")

        if customer and customer.is_all_seeing:  # None of the further conditions matter for omniscient customers.
            return

        if not self.visible:
            yield ValidationError(_('This product is not visible.'),
                                  code="product_not_visible")

        is_logged_in = (bool(customer) and not customer.is_anonymous)

        if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL:
            yield ValidationError(
                _('The Product is invisible to users not logged in.'),
                code="product_not_visible_to_anonymous")

        if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS:
            # TODO: Optimization
            user_groups = set(customer.groups.all().values_list("pk",
                                                                flat=True))
            my_groups = set(self.visibility_groups.values_list("pk",
                                                               flat=True))
            if not bool(user_groups & my_groups):
                yield ValidationError(
                    _('This product is not visible to your group.'),
                    code="product_not_visible_to_group")

        for receiver, response in get_visibility_errors.send(
                ShopProduct, shop_product=self, customer=customer):
            for error in response:
                yield error

    # TODO: Refactor get_orderability_errors, it's too complex
    def get_orderability_errors(  # noqa (C901)
            self,
            supplier,
            quantity,
            customer,
            ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be orderable.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        for error in self.get_visibility_errors(customer):
            yield error

        if supplier is None and not self.suppliers.exists():
            # `ShopProduct` must have at least one `Supplier`.
            # If supplier is not given and the `ShopProduct` itself
            # doesn't have suppliers we cannot sell this product.
            yield ValidationError(_('The product has no supplier.'),
                                  code="no_supplier")

        if not ignore_minimum and quantity < self.minimum_purchase_quantity:
            yield ValidationError(_(
                'The purchase quantity needs to be at least %d for this product.'
            ) % self.minimum_purchase_quantity,
                                  code="purchase_quantity_not_met")

        if supplier and not self.suppliers.filter(pk=supplier.pk).exists():
            yield ValidationError(_('The product is not supplied by %s.') %
                                  supplier,
                                  code="invalid_supplier")

        if self.product.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            sellable = False
            for child_product in self.product.variation_children.all():
                child_shop_product = child_product.get_shop_instance(self.shop)
                if child_shop_product.is_orderable(
                        supplier=supplier,
                        customer=customer,
                        quantity=child_shop_product.minimum_purchase_quantity,
                        allow_cache=False):
                    sellable = True
                    break
            if not sellable:
                yield ValidationError(_("Product has no sellable children"),
                                      code="no_sellable_children")
        elif self.product.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            from shuup.core.models import ProductVariationResult
            sellable = False
            for combo in self.product.get_all_available_combinations():
                res = ProductVariationResult.resolve(
                    self.product, combo["variable_to_value"])
                if not res:
                    continue
                child_shop_product = res.get_shop_instance(self.shop)
                if child_shop_product.is_orderable(
                        supplier=supplier,
                        customer=customer,
                        quantity=child_shop_product.minimum_purchase_quantity,
                        allow_cache=False):
                    sellable = True
                    break
            if not sellable:
                yield ValidationError(_("Product has no sellable children"),
                                      code="no_sellable_children")

        if self.product.is_package_parent():
            for child_product, child_quantity in six.iteritems(
                    self.product.get_package_child_to_quantity_map()):
                try:
                    child_shop_product = child_product.get_shop_instance(
                        shop=self.shop, allow_cache=False)
                except ShopProduct.DoesNotExist:
                    yield ValidationError("%s: Not available in %s" %
                                          (child_product, self.shop),
                                          code="invalid_shop")
                else:
                    for error in child_shop_product.get_orderability_errors(
                            supplier=supplier,
                            quantity=(quantity * child_quantity),
                            customer=customer,
                            ignore_minimum=ignore_minimum):
                        message = getattr(error, "message", "")
                        code = getattr(error, "code", None)
                        yield ValidationError("%s: %s" %
                                              (child_product, message),
                                              code=code)

        if supplier and self.product.stock_behavior == StockBehavior.STOCKED:
            for error in supplier.get_orderability_errors(self,
                                                          quantity,
                                                          customer=customer):
                yield error

        purchase_multiple = self.purchase_multiple
        if quantity > 0 and purchase_multiple > 0 and (quantity %
                                                       purchase_multiple) != 0:
            p = (quantity // purchase_multiple)
            smaller_p = max(purchase_multiple, p * purchase_multiple)
            larger_p = max(purchase_multiple, (p + 1) * purchase_multiple)
            render_qty = self.unit.render_quantity
            if larger_p == smaller_p:
                message = _("The product can only be ordered in multiples of "
                            "{package_size}, for example {amount}").format(
                                package_size=render_qty(purchase_multiple),
                                amount=render_qty(smaller_p))
            else:
                message = _("The product can only be ordered in multiples of "
                            "{package_size}, for example {smaller_amount} or "
                            "{larger_amount}").format(
                                package_size=render_qty(purchase_multiple),
                                smaller_amount=render_qty(smaller_p),
                                larger_amount=render_qty(larger_p))
            yield ValidationError(message, code="invalid_purchase_multiple")

        for receiver, response in get_orderability_errors.send(
                ShopProduct,
                shop_product=self,
                customer=customer,
                supplier=supplier,
                quantity=quantity):
            for error in response:
                yield error

    def raise_if_not_orderable(self,
                               supplier,
                               customer,
                               quantity,
                               ignore_minimum=False):
        for message in self.get_orderability_errors(
                supplier=supplier,
                quantity=quantity,
                customer=customer,
                ignore_minimum=ignore_minimum):
            raise ProductNotOrderableProblem(message.args[0])

    def raise_if_not_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            raise ProductNotVisibleProblem(message.args[0])

    def is_orderable(self, supplier, customer, quantity, allow_cache=True):
        key, val = context_cache.get_cached_value(
            identifier="is_orderable",
            item=self,
            context={"customer": customer},
            supplier=supplier,
            quantity=quantity,
            allow_cache=allow_cache)
        if customer and val is not None:
            return val

        if not supplier:
            supplier = self.suppliers.first()  # TODO: Allow multiple suppliers
        for message in self.get_orderability_errors(supplier=supplier,
                                                    quantity=quantity,
                                                    customer=customer):
            if customer:
                context_cache.set_cached_value(key, False)
            return False

        if customer:
            context_cache.set_cached_value(key, True)
        return True

    def is_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            return False
        return True

    @property
    def quantity_step(self):
        """
        Quantity step for purchasing this product.

        :rtype: decimal.Decimal

        Example:
            <input type="number" step="{{ shop_product.quantity_step }}">
        """
        step = self.purchase_multiple or self._sales_unit.quantity_step
        return self._sales_unit.round(step)

    @property
    def rounded_minimum_purchase_quantity(self):
        """
        The minimum purchase quantity, rounded to the sales unit's precision.

        :rtype: decimal.Decimal

        Example:
            <input type="number"
                min="{{ shop_product.rounded_minimum_purchase_quantity }}"
                value="{{ shop_product.rounded_minimum_purchase_quantity }}">

        """
        return self._sales_unit.round(self.minimum_purchase_quantity)

    @property
    def display_quantity_step(self):
        """
        Quantity step of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(self.unit.to_display(self.quantity_step),
                   self.unit.display_precision)

    @property
    def display_quantity_minimum(self):
        """
        Quantity minimum of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(self.unit.to_display(self.minimum_purchase_quantity),
                   self.unit.display_precision)

    @property
    def unit(self):
        """
        Unit of this product.

        :rtype: shuup.core.models.UnitInterface
        """
        return UnitInterface(self._sales_unit, self.display_unit)

    @property
    def _sales_unit(self):
        return self.product.sales_unit or PiecesSalesUnit()

    @property
    def images(self):
        return self.product.media.filter(
            shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering")

    @property
    def public_images(self):
        return self.images.filter(public=True)
コード例 #13
0
ファイル: _order_lines.py プロジェクト: quintuslabs/shuup
class AbstractOrderLine(MoneyPropped, models.Model, Priceful):
    product = UnsavedForeignKey(
        "shuup.Product", blank=True, null=True, related_name="order_lines",
        on_delete=models.PROTECT, verbose_name=_('product')
    )
    supplier = UnsavedForeignKey(
        "shuup.Supplier", blank=True, null=True, related_name="order_lines",
        on_delete=models.PROTECT, verbose_name=_('supplier')
    )

    parent_line = UnsavedForeignKey(
        "self", related_name="child_lines", blank=True, null=True,
        on_delete=models.PROTECT, verbose_name=_('parent line')
    )
    ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
    type = EnumIntegerField(OrderLineType, default=OrderLineType.PRODUCT, verbose_name=_('line type'))
    sku = models.CharField(max_length=48, blank=True, verbose_name=_('line SKU'))
    text = models.CharField(max_length=256, verbose_name=_('line text'))
    accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('accounting identifier'))
    require_verification = models.BooleanField(default=False, verbose_name=_('require verification'))
    verified = models.BooleanField(default=False, verbose_name=_('verified'))
    extra_data = JSONField(blank=True, null=True, verbose_name=_('extra data'))
    labels = models.ManyToManyField("Label", blank=True, verbose_name=_("labels"))

    # The following fields govern calculation of the prices
    quantity = QuantityField(verbose_name=_('quantity'), default=1)
    base_unit_price = PriceProperty('base_unit_price_value', 'order.currency', 'order.prices_include_tax')
    discount_amount = PriceProperty('discount_amount_value', 'order.currency', 'order.prices_include_tax')

    base_unit_price_value = MoneyValueField(verbose_name=_('unit price amount (undiscounted)'), default=0)
    discount_amount_value = MoneyValueField(verbose_name=_('total amount of discount'), default=0)

    created_on = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name=_('created on'))
    modified_on = models.DateTimeField(
        default=timezone.now, editable=False, db_index=True, verbose_name=_('modified on')
    )

    objects = OrderLineManager()

    class Meta:
        verbose_name = _('order line')
        verbose_name_plural = _('order lines')
        abstract = True

    def __str__(self):
        return "%dx %s (%s)" % (self.quantity, self.text, self.get_type_display())

    @property
    def tax_amount(self):
        """
        :rtype: shuup.utils.money.Money
        """
        zero = Money(0, self.order.currency)
        return sum((x.amount for x in self.taxes.all()), zero)

    @property
    def max_refundable_amount(self):
        """
        :rtype: shuup.utils.money.Money
        """
        refunds = self.child_lines.refunds().filter(parent_line=self)
        refund_total_value = sum(refund.taxful_price.amount.value for refund in refunds)
        return (self.taxful_price.amount + Money(refund_total_value, self.order.currency))

    @property
    def max_refundable_quantity(self):
        if self.type == OrderLineType.REFUND:
            return 0
        return self.quantity - self.refunded_quantity

    @property
    def refunded_quantity(self):
        return (
            self.child_lines.filter(type=OrderLineType.REFUND).aggregate(total=Sum("quantity"))["total"] or 0
        )

    @property
    def shipped_quantity(self):
        if not self.product:
            return 0
        return ShipmentProduct.objects.filter(
            shipment__supplier=self.supplier.id,
            product_id=self.product.id,
            shipment__order=self.order
        ).aggregate(total=Sum("quantity"))["total"] or 0

    def save(self, *args, **kwargs):
        if not self.sku:
            self.sku = u""
        if self.type == OrderLineType.PRODUCT and not self.product_id:
            raise ValidationError("Error! Product-type order line can not be saved without a set product.")

        if self.product_id and self.type != OrderLineType.PRODUCT:
            raise ValidationError("Error! Order line has product but is not of Product-type.")

        if self.product_id and not self.supplier_id:
            raise ValidationError("Error! Order line has product, but no supplier.")

        super(AbstractOrderLine, self).save(*args, **kwargs)
        if self.product_id:
            self.supplier.module.update_stock(self.product_id)
コード例 #14
0
ファイル: _product_shops.py プロジェクト: Bobby00/boss_shuup
class ShopProduct(MoneyPropped, TranslatableModel):
    shop = models.ForeignKey("Shop", related_name="shop_products", on_delete=models.CASCADE, verbose_name=_("shop"))
    product = UnsavedForeignKey(
        "Product", related_name="shop_products", on_delete=models.CASCADE, verbose_name=_("product"))
    suppliers = models.ManyToManyField(
        "Supplier", related_name="shop_products", blank=True, verbose_name=_("suppliers"), help_text=_(
            "List your suppliers here. Suppliers can be found by searching for `Suppliers`."
        )
    )

    visibility = EnumIntegerField(
        ShopProductVisibility,
        default=ShopProductVisibility.ALWAYS_VISIBLE,
        db_index=True,
        verbose_name=_("visibility"),
        help_text=mark_safe_lazy(_(
            "Choose how you want your product to be seen and found by the customers. "
            "<p>Not visible: Product will not be shown in your store front nor found in search.</p>"
            "<p>Searchable: Product will be shown in search, but not listed on any category page.</p>"
            "<p>Listed: Product will be shown on category pages, but not shown in search results.</p>"
            "<p>Always Visible: Product will be shown in your store front and found in search.</p>"
        ))
    )
    purchasable = models.BooleanField(default=True, db_index=True, verbose_name=_("purchasable"))
    visibility_limit = EnumIntegerField(
        ProductVisibility, db_index=True, default=ProductVisibility.VISIBLE_TO_ALL,
        verbose_name=_('visibility limitations'), help_text=_(
            "Select whether you want your product to have special limitations on its visibility in your store. "
            "You can make products visible to all, visible to only logged-in users, or visible only to certain "
            "customer groups."
        )
    )
    visibility_groups = models.ManyToManyField(
        "ContactGroup", related_name='visible_products', verbose_name=_('visible for groups'), blank=True, help_text=_(
            u"Select the groups you want to make your product visible for. "
            u"These groups are defined in Contacts Settings - Contact Groups."
        )
    )
    backorder_maximum = QuantityField(
        default=0, blank=True, null=True, verbose_name=_('backorder maximum'), help_text=_(
            "The number of units that can be purchased after the product is already sold out (out of stock). "
            "Set to blank for product to be purchasable without limits."
        ))
    purchase_multiple = QuantityField(default=0, verbose_name=_('purchase multiple'), help_text=_(
            "Set this to other than 0 if the product needs to be purchased in multiples. "
            "For example, if the purchase multiple is set to 2, then customers are required to order the product "
            "in multiples of 2. Not to be confused with the Minimum Purchase Quantity."
        )
    )
    minimum_purchase_quantity = QuantityField(default=1, verbose_name=_('minimum purchase quantity'), help_text=_(
            "Set a minimum number of products needed to be ordered for the purchase. "
            "This is useful for setting bulk orders and B2B purchases."
        )
    )
    limit_shipping_methods = models.BooleanField(
        default=False, verbose_name=_("limit the shipping methods"), help_text=_(
            "Enable this if you want to limit your product to use only the select shipping methods. "
            "You can select the allowed shipping method(s) in the field below - all the rest "
            "are disallowed."
        )
    )
    limit_payment_methods = models.BooleanField(
        default=False, verbose_name=_("limit the payment methods"), help_text=_(
            "Enable this if you want to limit your product to use only the select payment methods. "
            "You can select the allowed payment method(s) in the field below - all the rest "
            "are disallowed."
        )
    )
    shipping_methods = models.ManyToManyField(
        "ShippingMethod", related_name='shipping_products', verbose_name=_('shipping methods'), blank=True, help_text=_(
            "If you enabled the `Limit the payment methods` choice above, then here you can select the "
            "individual shipping methods you want to ALLOW for this product. The ones not mentioned are "
            "disabled. To change this, search for `Shipping Methods`."
        )
    )
    payment_methods = models.ManyToManyField(
        "PaymentMethod", related_name='payment_products', verbose_name=_('payment methods'), blank=True, help_text=_(
            "If you enabled the `Limit the payment methods` choice above, then here you can select the "
            "individuals payment methods you want to ALLOW for this product. The ones not mentioned are "
            "disabled. To change this, search for `Payment Methods`."
        )
    )
    primary_category = models.ForeignKey(
        "Category", related_name='primary_shop_products', verbose_name=_('primary category'), blank=True, null=True,
        on_delete=models.PROTECT, help_text=_(
            "Choose the primary category for the product. "
            "This will be the main category for classification in the system. "
            "The product will be found under this category in your store. "
            "To change this, search for `Categories`."
        )
    )
    categories = models.ManyToManyField(
        "Category", related_name='shop_products', verbose_name=_('categories'), blank=True, help_text=_(
            "Add secondary categories for your product. "
            "These are other categories that your product fits under and that it can be found by in your store."
        )
    )
    shop_primary_image = models.ForeignKey(
        "ProductMedia", null=True, blank=True,
        related_name="primary_image_for_shop_products", on_delete=models.SET_NULL,
        verbose_name=_("primary image"), help_text=_(
            "Click this to set this image as the primary display image for the product."
        )
    )

    # the default price of this product in the shop
    default_price = PriceProperty('default_price_value', 'shop.currency', 'shop.prices_include_tax')
    default_price_value = MoneyValueField(verbose_name=_("default price"), null=True, blank=True, help_text=_(
            "This is the default individual base unit (or multi-pack) price of the product. "
            "All discounts or coupons will be calculated based off of this price."
        )
    )

    minimum_price = PriceProperty('minimum_price_value', 'shop.currency', 'shop.prices_include_tax')
    minimum_price_value = MoneyValueField(verbose_name=_("minimum price"), null=True, blank=True, help_text=_(
            "This is the default price that the product cannot go under in your store, "
            "despite coupons or discounts being applied. "
            "This is useful to make sure your product price stays above the cost."
        )
    )
    available_until = models.DateTimeField(verbose_name=_("available until"), null=True, blank=True, help_text=_(
        "After this date this product will be invisible in store front."
    ))

    display_unit = models.ForeignKey(
        DisplayUnit,
        on_delete=models.CASCADE,
        null=True, blank=True,
        verbose_name=_("display unit"),
        help_text=_("Unit for displaying quantities of this product.")
    )

    translations = TranslatedFields(
        name=models.CharField(
            blank=True, null=True, max_length=256, verbose_name=_('name'),
            help_text=_("Enter a descriptive name for your product. This will be its title in your store front.")),
        description=models.TextField(
            blank=True, null=True, verbose_name=_('description'),
            help_text=_(
                "To make your product stands 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(
            blank=True, null=True, max_length=150, verbose_name=_('short description'),
            help_text=_(
                "Enter a short description for your product. The short description will "
                "be used to get the attention of your customer with a small, but "
                "precise description of your product. It also helps with getting more "
                "traffic via search engines."
            )
        ),
        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)'
            )
        )
    )

    class Meta:
        unique_together = (("shop", "product",),)

    def save(self, *args, **kwargs):
        self.clean()
        super(ShopProduct, self).save(*args, **kwargs)
        for supplier in self.suppliers.enabled():
            supplier.module.update_stock(product_id=self.product.id)

    def clean(self):
        pre_clean.send(type(self), instance=self)
        super(ShopProduct, self).clean()
        if self.display_unit:
            if self.display_unit.internal_unit != self.product.sales_unit:
                raise ValidationError({'display_unit': _(
                    "Error! Invalid display unit: Internal unit of "
                    "the selected display unit does not match "
                    "with the sales unit of the product.")})
        post_clean.send(type(self), instance=self)

    def is_list_visible(self):
        """
        Return True if this product should be visible in listings in general,
        without taking into account any other visibility limitations.

        :rtype: bool
        """
        if self.product.deleted:
            return False
        if not self.listed:
            return False
        if self.product.is_variation_child():
            return False
        return True

    @property
    def primary_image(self):
        if self.shop_primary_image_id:
            return self.shop_primary_image
        else:
            return self.product.primary_image

    @property
    def searchable(self):
        return self.visibility in (ShopProductVisibility.SEARCHABLE, ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def listed(self):
        return self.visibility in (ShopProductVisibility.LISTED, ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def visible(self):
        return not (self.visibility == ShopProductVisibility.NOT_VISIBLE)

    @property
    def public_primary_image(self):
        primary_image = self.primary_image
        return primary_image if primary_image and primary_image.public else None

    def get_visibility_errors(self, customer):
        if self.product.deleted:
            yield ValidationError(_("This product has been deleted."), code="product_deleted")

        if customer and customer.is_all_seeing:  # None of the further conditions matter for omniscient customers.
            return

        if not self.visible:
            yield ValidationError(_("This product is not visible."), code="product_not_visible")

        if self.available_until and self.available_until <= now():
            yield ValidationError(
                _("Error! This product is not available until the current date."),
                code="product_not_available"
            )

        is_logged_in = (bool(customer) and not customer.is_anonymous)

        if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL:
            yield ValidationError(
                _("The Product is invisible to users not logged in."),
                code="product_not_visible_to_anonymous")

        if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS:
            # TODO: Optimization
            user_groups = set(customer.groups.all().values_list("pk", flat=True))
            my_groups = set(self.visibility_groups.values_list("pk", flat=True))
            if not bool(user_groups & my_groups):
                yield ValidationError(
                    _("This product is not visible to your group."),
                    code="product_not_visible_to_group"
                )

        # TODO: Remove from Shuup 2.0
        for receiver, response in get_visibility_errors.send(ShopProduct, shop_product=self, customer=customer):
            warnings.warn("Warning! Visibility errors through signals are deprecated.", DeprecationWarning)
            for error in response:
                yield error

    def get_orderability_errors(self, supplier, quantity, customer, ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be orderable.

        Shop product to be orderable it needs to be visible visible and purchasable.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        for error in self.get_visibility_errors(customer):
            yield error

        for error in self.get_purchasability_errors(supplier, customer, quantity, ignore_minimum):
            yield error

    def get_purchasability_errors(self, supplier, customer, quantity, ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be purchasable.

        Shop product to be purchasable it has to have purchasable attribute set on
        and pass all quantity and supplier checks.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        if not self.purchasable:
            yield ValidationError(_("The product is not purchasable."), code="not_purchasable")

        for error in self.get_quantity_errors(quantity, ignore_minimum):
            yield error

        for error in self.get_supplier_errors(supplier, customer, quantity, ignore_minimum):
            yield error

        # TODO: Remove from Shuup 2.0
        for receiver, response in get_orderability_errors.send(
            ShopProduct, shop_product=self, customer=customer, supplier=supplier, quantity=quantity
        ):
            warnings.warn("Warning! Orderability errors through signals are deprecated.", DeprecationWarning)
            for error in response:
                yield error

    def get_quantity_errors(self, quantity, ignore_minimum):
        if not ignore_minimum and quantity < self.minimum_purchase_quantity:
            yield ValidationError(
                _("The purchase quantity needs to be at least %d for this product.")
                % self.minimum_purchase_quantity,
                code="purchase_quantity_not_met"
            )

        purchase_multiple = self.purchase_multiple
        if quantity > 0 and purchase_multiple > 0 and (quantity % purchase_multiple) != 0:
            p = (quantity // purchase_multiple)
            smaller_p = max(purchase_multiple, p * purchase_multiple)
            larger_p = max(purchase_multiple, (p + 1) * purchase_multiple)
            render_qty = self.unit.render_quantity
            if larger_p == smaller_p:
                message = _(
                    "The product can only be ordered in multiples of "
                    "{package_size}, for example {amount}.").format(
                        package_size=render_qty(purchase_multiple),
                        amount=render_qty(smaller_p))
            else:
                message = _(
                    "The product can only be ordered in multiples of "
                    "{package_size}, for example {smaller_amount} or "
                    "{larger_amount}.").format(
                        package_size=render_qty(purchase_multiple),
                        smaller_amount=render_qty(smaller_p),
                        larger_amount=render_qty(larger_p))
            yield ValidationError(message, code="invalid_purchase_multiple")

    def get_supplier_errors(self, supplier, customer, quantity, ignore_minimum):
        enabled_supplier = self.suppliers.enabled(shop=self.shop)
        if supplier is None and not enabled_supplier.exists():
            # `ShopProduct` must have at least one `Supplier`.
            # If supplier is not given and the `ShopProduct` itself
            # doesn't have suppliers we cannot sell this product.
            yield ValidationError(
                _("The product has no supplier."),
                code="no_supplier"
            )

        if supplier and not enabled_supplier.filter(pk=supplier.pk).exists():
            yield ValidationError(
                _("The product is not supplied by %s.") % supplier,
                code="invalid_supplier"
            )

        errors = []
        if self.product.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            errors = self.get_orderability_errors_for_simple_variation_parent(supplier, customer)
        elif self.product.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            errors = self.get_orderability_errors_for_variable_variation_parent(supplier, customer)
        elif self.product.is_package_parent():
            errors = self.get_orderability_errors_for_package_parent(supplier, customer, quantity, ignore_minimum)
        elif supplier:  # Test supplier orderability only for variation children and normal products
            errors = supplier.get_orderability_errors(self, quantity, customer=customer)

        for error in errors:
            yield error

    def get_orderability_errors_for_simple_variation_parent(self, supplier, customer):
        sellable = False
        for child_product in self.product.variation_children.visible(shop=self.shop, customer=customer):
            try:
                child_shop_product = child_product.get_shop_instance(self.shop)
            except ShopProduct.DoesNotExist:
                continue

            if child_shop_product.is_orderable(
                    supplier=supplier,
                    customer=customer,
                    quantity=child_shop_product.minimum_purchase_quantity,
                    allow_cache=False
            ):
                sellable = True
                break

        if not sellable:
            yield ValidationError(_("Product has no sellable children."), code="no_sellable_children")

    def get_orderability_errors_for_variable_variation_parent(self, supplier, customer):
        from shuup.core.models import ProductVariationResult
        sellable = False
        for combo in self.product.get_all_available_combinations():
            res = ProductVariationResult.resolve(self.product, combo["variable_to_value"])
            if not res:
                continue
            try:
                child_shop_product = res.get_shop_instance(self.shop)
            except ShopProduct.DoesNotExist:
                continue

            if child_shop_product.is_orderable(
                    supplier=supplier,
                    customer=customer,
                    quantity=child_shop_product.minimum_purchase_quantity,
                    allow_cache=False
            ):
                sellable = True
                break
        if not sellable:
            yield ValidationError(_("Product has no sellable children."), code="no_sellable_children")

    def get_orderability_errors_for_package_parent(self, supplier, customer, quantity, ignore_minimum):
        for child_product, child_quantity in six.iteritems(self.product.get_package_child_to_quantity_map()):
            try:
                child_shop_product = child_product.get_shop_instance(shop=self.shop, allow_cache=False)
            except ShopProduct.DoesNotExist:
                yield ValidationError(
                    "Error! %s is not available in %s." % (child_product, self.shop), code="invalid_shop")
            else:
                for error in child_shop_product.get_orderability_errors(
                        supplier=supplier,
                        quantity=(quantity * child_quantity),
                        customer=customer,
                        ignore_minimum=ignore_minimum
                ):
                    message = getattr(error, "message", "")
                    code = getattr(error, "code", None)
                    yield ValidationError("Error! %s: %s" % (child_product, message), code=code)

    def raise_if_not_orderable(self, supplier, customer, quantity, ignore_minimum=False):
        for message in self.get_orderability_errors(
            supplier=supplier, quantity=quantity, customer=customer, ignore_minimum=ignore_minimum
        ):
            raise ProductNotOrderableProblem(message.args[0])

    def raise_if_not_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            raise ProductNotVisibleProblem(message.args[0])

    def is_orderable(self, supplier, customer, quantity, allow_cache=True):
        """
        Product to be orderable it needs to be visible and purchasable.
        """
        key, val = context_cache.get_cached_value(
            identifier="is_orderable", item=self, context={"customer": customer},
            supplier=supplier, stock_managed=bool(supplier and supplier.stock_managed),
            quantity=quantity, allow_cache=allow_cache)
        if customer and val is not None:
            return val

        if not supplier:
            supplier = self.get_supplier(customer, quantity)

        for message in self.get_orderability_errors(supplier=supplier, quantity=quantity, customer=customer):
            if customer:
                context_cache.set_cached_value(key, False)
            return False

        if customer:
            context_cache.set_cached_value(key, True)
        return True

    def is_visible(self, customer):
        """
        Visible products are shown in store front based on customer
        or customer group limitations.
        """
        for message in self.get_visibility_errors(customer=customer):
            return False
        return True

    def is_purchasable(self, supplier, customer, quantity):
        """
        Whether product can be purchased.
        """
        for message in self.get_purchasability_errors(supplier, customer, quantity):
            return False
        return True

    @property
    def quantity_step(self):
        """
        Quantity step for purchasing this product.

        :rtype: decimal.Decimal

        Example:
            <input type="number" step="{{ shop_product.quantity_step }}">
        """
        step = self.purchase_multiple or self._sales_unit.quantity_step
        return self._sales_unit.round(step)

    @property
    def rounded_minimum_purchase_quantity(self):
        """
        The minimum purchase quantity, rounded to the sales unit's precision.

        :rtype: decimal.Decimal

        Example:
            <input type="number"
                min="{{ shop_product.rounded_minimum_purchase_quantity }}"
                value="{{ shop_product.rounded_minimum_purchase_quantity }}">

        """
        return self._sales_unit.round(self.minimum_purchase_quantity)

    @property
    def display_quantity_step(self):
        """
        Quantity step of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(
            self.unit.to_display(self.quantity_step),
            self.unit.display_precision)

    @property
    def display_quantity_minimum(self):
        """
        Quantity minimum of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(
            self.unit.to_display(self.minimum_purchase_quantity),
            self.unit.display_precision)

    @property
    def unit(self):
        """
        Unit of this product.

        :rtype: shuup.core.models.UnitInterface
        """
        return UnitInterface(self._sales_unit, self.display_unit)

    @property
    def _sales_unit(self):
        return self.product.sales_unit or PiecesSalesUnit()

    @property
    def images(self):
        return self.product.media.filter(shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering")

    @property
    def public_images(self):
        return self.images.filter(public=True)

    def get_supplier(self, customer=None, quantity=None, shipping_address=None):
        supplier_strategy = cached_load("SHUUP_SHOP_PRODUCT_SUPPLIERS_STRATEGY")
        kwargs = {
            "shop_product": self,
            "customer": customer,
            "quantity": quantity,
            "shipping_address": shipping_address
        }
        return supplier_strategy().get_supplier(**kwargs)

    def __str__(self):
        return self.get_name()

    def get_name(self):
        return self._safe_get_string("name")

    def get_description(self):
        return self._safe_get_string("description")

    def get_short_description(self):
        return self._safe_get_string("short_description")

    def _safe_get_string(self, key):
        return (
            self.safe_translation_getter(key, any_language=True)
            or self.product.safe_translation_getter(key, any_language=True)
        )
コード例 #15
0
ファイル: _product_shops.py プロジェクト: dragonsg/shuup-1
class ShopProduct(MoneyPropped, models.Model):
    shop = models.ForeignKey("Shop",
                             related_name="shop_products",
                             on_delete=models.CASCADE,
                             verbose_name=_("shop"))
    product = UnsavedForeignKey("Product",
                                related_name="shop_products",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    suppliers = models.ManyToManyField("Supplier",
                                       related_name="shop_products",
                                       blank=True,
                                       verbose_name=_("suppliers"))

    visible = models.BooleanField(default=True,
                                  db_index=True,
                                  verbose_name=_("visible"))
    listed = models.BooleanField(default=True,
                                 db_index=True,
                                 verbose_name=_("listed"))
    purchasable = models.BooleanField(default=True,
                                      db_index=True,
                                      verbose_name=_("purchasable"))
    searchable = models.BooleanField(default=True,
                                     db_index=True,
                                     verbose_name=_("searchable"))
    visibility_limit = EnumIntegerField(
        ProductVisibility,
        db_index=True,
        default=ProductVisibility.VISIBLE_TO_ALL,
        verbose_name=_('visibility limitations'))
    visibility_groups = models.ManyToManyField(
        "ContactGroup",
        related_name='visible_products',
        verbose_name=_('visible for groups'),
        blank=True)
    purchase_multiple = QuantityField(default=0,
                                      verbose_name=_('purchase multiple'))
    minimum_purchase_quantity = QuantityField(
        default=1, verbose_name=_('minimum purchase'))
    limit_shipping_methods = models.BooleanField(
        default=False, verbose_name=_("limited for shipping methods"))
    limit_payment_methods = models.BooleanField(
        default=False, verbose_name=_("limited for payment methods"))
    shipping_methods = models.ManyToManyField(
        "ShippingMethod",
        related_name='shipping_products',
        verbose_name=_('shipping methods'),
        blank=True)
    payment_methods = models.ManyToManyField("PaymentMethod",
                                             related_name='payment_products',
                                             verbose_name=_('payment methods'),
                                             blank=True)
    primary_category = models.ForeignKey("Category",
                                         related_name='primary_shop_products',
                                         verbose_name=_('primary category'),
                                         blank=True,
                                         null=True,
                                         on_delete=models.PROTECT)
    categories = models.ManyToManyField("Category",
                                        related_name='shop_products',
                                        verbose_name=_('categories'),
                                        blank=True)
    shop_primary_image = models.ForeignKey(
        "ProductMedia",
        null=True,
        blank=True,
        related_name="primary_image_for_shop_products",
        on_delete=models.SET_NULL,
        verbose_name=_("primary image"))

    # the default price of this product in the shop
    default_price = PriceProperty('default_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    default_price_value = MoneyValueField(verbose_name=_("default price"),
                                          null=True,
                                          blank=True)

    minimum_price = PriceProperty('minimum_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    minimum_price_value = MoneyValueField(verbose_name=_("minimum price"),
                                          null=True,
                                          blank=True)

    class Meta:
        unique_together = ((
            "shop",
            "product",
        ), )

    def save(self, *args, **kwargs):
        super(ShopProduct, self).save(*args, **kwargs)
        for supplier in self.suppliers.all():
            supplier.module.update_stock(product_id=self.product.id)

    def is_list_visible(self):
        """
        Return True if this product should be visible in listings in general,
        without taking into account any other visibility limitations.
        :rtype: bool
        """
        if self.product.deleted:
            return False
        if not self.visible:
            return False
        if not self.listed:
            return False
        if self.product.is_variation_child():
            return False
        return True

    @property
    def primary_image(self):
        if self.shop_primary_image_id:
            return self.shop_primary_image
        else:
            return self.product.primary_image

    def get_visibility_errors(self, customer):
        if self.product.deleted:
            yield ValidationError(_('This product has been deleted.'),
                                  code="product_deleted")

        if customer and customer.is_all_seeing:  # None of the further conditions matter for omniscient customers.
            return

        if not self.visible:
            yield ValidationError(_('This product is not visible.'),
                                  code="product_not_visible")

        is_logged_in = (bool(customer) and not customer.is_anonymous)

        if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL:
            yield ValidationError(
                _('The Product is invisible to users not logged in.'),
                code="product_not_visible_to_anonymous")

        if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS:
            # TODO: Optimization
            user_groups = set(customer.groups.all().values_list("pk",
                                                                flat=True))
            my_groups = set(self.visibility_groups.values_list("pk",
                                                               flat=True))
            if not bool(user_groups & my_groups):
                yield ValidationError(
                    _('This product is not visible to your group.'),
                    code="product_not_visible_to_group")

        for receiver, response in get_visibility_errors.send(
                ShopProduct, shop_product=self, customer=customer):
            for error in response:
                yield error

    # TODO: Refactor get_orderability_errors, it's too complex
    def get_orderability_errors(  # noqa (C901)
            self,
            supplier,
            quantity,
            customer,
            ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be orderable.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        for error in self.get_visibility_errors(customer):
            yield error

        if supplier is None and not self.suppliers.exists():
            # `ShopProduct` must have at least one `Supplier`.
            # If supplier is not given and the `ShopProduct` itself
            # doesn't have suppliers we cannot sell this product.
            yield ValidationError(_('The product has no supplier.'),
                                  code="no_supplier")

        if not ignore_minimum and quantity < self.minimum_purchase_quantity:
            yield ValidationError(_(
                'The purchase quantity needs to be at least %d for this product.'
            ) % self.minimum_purchase_quantity,
                                  code="purchase_quantity_not_met")

        if supplier and not self.suppliers.filter(pk=supplier.pk).exists():
            yield ValidationError(_('The product is not supplied by %s.') %
                                  supplier,
                                  code="invalid_supplier")

        if self.product.is_package_parent():
            for child_product, child_quantity in six.iteritems(
                    self.product.get_package_child_to_quantity_map()):
                child_shop_product = child_product.get_shop_instance(
                    shop=self.shop)
                if not child_shop_product:
                    yield ValidationError("%s: Not available in %s" %
                                          (child_product, self.shop),
                                          code="invalid_shop")
                for error in child_shop_product.get_orderability_errors(
                        supplier=supplier,
                        quantity=(quantity * child_quantity),
                        customer=customer,
                        ignore_minimum=ignore_minimum):
                    code = getattr(error, "code", None)
                    yield ValidationError("%s: %s" % (child_product, error),
                                          code=code)

        if supplier and self.product.stock_behavior == StockBehavior.STOCKED:
            for error in supplier.get_orderability_errors(self,
                                                          quantity,
                                                          customer=customer):
                yield error

        purchase_multiple = self.purchase_multiple
        if quantity > 0 and purchase_multiple > 1 and (quantity %
                                                       purchase_multiple) != 0:
            p = (quantity // purchase_multiple)
            smaller_p = max(purchase_multiple, p * purchase_multiple)
            larger_p = max(purchase_multiple, (p + 1) * purchase_multiple)
            if larger_p == smaller_p:
                message = _(
                    'The product can only be ordered in multiples of %(package_size)s, '
                    'for example %(smaller_p)s %(unit)s.') % {
                        "package_size": purchase_multiple,
                        "smaller_p": smaller_p,
                        "unit": self.product.sales_unit,
                    }
            else:
                message = _(
                    'The product can only be ordered in multiples of %(package_size)s, '
                    'for example %(smaller_p)s or %(larger_p)s %(unit)s.') % {
                        "package_size": purchase_multiple,
                        "smaller_p": smaller_p,
                        "larger_p": larger_p,
                        "unit": self.product.sales_unit,
                    }
            yield ValidationError(message, code="invalid_purchase_multiple")

        for receiver, response in get_orderability_errors.send(
                ShopProduct,
                shop_product=self,
                customer=customer,
                supplier=supplier,
                quantity=quantity):
            for error in response:
                yield error

    def raise_if_not_orderable(self,
                               supplier,
                               customer,
                               quantity,
                               ignore_minimum=False):
        for message in self.get_orderability_errors(
                supplier=supplier,
                quantity=quantity,
                customer=customer,
                ignore_minimum=ignore_minimum):
            raise ProductNotOrderableProblem(message.args[0])

    def raise_if_not_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            raise ProductNotVisibleProblem(message.args[0])

    def is_orderable(self, supplier, customer, quantity):
        if not supplier:
            supplier = self.suppliers.first()  # TODO: Allow multiple suppliers
        for message in self.get_orderability_errors(supplier=supplier,
                                                    quantity=quantity,
                                                    customer=customer):
            return False
        return True

    def is_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            return False
        return True

    @property
    def quantity_step(self):
        """
        Quantity step for purchasing this product.

        :rtype: decimal.Decimal

        Example:
            <input type="number" step="{{ shop_product.quantity_step }}">
        """
        if self.purchase_multiple:
            return self.purchase_multiple
        return self.product.sales_unit.quantity_step

    @property
    def rounded_minimum_purchase_quantity(self):
        """
        The minimum purchase quantity, rounded to the sales unit's precision.

        :rtype: decimal.Decimal

        Example:
            <input type="number"
                min="{{ shop_product.rounded_minimum_purchase_quantity }}"
                value="{{ shop_product.rounded_minimum_purchase_quantity }}">

        """
        return self.product.sales_unit.round(self.minimum_purchase_quantity)

    @property
    def images(self):
        return self.product.media.filter(
            shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering")
コード例 #16
0
ファイル: test_properties.py プロジェクト: yashodhank/shuup
class FooItem(MoneyPropped, Base):
    price = PriceProperty('value', 'foo.currency', 'foo.includes_tax')
コード例 #17
0
class OrderLine(MoneyPropped, models.Model, Priceful):
    order = UnsavedForeignKey("Order",
                              related_name='lines',
                              on_delete=models.PROTECT,
                              verbose_name=_('order'))
    product = UnsavedForeignKey("Product",
                                blank=True,
                                null=True,
                                related_name="order_lines",
                                on_delete=models.PROTECT,
                                verbose_name=_('product'))
    supplier = UnsavedForeignKey("Supplier",
                                 blank=True,
                                 null=True,
                                 related_name="order_lines",
                                 on_delete=models.PROTECT,
                                 verbose_name=_('supplier'))

    parent_line = UnsavedForeignKey("self",
                                    related_name="child_lines",
                                    blank=True,
                                    null=True,
                                    on_delete=models.PROTECT,
                                    verbose_name=_('parent line'))
    ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
    type = EnumIntegerField(OrderLineType,
                            default=OrderLineType.PRODUCT,
                            verbose_name=_('line type'))
    sku = models.CharField(max_length=48,
                           blank=True,
                           verbose_name=_('line SKU'))
    text = models.CharField(max_length=256, verbose_name=_('line text'))
    accounting_identifier = models.CharField(
        max_length=32, blank=True, verbose_name=_('accounting identifier'))
    require_verification = models.BooleanField(
        default=False, verbose_name=_('require verification'))
    verified = models.BooleanField(default=False, verbose_name=_('verified'))
    extra_data = JSONField(blank=True, null=True, verbose_name=_('extra data'))

    # The following fields govern calculation of the prices
    quantity = QuantityField(verbose_name=_('quantity'), default=1)
    base_unit_price = PriceProperty('base_unit_price_value', 'order.currency',
                                    'order.prices_include_tax')
    discount_amount = PriceProperty('discount_amount_value', 'order.currency',
                                    'order.prices_include_tax')

    base_unit_price_value = MoneyValueField(
        verbose_name=_('unit price amount (undiscounted)'), default=0)
    discount_amount_value = MoneyValueField(
        verbose_name=_('total amount of discount'), default=0)

    objects = OrderLineManager()

    class Meta:
        verbose_name = _('order line')
        verbose_name_plural = _('order lines')

    def __str__(self):
        return "%dx %s (%s)" % (self.quantity, self.text,
                                self.get_type_display())

    @property
    def tax_amount(self):
        """
        :rtype: shuup.utils.money.Money
        """
        zero = Money(0, self.order.currency)
        return sum((x.amount for x in self.taxes.all()), zero)

    @property
    def max_refundable_amount(self):
        """
        :rtype: shuup.utils.money.Money
        """
        refunds = self.order.lines.refunds().filter(parent_line=self)
        refund_total_value = sum(refund.taxful_price.amount.value
                                 for refund in refunds)
        return (self.taxful_price.amount +
                Money(refund_total_value, self.order.currency))

    def save(self, *args, **kwargs):
        if not self.sku:
            self.sku = u""
        if self.type == OrderLineType.PRODUCT and not self.product_id:
            raise ValidationError(
                "Product-type order line can not be saved without a set product"
            )

        if self.product_id and self.type != OrderLineType.PRODUCT:
            raise ValidationError(
                "Order line has product but is not of Product type")

        if self.product_id and not self.supplier_id:
            raise ValidationError("Order line has product but no supplier")

        super(OrderLine, self).save(*args, **kwargs)
        if self.product_id:
            self.supplier.module.update_stock(self.product_id)