示例#1
0
class DiscountFromProduct(BasketLineEffect):
    identifier = "discount_from_product_line_effect"
    model = Product
    name = _("Discount from Product")

    per_line_discount = models.BooleanField(
        default=True,
        verbose_name=_("per line discount"),
        help_text=
        _("Uncheck this if you want to give discount for each matched product."
          ))

    discount_amount = MoneyValueField(default=None,
                                      blank=True,
                                      null=True,
                                      verbose_name=_("discount amount"),
                                      help_text=_("Flat amount of discount."))

    products = models.ManyToManyField(Product, verbose_name=_("product"))

    @property
    def description(self):
        return _("Select discount amount and products.")

    def get_discount_lines(self, order_source, original_lines):
        product_ids = self.products.values_list("pk", flat=True)
        campaign = self.campaign
        campaign_supplier = campaign.supplier if hasattr(
            campaign, "supplier") and campaign.supplier else None

        for line in original_lines:
            if campaign_supplier and line.supplier != campaign_supplier:
                continue
            if not line.type == OrderLineType.PRODUCT:
                continue
            if line.product.pk not in product_ids:
                continue

            base_price = line.base_unit_price.value * line.quantity
            amnt = (self.discount_amount * line.quantity
                    ) if not self.per_line_discount else self.discount_amount

            # we use min() to limit the amount of discount to the products price
            discount_price = order_source.create_price(min(base_price, amnt))

            if not line.discount_amount or line.discount_amount < discount_price:
                line.discount_amount = discount_price

            # check for minimum price, if set, and change the discount amount
            _limit_discount_amount_by_min_price(line, order_source)

        return []
示例#2
0
class WaivingCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Waiving cost")
    help_text = _("Add cost to price of the service if total price "
                  "of products is less than a waive limit.")

    price_value = MoneyValueField()
    waive_limit_value = MoneyValueField()
    description = TranslatedField(any_language=True)

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

    def get_costs(self, service, source):
        waive_limit = source.create_price(self.waive_limit_value)
        product_total = source.total_price_of_products
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter('description')
        zero_price = source.create_price(0)
        if product_total and product_total >= waive_limit:
            yield ServiceCost(zero_price, description, base_price=price)
        else:
            yield ServiceCost(price, description)
示例#3
0
class OrderTotalLimitBehaviorComponent(ServiceBehaviorComponent):
    name = _("Order total price limit")
    help_text = _("Limit service availability based on order's total price.")

    min_price_value = MoneyValueField(blank=True,
                                      null=True,
                                      verbose_name=_("min price value"))
    max_price_value = MoneyValueField(blank=True,
                                      null=True,
                                      verbose_name=_("max price value"))

    def get_unavailability_reasons(self, service, source):
        total = (source.taxful_total_price.value
                 if source.shop.prices_include_tax else
                 source.taxless_total_price.value)
        is_in_range = _is_in_range(total, self.min_price_value,
                                   self.max_price_value)
        if not is_in_range:
            yield ValidationError(_(
                "Order's total price is not within the defined service limits."
            ),
                                  code="order_total_out_of_range")
示例#4
0
class FixedCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Fixed cost")
    help_text = _("Add fixed cost to price of the service.")

    price_value = MoneyValueField()
    description = TranslatedField(any_language=True)

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

    def get_costs(self, service, source):
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter('description')
        yield ServiceCost(price, description)
class ContactGroupSalesRange(models.Model):
    group = models.ForeignKey(ContactGroup,
                              related_name="+",
                              on_delete=models.CASCADE,
                              verbose_name=_("group"))
    shop = models.ForeignKey(Shop, related_name="+", verbose_name=_("shop"))
    min_value = MoneyValueField(verbose_name=_("min amount"),
                                blank=True,
                                null=True)
    max_value = MoneyValueField(verbose_name=_("max amount"),
                                blank=True,
                                null=True)

    objects = SalesRangeQuerySet.as_manager()

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

    def save(self, *args, **kwargs):
        self.clean()
        super(ContactGroupSalesRange, self).save(*args, **kwargs)
        if self.is_active(
        ):  # Update group members only if the range is still active
            contact_ids = get_contacts_in_sales_range(self.shop,
                                                      self.min_value,
                                                      self.max_value)
            self.group.members = contact_ids

    def clean(self):
        super(ContactGroupSalesRange, self).clean()
        if self.group.identifier in PROTECTED_CONTACT_GROUP_IDENTIFIERS:
            raise ValidationError(
                _("Can not add sales limits for default contact groups"))

    def is_active(self):
        return bool(self.min_value is not None
                    and (self.max_value is None or self.max_value > 0))
示例#6
0
class DiscountFromCategoryProducts(BasketLineEffect):
    identifier = "discount_from_category_products_line_effect"
    model = Category
    name = _("Discount from Category products")

    discount_amount = MoneyValueField(default=None,
                                      blank=True,
                                      null=True,
                                      verbose_name=_("discount amount"),
                                      help_text=_("Flat amount of discount."))
    discount_percentage = models.DecimalField(
        max_digits=6,
        decimal_places=5,
        blank=True,
        null=True,
        verbose_name=_("discount percentage"),
        help_text=_("The discount percentage for this campaign."))
    category = models.ForeignKey(Category, verbose_name=_("category"))

    @property
    def description(self):
        return _(
            'Select discount amount and category. '
            'Please note that the discount will be given to all matching products in basket.'
        )

    def get_discount_lines(self, order_source, original_lines):
        if not (self.discount_percentage or self.discount_amount):
            return []

        product_ids = self.category.shop_products.values_list("product_id",
                                                              flat=True)
        for line in original_lines:  # Use original lines since we don't want to discount free product lines
            if not line.type == OrderLineType.PRODUCT:
                continue
            if line.product.pk not in product_ids:
                continue

            if self.discount_amount:
                amount = self.discount_amount * line.quantity
                discount_price = order_source.create_price(amount)
            elif self.discount_percentage:
                amount = line.taxless_price * self.discount_percentage
                discount_price = order_source.create_price(amount)

            if not line.discount_amount or line.discount_amount < discount_price:
                line.discount_amount = discount_price

        return []
