コード例 #1
0
ファイル: _service_behavior.py プロジェクト: vituocgia/wshop
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(
        help_text=_("The cost to apply to this service if the total price is below the waive limit."))
    waive_limit_value = MoneyValueField(help_text=_(
        "The total price of products at which this service cost is waived."
    ))
    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):
        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)
コード例 #2
0
class AbstractPayment(MoneyPropped, models.Model):
    created_on = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created on'))
    gateway_id = models.CharField(
        max_length=32, verbose_name=_('gateway ID'))  # TODO: do we need this?
    payment_identifier = models.CharField(max_length=96,
                                          unique=True,
                                          verbose_name=_('identifier'))

    amount = MoneyProperty('amount_value', 'currency')
    foreign_amount = MoneyProperty('foreign_amount_value', 'foreign_currency')

    amount_value = MoneyValueField(verbose_name=_('amount'))
    foreign_amount_value = MoneyValueField(default=None,
                                           blank=True,
                                           null=True,
                                           verbose_name=_('foreign amount'))
    foreign_currency = CurrencyField(default=None,
                                     blank=True,
                                     null=True,
                                     verbose_name=_('foreign amount currency'))

    description = models.CharField(max_length=256,
                                   blank=True,
                                   verbose_name=_('description'))

    class Meta:
        abstract = True
コード例 #3
0
ファイル: _order_lines.py プロジェクト: vituocgia/wshop
class OrderLineTax(MoneyPropped, WshopModel, 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)
コード例 #4
0
ファイル: _service_behavior.py プロジェクト: vituocgia/wshop
class OrderTotalLimitBehaviorComponent(ServiceBehaviorComponent):
    name = _("Order total limit")
    help_text = _("Limit service availability based on order total")

    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 total does not match with service limits."), code="order_total_out_of_range")
コード例 #5
0
ファイル: basket_conditions.py プロジェクト: vituocgia/wshop
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
コード例 #6
0
ファイル: models.py プロジェクト: vituocgia/wshop
class StockAdjustment(models.Model):
    product = models.ForeignKey("wshop.Product",
                                related_name="+",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    supplier = models.ForeignKey("wshop.Supplier",
                                 on_delete=models.CASCADE,
                                 verbose_name=_("supplier"))
    created_on = models.DateTimeField(auto_now_add=True,
                                      editable=False,
                                      db_index=True,
                                      verbose_name=_("created on"))
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                   blank=True,
                                   null=True,
                                   on_delete=models.PROTECT,
                                   verbose_name=_("created by"))
    delta = QuantityField(default=0, verbose_name=_("delta"))
    purchase_price_value = MoneyValueField(default=0)
    purchase_price = PriceProperty("purchase_price_value", "currency",
                                   "includes_tax")
    type = EnumIntegerField(StockAdjustmentType,
                            db_index=True,
                            default=StockAdjustmentType.INVENTORY,
                            verbose_name=_("type"))

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

    @cached_property
    def includes_tax(self):
        return _get_prices_include_tax()
コード例 #7
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.variation_parent:
                if line.product.variation_parent.pk not in product_ids and line.product.pk not in product_ids:
                    continue
            else:
                if line.product.pk not in product_ids:
                    continue

            amount = order_source.zero_price.value
            base_price = line.base_unit_price.value * line.quantity

            if self.discount_amount:
                amount = self.discount_amount * line.quantity
            elif self.discount_percentage:
                amount = base_price * self.discount_percentage

            # we use min() to limit the amount of discount to base price
            # also in percentage, since one can configure 150% of discount
            discount_price = order_source.create_price(min(base_price, amount))

            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 []
コード例 #8
0
ファイル: _service_behavior.py プロジェクト: vituocgia/wshop
class FixedCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Fixed cost")
    help_text = _("Add fixed cost to 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)
コード例 #9
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

            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 []
コード例 #10
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")
コード例 #11
0
ファイル: models.py プロジェクト: vituocgia/wshop
class StockCount(models.Model):
    alert_limit = QuantityField(default=0,
                                editable=False,
                                verbose_name=_("alert limit"))
    product = models.ForeignKey("wshop.Product",
                                related_name="+",
                                editable=False,
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    supplier = models.ForeignKey("wshop.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)
コード例 #12
0
ファイル: basket_conditions.py プロジェクト: vituocgia/wshop
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 wshop.campaigns.models import CatalogCampaign

        total_undiscounted_price_value = basket.total_price_of_products.value
        shop = basket.shop
        context = PricingContext(shop, basket.customer)
        for line in basket.get_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
コード例 #13
0
ファイル: _service_behavior.py プロジェクト: vituocgia/wshop
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)
コード例 #14
0
ファイル: models.py プロジェクト: vituocgia/wshop
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)
コード例 #15
0
ファイル: basket_effects.py プロジェクト: vituocgia/wshop
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)
コード例 #16
0
ファイル: product_effects.py プロジェクト: vituocgia/wshop
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)
コード例 #17
0
ファイル: _order_lines.py プロジェクト: vituocgia/wshop
class AbstractOrderLine(MoneyPropped, models.Model, Priceful):
    product = UnsavedForeignKey("wshop.Product",
                                blank=True,
                                null=True,
                                related_name="order_lines",
                                on_delete=models.PROTECT,
                                verbose_name=_('product'))
    supplier = UnsavedForeignKey("wshop.Supplier",
                                 blank=True,
                                 null=True,
                                 related_name="order_lines",
                                 on_delete=models.PROTECT,
                                 verbose_name=_('supplier'))

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

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

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

    objects = OrderLineManager()

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

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

    @property
    def tax_amount(self):
        """
        :rtype: wshop.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: wshop.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.PRODUCT:
            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(
                "Product-type order line can not be saved without a set product"
            )

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

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

        super(AbstractOrderLine, self).save(*args, **kwargs)
        if self.product_id:
            self.supplier.module.update_stock(self.product_id)
コード例 #18
0
ファイル: _basket.py プロジェクト: vituocgia/wshop
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')
コード例 #19
0
ファイル: _taxes.py プロジェクト: vituocgia/wshop
class Tax(MoneyPropped, ChangeProtected, TranslatableWshopModel):
    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=_("The abbreviated tax code name."))

    translations = TranslatedFields(name=models.CharField(
        max_length=124,
        verbose_name=_("name"),
        help_text=
        _("The tax name. This is shown in order lines in order invoices and confirmations."
          )), )

    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'),
        help_text=_("Check this if this tax is valid and active."))

    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: wshop.utils.money.Money
        :rtype: wshop.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')
コード例 #20
0
ファイル: _product_shops.py プロジェクト: vituocgia/wshop
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 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"))

    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."
              )),
        description=models.TextField(
            blank=True,
            null=True,
            verbose_name=_(
                'description'),
            help_text=
            _("To make your product stand out, give it an awesome description. "
              "This is what will help your shoppers learn about your products. "
              "It will also help shoppers find them in the store and on the web."
              )),
        short_description=models.CharField(
            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."
              )))

    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):
        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':
                    _("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")

        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: wshop.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: wshop.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():
                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")
        elif self.product.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            from wshop.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")

        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

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

        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 get_quantity_errors(self, quantity):
        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 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.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):
        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: wshop.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(
            "WSHOP_SHOP_PRODUCT_SUPPLIERS_STRATEGY")
        kwargs = {
            "shop_product": self,
            "customer": customer,
            "quantity": quantity,
            "shipping_address": shipping_address
        }
        return supplier_strategy().get_supplier(**kwargs)