示例#7
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
示例#8
0
class OrderLineTax(MoneyPropped, ShuupModel, LineTax):
    order_line = models.ForeignKey(
        OrderLine, related_name='taxes', on_delete=models.PROTECT,
        verbose_name=_('order line'))
    tax = models.ForeignKey(
        "Tax", related_name="order_line_taxes",
        on_delete=models.PROTECT, verbose_name=_('tax'))
    name = models.CharField(max_length=200, verbose_name=_('tax name'))

    amount = MoneyProperty('amount_value', 'order_line.order.currency')
    base_amount = MoneyProperty('base_amount_value', 'order_line.order.currency')

    amount_value = MoneyValueField(verbose_name=_('tax amount'))
    base_amount_value = MoneyValueField(
        verbose_name=_('base amount'),
        help_text=_('Amount that this tax is calculated from.'))

    ordering = models.IntegerField(default=0, verbose_name=_('ordering'))

    class Meta:
        ordering = ["ordering"]

    def __str__(self):
        return "%s: %s on %s" % (self.name, self.amount, self.base_amount)
示例#9
0
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 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)
示例#11
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"))

    @property
    def currency(self):
        return SHUUP_HOME_CURRENCY

    @property
    def includes_tax(self):
        return False
示例#12
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)
示例#13
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)
示例#14
0
class FixedCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Fixed cost")
    help_text = _("Add a fixed cost to the price of the service.")

    price_value = MoneyValueField(
        help_text=_("The fixed cost to apply to this service."))
    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 get_costs(self, service, source):
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter("description")
        yield ServiceCost(price, description)
示例#15
0
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):
        return (basket.total_price_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
示例#16
0
    def get_objects(self):
        order_line_taxes = OrderLineTax.objects.filter(
            order_line__order__in=super(TaxesReport, self).get_objects())

        tax = self.options.get("tax")
        tax_class = self.options.get("tax_class")

        filters = Q()
        if tax:
            filters &= Q(tax__in=tax)
        if tax_class:
            filters &= Q(order_line__product__tax_class__in=tax_class)

        return (order_line_taxes.filter(filters).values(
            "tax", "tax__rate").annotate(
                total_tax_amount=Sum("amount_value"),
                total_pretax_amount=Sum("base_amount_value"),
                total=Sum(F("amount_value") + F("base_amount_value"),
                          output_field=MoneyValueField()),
                order_count=Count("order_line__order", distinct=True),
            ).order_by("total_tax_amount"))
示例#17
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")]

    @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)
示例#18
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)
示例#19
0
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")
示例#20
0
class BasketDiscountAmount(BasketDiscountEffect):
    identifier = "discount_amount_effect"
    name = _("Discount amount value")

    discount_amount = MoneyValueField(
        default=None, blank=True, null=True, verbose_name=_("discount amount"), help_text=_("Flat amount of discount.")
    )

    @property
    def description(self):
        return _("Give discount amount.")

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

    @value.setter
    def value(self, value):
        self.discount_amount = value

    def apply_for_basket(self, order_source):
        return order_source.create_price(self.value)
示例#21
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)
示例#22
0
class ProductDiscountAmount(ProductDiscountEffect):
    identifier = "discount_amount_effect"
    name = _("Discount amount value")

    discount_amount = MoneyValueField(
        default=None, blank=True, null=True,
        verbose_name=_("discount amount"),
        help_text=_("Flat amount of discount."))

    @property
    def description(self):
        return _("Give discount amount.")

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

    @value.setter
    def value(self, value):
        self.discount_amount = value

    def apply_for_product(self, context, product, price_info):
        return price_info.price.new(self.value * price_info.quantity)
示例#23
0
class DiscountFromProduct(BasketLineEffect):
    identifier = "discount_from_product_line_effect"
    model = Product
    name = _("Discount from Product")

    per_line_discount = models.BooleanField(
        default=True,
        verbose_name=_("per line discount"),
        help_text=
        _("Uncheck this if you want to give discount for each matched product."
          ))

    discount_amount = MoneyValueField(default=None,
                                      blank=True,
                                      null=True,
                                      verbose_name=_("discount amount"),
                                      help_text=_("Flat amount of discount."))

    products = models.ManyToManyField(Product, verbose_name=_("product"))

    @property
    def description(self):
        return _("Select discount amount and products.")

    def get_discount_lines(self, order_source, original_lines):
        product_ids = self.products.values_list("pk", flat=True)
        for line in original_lines:
            if not line.type == OrderLineType.PRODUCT:
                continue
            if line.product.pk not in product_ids:
                continue
            amnt = (self.discount_amount * line.quantity
                    ) if not self.per_line_discount else self.discount_amount
            discount_price = order_source.create_price(amnt)
            if not line.discount_amount or line.discount_amount < discount_price:
                line.discount_amount = discount_price
        return []
示例#24
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)
示例#25
0
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)
示例#26
0
class Order(MoneyPropped, models.Model):
    # Identification
    shop = UnsavedForeignKey("Shop",
                             on_delete=models.PROTECT,
                             verbose_name=_("shop"))
    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,
                                       db_index=True,
                                       verbose_name=_("modified on"))
    identifier = InternalIdentifierField(unique=True,
                                         db_index=True,
                                         verbose_name=_("order identifier"))
    # TODO: label is actually a choice field, need to check migrations/choice deconstruction
    label = models.CharField(max_length=32,
                             db_index=True,
                             verbose_name=_("label"))
    # The key shouldn't be possible to deduce (i.e. it should be random), but it is
    # not a secret. (It could, however, be used as key material for an actual secret.)
    key = models.CharField(max_length=32,
                           unique=True,
                           blank=False,
                           verbose_name=_("key"))
    reference_number = models.CharField(max_length=64,
                                        db_index=True,
                                        unique=True,
                                        blank=True,
                                        null=True,
                                        verbose_name=_("reference number"))

    # Contact information
    customer = UnsavedForeignKey(
        "Contact",
        related_name="customer_orders",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("customer"),
    )
    orderer = UnsavedForeignKey(
        "PersonContact",
        related_name="orderer_orders",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("orderer"),
    )
    billing_address = models.ForeignKey(
        "ImmutableAddress",
        related_name="billing_orders",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("billing address"),
    )
    shipping_address = models.ForeignKey(
        "ImmutableAddress",
        related_name="shipping_orders",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("shipping address"),
    )
    tax_number = models.CharField(max_length=64,
                                  blank=True,
                                  verbose_name=_("tax number"))
    phone = models.CharField(max_length=64,
                             blank=True,
                             verbose_name=_("phone"))
    email = models.EmailField(max_length=128,
                              blank=True,
                              verbose_name=_("email address"))

    # Customer related information that might change after order, but is important
    # for accounting and/or reports later.
    account_manager = models.ForeignKey("PersonContact",
                                        blank=True,
                                        null=True,
                                        on_delete=models.PROTECT,
                                        verbose_name=_("account manager"))
    customer_groups = models.ManyToManyField(
        "ContactGroup",
        related_name="customer_group_orders",
        verbose_name=_("customer groups"),
        blank=True)
    tax_group = models.ForeignKey("CustomerTaxGroup",
                                  blank=True,
                                  null=True,
                                  on_delete=models.PROTECT,
                                  verbose_name=_("tax group"))

    # Status
    creator = UnsavedForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="orders_created",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("creating user"),
    )
    modified_by = UnsavedForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="orders_modified",
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        verbose_name=_("modifier user"),
    )
    deleted = models.BooleanField(db_index=True,
                                  default=False,
                                  verbose_name=_("deleted"))
    status = UnsavedForeignKey("OrderStatus",
                               verbose_name=_("status"),
                               on_delete=models.PROTECT)
    payment_status = EnumIntegerField(PaymentStatus,
                                      db_index=True,
                                      default=PaymentStatus.NOT_PAID,
                                      verbose_name=_("payment status"))
    shipping_status = EnumIntegerField(ShippingStatus,
                                       db_index=True,
                                       default=ShippingStatus.NOT_SHIPPED,
                                       verbose_name=_("shipping status"))

    # Methods
    payment_method = UnsavedForeignKey(
        "PaymentMethod",
        related_name="payment_orders",
        blank=True,
        null=True,
        default=None,
        on_delete=models.PROTECT,
        verbose_name=_("payment method"),
    )
    payment_method_name = models.CharField(
        max_length=100,
        blank=True,
        default="",
        verbose_name=_("payment method name"))
    payment_data = JSONField(blank=True,
                             null=True,
                             verbose_name=_("payment data"))

    shipping_method = UnsavedForeignKey(
        "ShippingMethod",
        related_name="shipping_orders",
        blank=True,
        null=True,
        default=None,
        on_delete=models.PROTECT,
        verbose_name=_("shipping method"),
    )
    shipping_method_name = models.CharField(
        max_length=100,
        blank=True,
        default="",
        verbose_name=_("shipping method name"))
    shipping_data = JSONField(blank=True,
                              null=True,
                              verbose_name=_("shipping data"))

    extra_data = JSONField(blank=True, null=True, verbose_name=_("extra data"))

    # Money stuff
    taxful_total_price = TaxfulPriceProperty("taxful_total_price_value",
                                             "currency")
    taxless_total_price = TaxlessPriceProperty("taxless_total_price_value",
                                               "currency")

    taxful_total_price_value = MoneyValueField(editable=False,
                                               verbose_name=_("grand total"),
                                               default=0)
    taxless_total_price_value = MoneyValueField(
        editable=False, verbose_name=_("taxless total"), default=0)
    currency = CurrencyField(verbose_name=_("currency"))
    prices_include_tax = models.BooleanField(
        verbose_name=_("prices include tax"))

    display_currency = CurrencyField(blank=True,
                                     verbose_name=_("display currency"))
    display_currency_rate = models.DecimalField(
        max_digits=36,
        decimal_places=9,
        default=1,
        verbose_name=_("display currency rate"))

    # Other
    ip_address = models.GenericIPAddressField(null=True,
                                              blank=True,
                                              verbose_name=_("IP address"))
    # `order_date` is not `auto_now_add` for backdating purposes
    order_date = models.DateTimeField(editable=False,
                                      db_index=True,
                                      verbose_name=_("order date"))
    payment_date = models.DateTimeField(null=True,
                                        editable=False,
                                        verbose_name=_("payment date"))

    language = LanguageField(blank=True, verbose_name=_("language"))
    customer_comment = models.TextField(blank=True,
                                        verbose_name=_("customer comment"))
    admin_comment = models.TextField(blank=True,
                                     verbose_name=_("admin comment/notes"))
    require_verification = models.BooleanField(
        default=False, verbose_name=_("requires verification"))
    all_verified = models.BooleanField(default=False,
                                       verbose_name=_("all lines verified"))
    marketing_permission = models.BooleanField(
        default=False, verbose_name=_("marketing permission"))
    _codes = JSONField(blank=True, null=True, verbose_name=_("codes"))

    common_select_related = ("billing_address", )
    objects = OrderQuerySet.as_manager()

    class Meta:
        ordering = ("-id", )
        verbose_name = _("order")
        verbose_name_plural = _("orders")

    def __str__(self):  # pragma: no cover
        if self.billing_address_id:
            name = self.billing_address.name
        else:
            name = "-"
        if ShuupSettings.get_setting("SHUUP_ENABLE_MULTIPLE_SHOPS"):
            return "Order %s (%s, %s)" % (self.identifier, self.shop.name,
                                          name)
        else:
            return "Order %s (%s)" % (self.identifier, name)

    @property
    def codes(self):
        return list(self._codes or [])

    @codes.setter
    def codes(self, value):
        codes = []
        for code in value:
            if not isinstance(code, six.text_type):
                raise TypeError("Error! `codes` must be a list of strings.")
            codes.append(code)
        self._codes = codes

    def cache_prices(self):
        taxful_total = TaxfulPrice(0, self.currency)
        taxless_total = TaxlessPrice(0, self.currency)
        for line in self.lines.all().prefetch_related("taxes"):
            taxful_total += line.taxful_price
            taxless_total += line.taxless_price
        self.taxful_total_price = taxful_total
        self.taxless_total_price = taxless_total

    def _cache_contact_values(self):
        sources = [
            self.shipping_address,
            self.billing_address,
            self.customer,
            self.orderer,
        ]

        fields = ("tax_number", "email", "phone")

        for field in fields:
            if getattr(self, field, None):
                continue
            for source in sources:
                val = getattr(source, field, None)
                if val:
                    setattr(self, field, val)
                    break

        if not self.id and self.customer:
            # These fields are used for reporting and should not
            # change after create even if empty at the moment of ordering.
            self.account_manager = getattr(self.customer, "account_manager",
                                           None)
            self.tax_group = self.customer.tax_group

    def _cache_contact_values_post_create(self):
        if self.customer:
            # These fields are used for reporting and should not
            # change after create even if empty at the  moment of ordering.
            self.customer_groups.set(self.customer.groups.all())

    def _cache_values(self):
        self._cache_contact_values()

        if not self.label:
            self.label = settings.SHUUP_DEFAULT_ORDER_LABEL

        if not self.currency:
            self.currency = self.shop.currency

        if not self.prices_include_tax:
            self.prices_include_tax = self.shop.prices_include_tax

        if not self.display_currency:
            self.display_currency = self.currency
            self.display_currency_rate = 1

        if self.shipping_method_id and not self.shipping_method_name:
            self.shipping_method_name = self.shipping_method.safe_translation_getter(
                "name",
                default=self.shipping_method.identifier,
                any_language=True)

        if self.payment_method_id and not self.payment_method_name:
            self.payment_method_name = self.payment_method.safe_translation_getter(
                "name",
                default=self.payment_method.identifier,
                any_language=True)

        if not self.key:
            self.key = get_random_string(32)

        if not self.modified_by:
            self.modified_by = self.creator

    def _save_identifiers(self):
        self.identifier = "%s" % (get_order_identifier(self))
        self.reference_number = get_reference_number(self)
        super(Order, self).save(update_fields=(
            "identifier",
            "reference_number",
        ))

    def full_clean(self, exclude=None, validate_unique=True):
        self._cache_values()
        return super(Order, self).full_clean(exclude, validate_unique)

    def save(self, *args, **kwargs):
        if not self.creator_id:
            if not settings.SHUUP_ALLOW_ANONYMOUS_ORDERS:
                raise ValidationError(
                    "Error! Anonymous (userless) orders are not allowed "
                    "when `SHUUP_ALLOW_ANONYMOUS_ORDERS` is not enabled.")
        self._cache_values()
        first_save = not self.pk
        old_status = self.status

        if not first_save:
            old_status = Order.objects.only("status").get(pk=self.pk).status

        super(Order, self).save(*args, **kwargs)

        if first_save:  # Have to do a double save the first time around to be able to save identifiers
            self._save_identifiers()
            self._cache_contact_values_post_create()

        order_changed.send(type(self), order=self)

        if self.status != old_status:
            order_status_changed.send(type(self),
                                      order=self,
                                      old_status=old_status,
                                      new_status=self.status)

    def delete(self, using=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Success! Deleted (soft).",
                               kind=LogEntryKind.DELETION)
            # Bypassing local `save()` on purpose.
            super(Order, self).save(update_fields=("deleted", ), using=using)

    def set_canceled(self):
        if self.status.role != OrderStatusRole.CANCELED:
            self.status = OrderStatus.objects.get_default_canceled()
            self.save()

    def _set_paid(self):
        if self.payment_status != PaymentStatus.FULLY_PAID:  # pragma: no branch
            self.add_log_entry(_("Order was marked as paid."))
            self.payment_status = PaymentStatus.FULLY_PAID
            self.payment_date = local_now()
            self.save()

    def _set_partially_paid(self):
        if self.payment_status != PaymentStatus.PARTIALLY_PAID:
            self.add_log_entry(_("Order was marked as partially paid."))
            self.payment_status = PaymentStatus.PARTIALLY_PAID
            self.save()

    def is_paid(self):
        return self.payment_status == PaymentStatus.FULLY_PAID

    def is_partially_paid(self):
        return self.payment_status == PaymentStatus.PARTIALLY_PAID

    def is_deferred(self):
        return self.payment_status == PaymentStatus.DEFERRED

    def is_not_paid(self):
        return self.payment_status == PaymentStatus.NOT_PAID

    def get_total_paid_amount(self):
        amounts = self.payments.values_list("amount_value", flat=True)
        return Money(sum(amounts, Decimal(0)), self.currency)

    def get_total_unpaid_amount(self):
        difference = self.taxful_total_price.amount - self.get_total_paid_amount(
        )
        return max(difference, Money(0, self.currency))

    def can_create_payment(self):
        zero = Money(0, self.currency)
        return not (self.is_paid() or self.is_canceled()
                    ) and self.get_total_unpaid_amount() > zero

    def create_payment(self, amount, payment_identifier=None, description=""):
        """
        Create a payment with a given amount for this order.

        If the order already has payments and sum of their amounts is
        equal or greater than `self.taxful_total_price` and the order is not
        a zero price order, an exception is raised.

        If the end sum of all payments is equal or greater than
        `self.taxful_total_price`, then the order is marked as paid.

        :param amount:
          Amount of the payment to be created.
        :type amount: Money
        :param payment_identifier:
          Identifier of the created payment. If not set, default value
          of `gateway_id:order_id:number` will be used (where `number` is
          a number of payments in the order).
        :type payment_identifier: str|None
        :param description:
          Description of the payment. Will be set to `method` property
          of the created payment.
        :type description: str

        :returns: The created Payment object
        :rtype: shuup.core.models.Payment
        """
        assert isinstance(amount, Money)
        assert amount.currency == self.currency

        payments = self.payments.order_by("created_on")

        total_paid_amount = self.get_total_paid_amount()
        if total_paid_amount >= self.taxful_total_price.amount and self.taxful_total_price:
            raise NoPaymentToCreateException(
                "Error! Order %s has already been fully paid (%s >= %s)." %
                (self.pk, total_paid_amount, self.taxful_total_price))

        if not payment_identifier:
            number = payments.count() + 1
            payment_identifier = "%d:%d" % (self.id, number)

        payment = self.payments.create(
            payment_identifier=payment_identifier,
            amount_value=amount.value,
            description=description,
        )

        if self.get_total_paid_amount() >= self.taxful_total_price.amount:
            self._set_paid()  # also calls save
        else:
            self._set_partially_paid()

        payment_created.send(sender=type(self), order=self, payment=payment)
        return payment

    def can_create_shipment(self):
        return self.get_unshipped_products(
        ) and not self.is_canceled() and self.shipping_address

    # TODO: Rethink either the usage of shipment parameter or renaming the method for 2.0
    @atomic
    def create_shipment(self,
                        product_quantities,
                        supplier=None,
                        shipment=None):
        """
        Create a shipment for this order from `product_quantities`.
        `product_quantities` is expected to be a dict, which maps Product instances to quantities.

        Only quantities over 0 are taken into account, and if the mapping is empty or has no quantity value
        over 0, `NoProductsToShipException` will be raised.

        Orders without a shipping address defined, will raise `NoShippingAddressException`.

        :param product_quantities: a dict mapping Product instances to quantities to ship.
        :type product_quantities: dict[shuup.shop.models.Product, decimal.Decimal]
        :param supplier: Optional Supplier for this product. No validation is made.
        :param shipment: Optional unsaved Shipment for ShipmentProduct's. If not given
                         Shipment is created based on supplier parameter.
        :raises: NoProductsToShipException, NoShippingAddressException
        :return: Saved, complete Shipment object.
        :rtype: shuup.core.models.Shipment
        """
        if not product_quantities or not any(
                quantity > 0 for quantity in product_quantities.values()):
            raise NoProductsToShipException(
                "Error! No products to ship (`quantities` is empty or has no quantity over 0)."
            )

        if self.shipping_address is None:
            raise NoShippingAddressException(
                "Error! Shipping address is not defined for this order.")

        assert supplier or shipment
        if shipment:
            assert shipment.order == self
        else:
            from ._shipments import Shipment

            shipment = Shipment(order=self, supplier=supplier)
        shipment.save()

        if not supplier:
            supplier = shipment.supplier

        supplier.module.ship_products(shipment, product_quantities)

        self.add_log_entry(
            _("Success! Shipment #%d was created.") % shipment.id)
        self.update_shipping_status()
        shipment_created.send(sender=type(self), order=self, shipment=shipment)
        shipment_created_and_processed.send(sender=type(self),
                                            order=self,
                                            shipment=shipment)
        return shipment

    def can_create_refund(self, supplier=None):
        unrefunded_amount = self.get_total_unrefunded_amount(supplier)
        unrefunded_quantity = self.get_total_unrefunded_quantity(supplier)
        return ((unrefunded_amount.value > 0 or unrefunded_quantity > 0)
                and not self.is_canceled()
                and (self.payment_status
                     not in (PaymentStatus.NOT_PAID, PaymentStatus.CANCELED)))

    @atomic
    def create_refund(self, refund_data, created_by=None, supplier=None):
        """
        Create a refund if passed a list of refund line data.

        Refund line data is simply a list of dictionaries where
        each dictionary contains data for a particular refund line.

        Additionally, if the parent line is of `enum` type
        `OrderLineType.PRODUCT` and the `restock_products` boolean
        flag is set to `True`, the products will be restocked with the
        exact amount set in the order supplier's `quantity` field.

        :param refund_data: List of dicts containing refund data.
        :type refund_data: [dict]
        :param created_by: Refund creator's user instance, used for
                           adjusting supplier stock.
        :type created_by: django.contrib.auth.User|None
        """
        tax_module = taxing.get_tax_module()
        refund_lines = tax_module.create_refund_lines(self, supplier,
                                                      created_by, refund_data)

        self.cache_prices()
        self.save()
        self.update_shipping_status()
        self.update_payment_status()
        refund_created.send(sender=type(self),
                            order=self,
                            refund_lines=refund_lines)

    def create_full_refund(self, restock_products=False, created_by=None):
        """
        Create a full refund for entire order content, with the option of
        restocking stocked products.

        :param restock_products: Boolean indicating whether to also restock the products.
        :param created_by: Refund creator's user instance, used for
                           adjusting supplier stock.
        :type restock_products: bool|False
        """
        if self.has_refunds():
            raise NoRefundToCreateException
        self.cache_prices()
        line_data = [{
            "line": line,
            "quantity": line.quantity,
            "amount": line.taxful_price.amount,
            "restock_products": restock_products,
        } for line in self.lines.filter(quantity__gt=0)
                     if line.type != OrderLineType.REFUND]
        self.create_refund(line_data, created_by)

    def get_total_refunded_amount(self, supplier=None):
        refunds = self.lines.refunds()
        if supplier:
            refunds = refunds.filter(
                Q(parent_line__supplier=supplier) | Q(supplier=supplier))
        total = sum([line.taxful_price.amount.value for line in refunds])
        return Money(-total, self.currency)

    def get_total_unrefunded_amount(self, supplier=None):
        if supplier:
            total_refund_amount = sum([
                line.max_refundable_amount.value for line in self.lines.filter(
                    supplier=supplier).exclude(type=OrderLineType.REFUND)
            ])
            arbitrary_refunds = abs(
                sum([
                    refund_line.taxful_price.value for refund_line in
                    self.lines.filter(supplier=supplier,
                                      parent_line__isnull=True,
                                      type=OrderLineType.REFUND)
                ]))
            return (Money(max(total_refund_amount -
                              arbitrary_refunds, 0), self.currency)
                    if total_refund_amount else Money(0, self.currency))
        return max(self.taxful_total_price.amount, Money(0, self.currency))

    def get_total_unrefunded_quantity(self, supplier=None):
        queryset = self.lines.all()
        if supplier:
            queryset = queryset.filter(supplier=supplier)
        return sum([line.max_refundable_quantity for line in queryset])

    def get_total_tax_amount(self):
        return sum((line.tax_amount for line in self.lines.all()),
                   Money(0, self.currency))

    def has_refunds(self):
        return self.lines.refunds().exists()

    def create_shipment_of_all_products(self, supplier=None):
        """
        Create a shipment of all the products in this Order, no matter whether or
        not any have been previously marked as shipped or not.

        See the documentation for `create_shipment`.

        :param supplier: The Supplier to use. If `None`, the first supplier in
                         the order is used. (If several are in the order, this fails.)
        :return: Saved, complete Shipment object.
        :rtype: shuup.shop.models.Shipment
        """
        from ._products import ShippingMode

        suppliers_to_product_quantities = defaultdict(
            lambda: defaultdict(lambda: 0))
        lines = self.lines.filter(
            type=OrderLineType.PRODUCT,
            product__shipping_mode=ShippingMode.SHIPPED).values_list(
                "supplier_id", "product_id", "quantity")
        for supplier_id, product_id, quantity in lines:
            if product_id:
                suppliers_to_product_quantities[supplier_id][
                    product_id] += quantity

        if not suppliers_to_product_quantities:
            raise NoProductsToShipException(
                "Error! Could not find any products to ship.")

        if supplier is None:
            if len(suppliers_to_product_quantities) > 1:  # pragma: no cover
                raise ValueError(
                    "Error! `create_shipment_of_all_products` can be used only when there is a single supplier."
                )
            supplier_id, quantities = suppliers_to_product_quantities.popitem()
            supplier = Supplier.objects.get(pk=supplier_id)
        else:
            quantities = suppliers_to_product_quantities[supplier.id]

        products = dict(
            (product.pk, product)
            for product in Product.objects.filter(pk__in=quantities.keys()))
        quantities = dict((products[product_id], quantity)
                          for (product_id, quantity) in quantities.items())
        return self.create_shipment(quantities, supplier=supplier)

    def check_all_verified(self):
        if not self.all_verified:
            new_all_verified = not self.lines.filter(verified=False).exists()
            if new_all_verified:
                self.all_verified = True
                if self.require_verification:
                    self.add_log_entry(
                        _("All rows requiring verification have been verified."
                          ))
                    self.require_verification = False
                self.save()
        return self.all_verified

    def get_purchased_attachments(self):
        from ._product_media import ProductMedia

        if self.payment_status != PaymentStatus.FULLY_PAID:
            return ProductMedia.objects.none()
        prods = self.lines.exclude(product=None).values_list("product_id",
                                                             flat=True)
        return ProductMedia.objects.filter(product__in=prods,
                                           enabled=True,
                                           purchased=True)

    def get_tax_summary(self):
        """
        :rtype: taxing.TaxSummary
        """
        all_line_taxes = []
        untaxed = TaxlessPrice(0, self.currency)
        for line in self.lines.all():
            line_taxes = list(line.taxes.all())
            all_line_taxes.extend(line_taxes)
            if not line_taxes:
                untaxed += line.taxless_price
        return taxing.TaxSummary.from_line_taxes(all_line_taxes, untaxed)

    def get_product_ids_and_quantities(self, supplier=None):
        lines = self.lines.filter(type=OrderLineType.PRODUCT)
        if supplier:
            supplier_id = supplier if isinstance(
                supplier, six.integer_types) else supplier.pk
            lines = lines.filter(supplier_id=supplier_id)

        quantities = defaultdict(lambda: 0)
        for product_id, quantity in lines.values_list("product_id",
                                                      "quantity"):
            quantities[product_id] += quantity
        return dict(quantities)

    def has_products(self):
        return self.lines.products().exists()

    def has_products_requiring_shipment(self, supplier=None):
        from ._products import ShippingMode

        lines = self.lines.products().filter(
            product__shipping_mode=ShippingMode.SHIPPED)
        if supplier:
            supplier_id = supplier if isinstance(
                supplier, six.integer_types) else supplier.pk
            lines = lines.filter(supplier_id=supplier_id)
        return lines.exists()

    def is_complete(self):
        return self.status.role == OrderStatusRole.COMPLETE

    def can_set_complete(self):
        return not (self.is_complete() or self.is_canceled()
                    or bool(self.get_unshipped_products()))

    def is_fully_shipped(self):
        return self.shipping_status == ShippingStatus.FULLY_SHIPPED

    def is_partially_shipped(self):
        return self.shipping_status == ShippingStatus.PARTIALLY_SHIPPED

    def is_canceled(self):
        return self.status.role == OrderStatusRole.CANCELED

    def can_set_canceled(self):
        canceled = self.status.role == OrderStatusRole.CANCELED
        paid = self.is_paid()
        shipped = self.shipping_status != ShippingStatus.NOT_SHIPPED
        return not (canceled or paid or shipped)

    def update_shipping_status(self):
        status_before_update = self.shipping_status
        if not self.get_unshipped_products():
            self.shipping_status = ShippingStatus.FULLY_SHIPPED
        elif self.shipments.all_except_deleted().count():
            self.shipping_status = ShippingStatus.PARTIALLY_SHIPPED
        else:
            self.shipping_status = ShippingStatus.NOT_SHIPPED
        if status_before_update != self.shipping_status:
            self.add_log_entry(
                _("New shipping status is set to: %(shipping_status)s." %
                  {"shipping_status": self.shipping_status}))
            self.save(update_fields=("shipping_status", ))

    def update_payment_status(self):
        status_before_update = self.payment_status
        if self.get_total_unpaid_amount().value == 0:
            self.payment_status = PaymentStatus.FULLY_PAID
        elif self.get_total_paid_amount().value > 0:
            self.payment_status = PaymentStatus.PARTIALLY_PAID
        elif self.payment_status != PaymentStatus.DEFERRED:  # Do not make deferred here not paid
            self.payment_status = PaymentStatus.NOT_PAID
        if status_before_update != self.payment_status:
            self.add_log_entry(
                _("New payment status is set to: %(payment_status)s." %
                  {"payment_status": self.payment_status}))
            self.save(update_fields=("payment_status", ))

    def get_known_additional_data(self):
        """
        Get a list of "known additional data" in this order's `payment_data`, `shipping_data` and `extra_data`.
        The list is returned in the order the fields are specified in the settings entries for said known keys.
        `dict(that_list)` can of course be used to "flatten" the list into a dict.
        :return: list of 2-tuples.
        """
        output = []
        for data_dict, name_mapping in (
            (self.payment_data, settings.SHUUP_ORDER_KNOWN_PAYMENT_DATA_KEYS),
            (self.shipping_data,
             settings.SHUUP_ORDER_KNOWN_SHIPPING_DATA_KEYS),
            (self.extra_data, settings.SHUUP_ORDER_KNOWN_EXTRA_DATA_KEYS),
        ):
            if hasattr(data_dict, "get"):
                for key, display_name in name_mapping:
                    if key in data_dict:
                        output.append(
                            (force_text(display_name), data_dict[key]))
        return output

    def get_product_summary(self, supplier=None):
        """Return a dict of product IDs -> {ordered, unshipped, refunded, shipped, line_text, suppliers}"""
        supplier_id = (supplier if isinstance(supplier, six.integer_types) else
                       supplier.pk) if supplier else None

        products = defaultdict(lambda: defaultdict(lambda: Decimal(0)))

        def _append_suppliers_info(product_id, supplier):
            if not products[product_id]["suppliers"]:
                products[product_id]["suppliers"] = [supplier]
            elif supplier not in products[product_id]["suppliers"]:
                products[product_id]["suppliers"].append(supplier)

        # Quantity for all orders
        # Note! This contains all product lines so we do not need to worry
        # about suppliers after this.
        lines = self.lines.filter(type=OrderLineType.PRODUCT)
        if supplier_id:
            lines = lines.filter(supplier_id=supplier_id)

        lines_values = lines.values_list("product_id", "text", "quantity",
                                         "supplier__name")
        for product_id, line_text, quantity, supplier_name in lines_values:
            products[product_id]["line_text"] = line_text
            products[product_id]["ordered"] += quantity
            _append_suppliers_info(product_id, supplier_name)

        # Quantity to ship
        for product_id, quantity in self._get_to_ship_quantities(supplier_id):
            products[product_id]["unshipped"] += quantity

        # Quantity shipped
        for product_id, quantity in self._get_shipped_quantities(supplier_id):
            products[product_id]["shipped"] += quantity
            products[product_id]["unshipped"] -= quantity

        # Quantity refunded
        for product_id in self._get_refunded_product_ids(supplier_id):
            refunds = self.lines.refunds().filter(
                parent_line__product_id=product_id)
            refunded_quantity = refunds.aggregate(
                total=models.Sum("quantity"))["total"] or 0
            products[product_id]["refunded"] = refunded_quantity
            products[product_id]["unshipped"] = max(
                products[product_id]["unshipped"] - refunded_quantity, 0)

        return products

    def _get_to_ship_quantities(self, supplier_id):
        from ._products import ShippingMode

        lines_to_ship = self.lines.filter(
            type=OrderLineType.PRODUCT,
            product__shipping_mode=ShippingMode.SHIPPED)
        if supplier_id:
            lines_to_ship = lines_to_ship.filter(supplier_id=supplier_id)
        return lines_to_ship.values_list("product_id", "quantity")

    def _get_shipped_quantities(self, supplier_id):
        from ._shipments import ShipmentProduct, ShipmentStatus

        shipment_prods = ShipmentProduct.objects.filter(
            shipment__order=self).exclude(
                shipment__status=ShipmentStatus.DELETED)
        if supplier_id:
            shipment_prods = shipment_prods.filter(
                shipment__supplier_id=supplier_id)
        return shipment_prods.values_list("product_id", "quantity")

    def _get_refunded_product_ids(self, supplier_id):
        refunded_prods = self.lines.refunds().filter(
            type=OrderLineType.REFUND, parent_line__type=OrderLineType.PRODUCT)
        if supplier_id:
            refunded_prods = refunded_prods.filter(
                parent_line__supplier_id=supplier_id)
        return refunded_prods.distinct().values_list("parent_line__product_id",
                                                     flat=True)

    def get_unshipped_products(self, supplier=None):
        return dict((product, summary_datum)
                    for product, summary_datum in self.get_product_summary(
                        supplier=supplier).items()
                    if summary_datum["unshipped"])

    def get_status_display(self):
        return force_text(self.status)

    def get_payment_method_display(self):
        return force_text(self.payment_method_name)

    def get_shipping_method_display(self):
        return force_text(self.shipping_method_name)

    def get_tracking_codes(self):
        return [
            shipment.tracking_code
            for shipment in self.shipments.all_except_deleted()
            if shipment.tracking_code
        ]

    def get_sent_shipments(self):
        return self.shipments.all_except_deleted().sent()

    def can_edit(self):
        return (settings.SHUUP_ALLOW_EDITING_ORDER and not self.has_refunds()
                and not self.is_canceled() and not self.is_complete()
                and self.shipping_status == ShippingStatus.NOT_SHIPPED
                and self.payment_status == PaymentStatus.NOT_PAID)

    def get_customer_name(self):
        name_attrs = [
            "customer", "billing_address", "orderer", "shipping_address"
        ]
        for attr in name_attrs:
            if getattr(self, "%s_id" % attr):
                return getattr(self, attr).name

    def get_available_shipping_methods(self):
        """
        Get available shipping methods.

        :rtype: list[ShippingMethod]
        """
        from shuup.core.models import ShippingMethod

        product_ids = self.lines.products().values_list("id", flat=True)
        return [
            m for m in ShippingMethod.objects.available(shop=self.shop,
                                                        products=product_ids)
            if m.is_available_for(self)
        ]

    def get_available_payment_methods(self):
        """
        Get available payment methods.

        :rtype: list[PaymentMethod]
        """
        from shuup.core.models import PaymentMethod

        product_ids = self.lines.products().values_list("id", flat=True)
        return [
            m for m in PaymentMethod.objects.available(shop=self.shop,
                                                       products=product_ids)
            if m.is_available_for(self)
        ]
示例#27
0
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)
        )
示例#28
0
文件: _taxes.py 项目: zarlant/shuup
class Tax(MoneyPropped, ChangeProtected, TranslatableShuupModel):
    identifier_attr = 'code'

    change_protect_message = _(
        "Cannot change business critical fields of Tax that is in use")
    unprotected_fields = ['enabled']

    code = InternalIdentifierField(
        unique=True, editable=True, verbose_name=_("code"), help_text="")

    translations = TranslatedFields(
        name=models.CharField(max_length=64, verbose_name=_("name")),
    )

    rate = models.DecimalField(
        max_digits=6, decimal_places=5, blank=True, null=True,
        verbose_name=_("tax rate"), help_text=_(
            "The percentage rate of the tax."))
    amount = MoneyProperty('amount_value', 'currency')
    amount_value = MoneyValueField(
        default=None, blank=True, null=True,
        verbose_name=_("tax amount value"), help_text=_(
            "The flat amount of the tax. "
            "Mutually exclusive with percentage rates."))
    currency = CurrencyField(
        default=None, blank=True, null=True,
        verbose_name=_("currency of tax amount"))

    enabled = models.BooleanField(default=True, verbose_name=_('enabled'))

    def clean(self):
        super(Tax, self).clean()
        if self.rate is None and self.amount is None:
            raise ValidationError(_('Either rate or amount is required'))
        if self.amount is not None and self.rate is not None:
            raise ValidationError(_('Cannot have both rate and amount'))
        if self.amount is not None and not self.currency:
            raise ValidationError(
                _("Currency is required if amount is specified"))

    def calculate_amount(self, base_amount):
        """
        Calculate tax amount with this tax for given base amount.

        :type base_amount: shuup.utils.money.Money
        :rtype: shuup.utils.money.Money
        """
        if self.amount is not None:
            return self.amount
        if self.rate is not None:
            return self.rate * base_amount
        raise ValueError("Improperly configured tax: %s" % self)

    def __str__(self):
        text = super(Tax, self).__str__()
        if self.rate is not None:
            text += " ({})".format(format_percent(self.rate, digits=3))
        if self.amount is not None:
            text += " ({})".format(format_money(self.amount))
        return text

    def _are_changes_protected(self):
        return self.order_line_taxes.exists()

    class Meta:
        verbose_name = _('tax')
        verbose_name_plural = _('taxes')
示例#29
0
class Basket(MoneyPropped, models.Model):
    # A combination of the PK and key is used to retrieve a basket for session situations.
    key = models.CharField(max_length=32,
                           default=generate_key,
                           verbose_name=_('key'),
                           unique=True,
                           db_index=True)

    shop = models.ForeignKey("Shop",
                             on_delete=models.CASCADE,
                             verbose_name=_('shop'))

    customer = models.ForeignKey("Contact",
                                 blank=True,
                                 null=True,
                                 on_delete=models.CASCADE,
                                 related_name="customer_core_baskets",
                                 verbose_name=_('customer'))
    orderer = models.ForeignKey("PersonContact",
                                blank=True,
                                null=True,
                                on_delete=models.CASCADE,
                                related_name="orderer_core_baskets",
                                verbose_name=_('orderer'))
    creator = models.ForeignKey(settings.AUTH_USER_MODEL,
                                blank=True,
                                null=True,
                                on_delete=models.CASCADE,
                                related_name="core_baskets_created",
                                verbose_name=_('creator'))

    created_on = models.DateTimeField(auto_now_add=True,
                                      db_index=True,
                                      editable=False,
                                      verbose_name=_('created on'))
    updated_on = models.DateTimeField(auto_now=True,
                                      db_index=True,
                                      editable=False,
                                      verbose_name=_('updated on'))
    persistent = models.BooleanField(db_index=True,
                                     default=False,
                                     verbose_name=_('persistent'))
    deleted = models.BooleanField(db_index=True,
                                  default=False,
                                  verbose_name=_('deleted'))
    finished = models.BooleanField(db_index=True,
                                   default=False,
                                   verbose_name=_('finished'))
    title = models.CharField(max_length=64,
                             blank=True,
                             verbose_name=_('title'))
    data = TaggedJSONField(verbose_name=_('data'))

    # For statistics etc., as `data` is opaque:
    taxful_total_price = TaxfulPriceProperty('taxful_total_price_value',
                                             'currency')
    taxless_total_price = TaxlessPriceProperty('taxless_total_price_value',
                                               'currency')

    taxless_total_price_value = MoneyValueField(
        default=0,
        null=True,
        blank=True,
        verbose_name=_('taxless total price'))
    taxful_total_price_value = MoneyValueField(
        default=0, null=True, blank=True, verbose_name=_('taxful total price'))
    currency = CurrencyField(verbose_name=_('currency'))
    prices_include_tax = models.BooleanField(
        verbose_name=_('prices include tax'))

    product_count = models.IntegerField(default=0,
                                        verbose_name=_('product_count'))
    products = ManyToManyField("Product",
                               blank=True,
                               verbose_name=_('products'))

    class Meta:
        verbose_name = _('basket')
        verbose_name_plural = _('baskets')
示例#30
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