示例#1
0
def test_money_field_formfield():
    field = MoneyField(
        'price', currency='BTC', default='5', max_digits=9, decimal_places=2)
    form_field = field.formfield()
    assert isinstance(form_field, forms.MoneyField)
    assert form_field.currency == 'BTC'
    assert isinstance(form_field.widget, widgets.MoneyInput)
示例#2
0
class ShippingMethod(models.Model):
    name = models.CharField(max_length=100)
    type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
    price = MoneyField(currency=settings.DEFAULT_CURRENCY,
                       max_digits=12,
                       decimal_places=settings.DEFAULT_DECIMAL_PLACES,
                       default=0)
    shipping_zone = models.ForeignKey(ShippingZone,
                                      related_name='shipping_methods',
                                      on_delete=models.CASCADE)
    minimum_order_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=12,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        blank=True,
        null=True)
    maximum_order_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=12,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True)
    minimum_order_weight = MeasurementField(measurement=Weight,
                                            unit_choices=WeightUnits.CHOICES,
                                            default=zero_weight,
                                            blank=True,
                                            null=True)
    maximum_order_weight = MeasurementField(measurement=Weight,
                                            unit_choices=WeightUnits.CHOICES,
                                            blank=True,
                                            null=True)

    objects = ShippingMethodQueryset.as_manager()
    translated = TranslationProxy()

    def __str__(self):
        return self.name

    def __repr__(self):
        if self.type == ShippingMethodType.PRICE_BASED:
            minimum = '%s%s' % (self.minimum_order_price.amount,
                                self.minimum_order_price.currency)
            max_price = self.maximum_order_price
            maximum = ('%s%s' % (max_price.amount, max_price.currency)
                       if max_price else 'no limit')
            return 'ShippingMethod(type=%s min=%s, max=%s)' % (
                self.type, minimum, maximum)
        return 'ShippingMethod(type=%s weight_range=(%s)' % (
            self.type,
            get_weight_type_display(self.minimum_order_weight,
                                    self.maximum_order_weight))

    def get_total(self, taxes=None):
        return get_taxed_shipping_price(self.price, taxes)

    def get_ajax_label(self):
        price_html = format_money(self.price)
        label = mark_safe('%s %s' % (self, price_html))
        return label

    def get_type_display(self):
        if self.type == ShippingMethodType.PRICE_BASED:
            return get_price_type_display(self.minimum_order_price,
                                          self.maximum_order_price)
        return get_weight_type_display(self.minimum_order_weight,
                                       self.maximum_order_weight)
示例#3
0
class Product(SeoModel, ModelWithMetadata, PublishableModel):
    product_type = models.ForeignKey(ProductType,
                                     related_name="products",
                                     on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    description = models.TextField(blank=True)
    description_json = SanitizedJSONField(blank=True,
                                          default=dict,
                                          sanitizer=clean_draft_js)
    category = models.ForeignKey(Category,
                                 related_name="products",
                                 on_delete=models.CASCADE)
    price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    attributes = FilterableJSONBField(default=dict,
                                      blank=True,
                                      validators=[validate_attribute_json])
    updated_at = models.DateTimeField(auto_now=True, null=True)
    charge_taxes = models.BooleanField(default=True)
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)
    objects = ProductsQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        app_label = "product"
        ordering = ("name", )
        permissions = ((
            "manage_products",
            pgettext_lazy("Permission description", "Manage products."),
        ), )

    def __iter__(self):
        if not hasattr(self, "__variants"):
            setattr(self, "__variants", self.variants.all())
        return iter(getattr(self, "__variants"))

    def __repr__(self):
        class_ = type(self)
        return "<%s.%s(pk=%r, name=%r)>" % (
            class_.__module__,
            class_.__name__,
            self.pk,
            self.name,
        )

    def __str__(self):
        return self.name

    @property
    def plain_text_description(self):
        if settings.USE_JSON_CONTENT:
            return json_content_to_raw_text(self.description_json)
        return strip_tags(self.description)

    @property
    def is_available(self):
        return self.is_visible and self.is_in_stock()

    def get_absolute_url(self):
        return reverse("product:details",
                       kwargs={
                           "slug": self.get_slug(),
                           "product_id": self.id
                       })

    def get_slug(self):
        return slugify(smart_text(unidecode(self.name)))

    def is_in_stock(self):
        return any(variant.is_in_stock() for variant in self)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else None

    def get_price_range(self, discounts: Iterable[DiscountInfo] = None):
        if self.variants.all():
            prices = [variant.get_price(discounts) for variant in self]
            return MoneyRange(min(prices), max(prices))
        price = calculate_discounted_price(self, self.price, discounts)
        return MoneyRange(start=price, stop=price)
示例#4
0
class Product(SeoModel, ModelWithMetadata, PublishableModel):
    product_type = models.ForeignKey(ProductType,
                                     related_name="products",
                                     on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    slug = models.SlugField(max_length=255, unique=True, allow_unicode=True)
    description = models.TextField(blank=True)
    description_json = SanitizedJSONField(blank=True,
                                          default=dict,
                                          sanitizer=clean_draft_js)
    category = models.ForeignKey(
        Category,
        related_name="products",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )

    minimal_variant_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    minimal_variant_price = MoneyField(
        amount_field="minimal_variant_price_amount", currency_field="currency")
    updated_at = models.DateTimeField(auto_now=True, null=True)
    charge_taxes = models.BooleanField(default=True)
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)
    objects = ProductsQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        app_label = "product"
        ordering = ("slug", )
        permissions = ((ProductPermissions.MANAGE_PRODUCTS.codename,
                        "Manage products."), )

    def __iter__(self):
        if not hasattr(self, "__variants"):
            setattr(self, "__variants", self.variants.all())
        return iter(getattr(self, "__variants"))

    def __repr__(self) -> str:
        class_ = type(self)
        return "<%s.%s(pk=%r, name=%r)>" % (
            class_.__module__,
            class_.__name__,
            self.pk,
            self.name,
        )

    def __str__(self) -> str:
        return self.name

    @property
    def plain_text_description(self) -> str:
        return json_content_to_raw_text(self.description_json)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else None

    @staticmethod
    def sort_by_attribute_fields() -> list:
        return ["concatenated_values_order", "concatenated_values", "name"]
示例#5
0
class Cart(models.Model):
    """A shopping cart."""

    created = models.DateTimeField(auto_now_add=True)
    last_change = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             blank=True,
                             null=True,
                             related_name='carts',
                             on_delete=models.CASCADE)
    email = models.EmailField(blank=True, default='')
    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    quantity = models.PositiveIntegerField(default=0)
    billing_address = models.ForeignKey(Address,
                                        related_name='+',
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name='+',
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    shipping_method = models.ForeignKey(ShippingMethod,
                                        blank=True,
                                        null=True,
                                        related_name='carts',
                                        on_delete=models.SET_NULL)
    note = models.TextField(blank=True, default='')
    discount_amount = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=12,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0)
    discount_name = models.CharField(max_length=255, blank=True, null=True)
    translated_discount_name = models.CharField(max_length=255,
                                                blank=True,
                                                null=True)
    voucher_code = models.CharField(max_length=12, blank=True, null=True)

    objects = CartQueryset.as_manager()

    class Meta:
        ordering = ('-last_change', )

    def __repr__(self):
        return 'Cart(quantity=%s)' % (self.quantity, )

    def __iter__(self):
        return iter(self.lines.all())

    def __len__(self):
        return self.lines.count()

    def is_shipping_required(self):
        """Return `True` if any of the lines requires shipping."""
        return any(line.is_shipping_required() for line in self)

    def get_shipping_price(self, taxes):
        return (self.shipping_method.get_total_price(taxes)
                if self.shipping_method and self.is_shipping_required() else
                ZERO_TAXED_MONEY)

    def get_subtotal(self, discounts=None, taxes=None):
        """Return the total cost of the cart prior to shipping."""
        subtotals = (line.get_total(discounts, taxes) for line in self)
        return sum(subtotals, ZERO_TAXED_MONEY)

    def get_total(self, discounts=None, taxes=None):
        """Return the total cost of the cart."""
        return (self.get_subtotal(discounts, taxes) +
                self.get_shipping_price(taxes) - self.discount_amount)

    def get_total_weight(self):
        # Cannot use `sum` as it parses an empty Weight to an int
        weights = Weight(kg=0)
        for line in self:
            weights += line.variant.get_weight() * line.quantity
        return weights

    def get_line(self, variant):
        """Return a line matching the given variant and data if any."""
        matching_lines = (line for line in self if line.variant == variant)
        return next(matching_lines, None)
示例#6
0
class Product(SeoModel, ModelWithMetadata, PublishableModel):
    product_type = models.ForeignKey(ProductType,
                                     related_name="products",
                                     on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    slug = models.SlugField(max_length=255, unique=True)
    description = models.TextField(blank=True)
    description_json = SanitizedJSONField(blank=True,
                                          default=dict,
                                          sanitizer=clean_draft_js)
    category = models.ForeignKey(
        Category,
        related_name="products",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )

    price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    price = MoneyField(amount_field="price_amount", currency_field="currency")

    minimal_variant_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    minimal_variant_price = MoneyField(
        amount_field="minimal_variant_price_amount", currency_field="currency")
    updated_at = models.DateTimeField(auto_now=True, null=True)
    charge_taxes = models.BooleanField(default=True)
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)
    objects = ProductsQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        app_label = "product"
        ordering = ("name", )
        permissions = ((ProductPermissions.MANAGE_PRODUCTS.codename,
                        "Manage products."), )

    def __iter__(self):
        if not hasattr(self, "__variants"):
            setattr(self, "__variants", self.variants.all())
        return iter(getattr(self, "__variants"))

    def __repr__(self) -> str:
        class_ = type(self)
        return "<%s.%s(pk=%r, name=%r)>" % (
            class_.__module__,
            class_.__name__,
            self.pk,
            self.name,
        )

    def __str__(self) -> str:
        return self.name

    def save(self,
             force_insert=False,
             force_update=False,
             using=None,
             update_fields=None):
        # Make sure the "minimal_variant_price_amount" is set
        if self.minimal_variant_price_amount is None:
            self.minimal_variant_price_amount = self.price_amount

        return super().save(force_insert, force_update, using, update_fields)

    @property
    def plain_text_description(self) -> str:
        return json_content_to_raw_text(self.description_json)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else None

    def get_price_range(
            self,
            discounts: Optional[Iterable[DiscountInfo]] = None) -> MoneyRange:
        import opentracing

        with opentracing.global_tracer().start_active_span("get_price_range"):
            if self.variants.all():
                prices = [variant.get_price(discounts) for variant in self]
                return MoneyRange(min(prices), max(prices))
            price = calculate_discounted_price(
                product=self,
                price=self.price,
                collections=self.collections.all(),
                discounts=discounts,
            )
            return MoneyRange(start=price, stop=price)

    @staticmethod
    def sort_by_attribute_fields() -> list:
        return ["concatenated_values_order", "concatenated_values", "name"]
示例#7
0
class Product(SeoModel, PublishableModel):
    product_type = models.ForeignKey(ProductType,
                                     related_name='products',
                                     on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    description = models.TextField()
    category = models.ForeignKey(Category,
                                 related_name='products',
                                 on_delete=models.CASCADE)
    price = MoneyField(currency=settings.DEFAULT_CURRENCY,
                       max_digits=settings.DEFAULT_MAX_DIGITS,
                       decimal_places=settings.DEFAULT_DECIMAL_PLACES)
    attributes = HStoreField(default=dict, blank=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)
    charge_taxes = models.BooleanField(default=True)
    tax_rate = models.CharField(max_length=128,
                                default=DEFAULT_TAX_RATE_NAME,
                                blank=True,
                                choices=TaxRateType.CHOICES)
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)

    translated = TranslationProxy()

    class Meta:
        app_label = 'product'
        ordering = ('name', )
        permissions = (('manage_products',
                        pgettext_lazy('Permission description',
                                      'Manage products.')), )

    def __iter__(self):
        if not hasattr(self, '__variants'):
            setattr(self, '__variants', self.variants.all())
        return iter(getattr(self, '__variants'))

    def __repr__(self):
        class_ = type(self)
        return '<%s.%s(pk=%r, name=%r)>' % (class_.__module__, class_.__name__,
                                            self.pk, self.name)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('product:details',
                       kwargs={
                           'slug': self.get_slug(),
                           'product_id': self.id
                       })

    def get_slug(self):
        return slugify(smart_text(unidecode(self.name)))

    def is_in_stock(self):
        return any(variant.is_in_stock() for variant in self)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else None

    def get_price_range(self, discounts=None, taxes=None):
        if self.variants.all():
            prices = [
                variant.get_price(discounts=discounts, taxes=taxes)
                for variant in self
            ]
            return TaxedMoneyRange(min(prices), max(prices))
        price = calculate_discounted_price(self, self.price, discounts)
        if not self.charge_taxes:
            taxes = None
        tax_rate = self.tax_rate or self.product_type.tax_rate
        price = apply_tax_to_price(taxes, tax_rate, price)
        return TaxedMoneyRange(start=price, stop=price)
示例#8
0
def test_money_field_get_prep_value():
    field = MoneyField(
        'price', currency='BTC', default='5', max_digits=9, decimal_places=2)
    assert field.get_prep_value(Money(5, 'BTC')) == Decimal(5)
示例#9
0
class Voucher(models.Model):
    type = models.CharField(max_length=20,
                            choices=VoucherType.CHOICES,
                            default=VoucherType.ENTIRE_ORDER)
    name = models.CharField(max_length=255, null=True, blank=True)
    code = models.CharField(max_length=12, unique=True, db_index=True)
    usage_limit = models.PositiveIntegerField(null=True, blank=True)
    used = models.PositiveIntegerField(default=0, editable=False)
    start_date = models.DateTimeField(default=timezone.now)
    end_date = models.DateTimeField(null=True, blank=True)
    # this field indicates if discount should be applied per order or
    # individually to every item
    apply_once_per_order = models.BooleanField(default=False)
    apply_once_per_customer = models.BooleanField(default=False)

    discount_value_type = models.CharField(
        max_length=10,
        choices=DiscountValueType.CHOICES,
        default=DiscountValueType.FIXED,
    )
    discount_value = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    discount = MoneyField(amount_field="discount_value",
                          currency_field="currency")

    # not mandatory fields, usage depends on type
    countries = CountryField(multiple=True, blank=True)
    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )
    min_spent_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    min_spent = MoneyField(amount_field="min_spent_amount",
                           currency_field="currency")
    min_checkout_items_quantity = models.PositiveIntegerField(null=True,
                                                              blank=True)
    products = models.ManyToManyField("product.Product", blank=True)
    collections = models.ManyToManyField("product.Collection", blank=True)
    categories = models.ManyToManyField("product.Category", blank=True)

    objects = VoucherQueryset.as_manager()
    translated = TranslationProxy()

    def __str__(self):
        if self.name:
            return self.name
        discount = "%s %s" % (
            self.discount_value,
            self.get_discount_value_type_display(),
        )
        if self.type == VoucherType.SHIPPING:
            if self.is_free:
                return "Free shipping"
            return f"{discount} off shipping"
        if self.type == VoucherType.SPECIFIC_PRODUCT:
            return f"%{discount} off specific products"
        return f"{discount} off"

    @property
    def is_free(self):
        return (self.discount_value == Decimal(100)
                and self.discount_value_type == DiscountValueType.PERCENTAGE)

    def get_discount(self):
        if self.discount_value_type == DiscountValueType.FIXED:
            discount_amount = Money(self.discount_value,
                                    settings.DEFAULT_CURRENCY)
            return partial(fixed_discount, discount=discount_amount)
        if self.discount_value_type == DiscountValueType.PERCENTAGE:
            return partial(percentage_discount, percentage=self.discount_value)
        raise NotImplementedError("Unknown discount type")

    def get_discount_amount_for(self, price: Money):
        discount = self.get_discount()
        after_discount = discount(price)
        if after_discount.amount < 0:
            return price
        return price - after_discount

    def validate_min_spent(self, value: Money):
        if self.min_spent and value < self.min_spent:
            msg = f"This offer is only valid for orders over {amount(self.min_spent)}."
            raise NotApplicable(msg, min_spent=self.min_spent)

    def validate_min_checkout_items_quantity(self, quantity):
        min_checkout_items_quantity = self.min_checkout_items_quantity
        if min_checkout_items_quantity and min_checkout_items_quantity > quantity:
            msg = ("This offer is only valid for orders with a minimum of "
                   f"{min_checkout_items_quantity} quantity.")
            raise NotApplicable(
                msg,
                min_checkout_items_quantity=min_checkout_items_quantity,
            )

    def validate_once_per_customer(self, customer_email):
        voucher_customer = VoucherCustomer.objects.filter(
            voucher=self, customer_email=customer_email)
        if voucher_customer:
            msg = "This offer is valid only once per customer."
            raise NotApplicable(msg)
示例#10
0
class ProductVariant(ModelWithMetadata):
    sku = models.CharField(max_length=255, unique=True, null=True)
    name = models.CharField(max_length=255, blank=True)
    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
        blank=True,
        null=True,
    )
    price_override_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    price_override = MoneyField(amount_field="price_override_amount",
                                currency_field="currency")
    product = models.ForeignKey(Product,
                                related_name="variants",
                                on_delete=models.CASCADE)
    images = models.ManyToManyField("ProductImage", through="VariantImage")
    videos = models.ManyToManyField("ProductVideo", through="VariantVideo")
    track_inventory = models.BooleanField(default=True)
    quantity = models.IntegerField(validators=[MinValueValidator(0)],
                                   default=Decimal(1))
    quantity_allocated = models.IntegerField(validators=[MinValueValidator(0)],
                                             default=Decimal(1))
    cost_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    cost_price = MoneyField(amount_field="cost_price_amount",
                            currency_field="currency")
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)

    objects = ProductVariantQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        app_label = "product"

    def __str__(self):
        return ''

    @property
    def quantity_available(self):
        # No need for video courses/quantity is limitless
        # return max(self.quantity - self.quantity_allocated, 0)
        return 1

    @property
    def is_visible(self):
        return self.product.is_visible

    @property
    def is_available(self):
        return self.is_visible and self.is_in_stock()

    def check_quantity(self, quantity):
        """Check if there is at least the given quantity in stock.

        If stock handling is disabled, it simply run no check.
        """
        # No need for video courses/quantity is limitless
        # if self.track_inventory and quantity > self.quantity_available:
        #     raise InsufficientStock(self)
        pass

    @property
    def base_price(self):
        return (self.price_override
                if self.price_override is not None else self.product.price)

    def get_price(self, discounts: Iterable[DiscountInfo] = None):
        return calculate_discounted_price(self.product, self.base_price,
                                          discounts)

    def get_weight(self):
        return self.weight or self.product.weight or self.product.product_type.weight

    def get_absolute_url(self):
        slug = self.product.get_slug()
        product_id = self.product.id
        return reverse("product:details",
                       kwargs={
                           "slug": slug,
                           "product_id": product_id
                       })

    def is_shipping_required(self):
        return False

    def is_digital(self):
        return True

    def is_in_stock(self):
        return self.quantity_available > 0

    def display_product(self, translated=False):
        if translated:
            product = self.product.translated
            variant_display = str(self.translated)
        else:
            variant_display = str(self)
            product = self.product
        product_display = (f"{product} ({variant_display})"
                           if variant_display else str(product))
        return smart_text(product_display)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else self.product.get_first_image()

    def get_ajax_label(self, discounts=None):
        price = self.get_price(discounts)
        return "%s, %s, %s" % (self.sku, self.display_product(),
                               prices.amount(price))
示例#11
0
def test_money_field_init():
    field = MoneyField(currency='BTC',
                       default='5',
                       max_digits=9,
                       decimal_places=2)
    assert field.get_default() == Money(5, 'BTC')
示例#12
0
def test_money_field_from_db_value_checks_min_value():
    field = MoneyField(
        'price', currency='BTC', default='5', max_digits=9, decimal_places=2)
    invalid = Money(1, 'USD')
    with pytest.raises(ValueError):
        field.from_db_value(invalid, None, None, None)
示例#13
0
def test_money_field_from_db_value_handles_none():
    field = MoneyField(
        'price', currency='BTC', default='5', max_digits=9, decimal_places=2)
    assert field.from_db_value(None, None, None, None) is None
示例#14
0
def test_money_field_get_db_prep_save():
    field = MoneyField(
        'price', currency='BTC', default='5', max_digits=9, decimal_places=2)
    value = field.get_db_prep_save(Money(5, 'BTC'), connection)
    assert value == '5.00'
示例#15
0
class Product(SeoModel):
    product_type = models.ForeignKey(ProductType,
                                     related_name='products',
                                     on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    description = models.TextField()
    category = models.ForeignKey(Category,
                                 related_name='products',
                                 on_delete=models.CASCADE)
    price = MoneyField(currency=settings.DEFAULT_CURRENCY,
                       max_digits=12,
                       decimal_places=settings.DEFAULT_DECIMAL_PLACES)
    available_on = models.DateField(blank=True, null=True)
    is_published = models.BooleanField(default=True)
    attributes = HStoreField(default={}, blank=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)
    is_featured = models.BooleanField(default=False)
    charge_taxes = models.BooleanField(default=True)
    tax_rate = models.CharField(max_length=128,
                                default=DEFAULT_TAX_RATE_NAME,
                                blank=True)

    objects = ProductQuerySet.as_manager()

    class Meta:
        app_label = 'product'
        permissions = (('view_product',
                        pgettext_lazy('Permission description',
                                      'Can view products')),
                       ('edit_product',
                        pgettext_lazy('Permission description',
                                      'Can edit products')),
                       ('view_properties',
                        pgettext_lazy('Permission description',
                                      'Can view product properties')),
                       ('edit_properties',
                        pgettext_lazy('Permission description',
                                      'Can edit product properties')))

    def __iter__(self):
        if not hasattr(self, '__variants'):
            setattr(self, '__variants', self.variants.all())
        return iter(getattr(self, '__variants'))

    def __repr__(self):
        class_ = type(self)
        return '<%s.%s(pk=%r, name=%r)>' % (class_.__module__, class_.__name__,
                                            self.pk, self.name)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('product:details',
                       kwargs={
                           'slug': self.get_slug(),
                           'product_id': self.id
                       })

    def get_slug(self):
        return slugify(smart_text(unidecode(self.name)))

    def is_in_stock(self):
        return any(variant.is_in_stock() for variant in self)

    def is_available(self):
        today = datetime.date.today()
        return self.available_on is None or self.available_on <= today

    def get_first_image(self):
        first_image = self.images.first()
        return first_image.image if first_image else None

    def get_price_range(self, discounts=None, taxes=None):
        if self.variants.exists():
            prices = [
                variant.get_price(discounts=discounts, taxes=taxes)
                for variant in self
            ]
            return TaxedMoneyRange(min(prices), max(prices))
        price = calculate_discounted_price(self, self.price, discounts)
        if not self.charge_taxes:
            taxes = None
        tax_rate = self.tax_rate or self.product_type.tax_rate
        price = apply_tax_to_price(taxes, tax_rate, price)
        return TaxedMoneyRange(start=price, stop=price)
示例#16
0
class GiftCard(ModelWithMetadata):
    code = models.CharField(max_length=16, unique=True, db_index=True)
    is_active = models.BooleanField(default=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    used_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="gift_cards",
    )
    created_by_email = models.EmailField(null=True, blank=True)
    used_by_email = models.EmailField(null=True, blank=True)
    app = models.ForeignKey(
        App,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    expiry_date = models.DateField(null=True, blank=True)

    tags = models.ManyToManyField(GiftCardTag, "gift_cards")
    created = models.DateTimeField(auto_now_add=True)
    last_used_on = models.DateTimeField(null=True, blank=True)
    product = models.ForeignKey(
        "product.Product",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="gift_cards",
    )
    fulfillment_line = models.ForeignKey(
        "order.FulfillmentLine",
        related_name="gift_cards",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=os.environ.get("DEFAULT_CURRENCY", "USD"),
    )

    initial_balance_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    initial_balance = MoneyField(amount_field="initial_balance_amount",
                                 currency_field="currency")

    current_balance_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    current_balance = MoneyField(amount_field="current_balance_amount",
                                 currency_field="currency")

    objects = models.Manager.from_queryset(GiftCardQueryset)()

    class Meta:
        ordering = ("code", )
        permissions = ((GiftcardPermissions.MANAGE_GIFT_CARD.codename,
                        "Manage gift cards."), )

    @property
    def display_code(self):
        return self.code[-4:]
示例#17
0
class Order(models.Model):
    created = models.DateTimeField(
        default=now, editable=False)
    last_status_change = models.DateTimeField(
        default=now, editable=False)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, related_name='orders',
        on_delete=models.SET_NULL)
    language_code = models.CharField(
        max_length=35, default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(
        max_length=36, blank=True, editable=False)
    billing_address = models.ForeignKey(
        Address, related_name='+', editable=False,
        on_delete=models.PROTECT)
    shipping_address = models.ForeignKey(
        Address, related_name='+', editable=False, null=True,
        on_delete=models.PROTECT)
    user_email = models.EmailField(
        blank=True, default='', editable=False)
    shipping_price_net = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        default=0, editable=False)
    shipping_price_gross = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        default=0, editable=False)
    shipping_price = TaxedMoneyField(
        net_field='shipping_price_net', gross_field='shipping_price_gross')
    token = models.CharField(max_length=36, unique=True)
    total_net = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        blank=True, null=True)
    total_gross = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        blank=True, null=True)
    total = TaxedMoneyField(net_field='total_net', gross_field='total_gross')
    voucher = models.ForeignKey(
        Voucher, null=True, blank=True, related_name='+', on_delete=models.SET_NULL)
    discount_amount = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        blank=True, null=True)
    discount_name = models.CharField(max_length=255, default='', blank=True)
    paytm_paid = models.BooleanField(default=False)

    objects = OrderQuerySet.as_manager()

    class Meta:
        ordering = ('-last_status_change',)
        permissions = (
            ('view_order',
             pgettext_lazy('Permission description', 'Can view orders')),
            ('edit_order',
             pgettext_lazy('Permission description', 'Can edit orders')))

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def get_lines(self):
        return OrderLine.objects.filter(delivery_group__order=self)

    def is_fully_paid(self):
        total_paid = sum(
            [
                payment.get_total_price() for payment in
                self.payments.filter(status=PaymentStatus.CONFIRMED)],
            TaxedMoney(
                net=Money(0, currency=settings.DEFAULT_CURRENCY),
                gross=Money(0, currency=settings.DEFAULT_CURRENCY)))
        return total_paid.gross >= self.total.gross

    def get_user_current_email(self):
        return self.user.email if self.user else self.user_email

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __iter__(self):
        return iter(self.groups.all())

    def __repr__(self):
        return '<Order #%r>' % (self.id,)

    def __str__(self):
        return '#%d' % (self.id,)

    def get_absolute_url(self):
        return reverse('order:details', kwargs={'token': self.token})

    def get_last_payment_status(self):
        last_payment = self.payments.last()
        if last_payment:
            return last_payment.status
        return None

    def get_last_payment_status_display(self):
        last_payment = self.payments.last()
        if last_payment:
            return last_payment.get_status_display()
        return None

    def is_pre_authorized(self):
        return self.payments.filter(status=PaymentStatus.PREAUTH).exists()

    def is_shipping_required(self):
        return any(group.is_shipping_required() for group in self.groups.all())

    @property
    def is_past_due(self):
    	  if self.paytm_paid:
    	  		return False
    	  return now().date() > self.created.date()
    
    @property
    def status(self):
        """Order status deduced from shipment groups."""
        statuses = set([group.status for group in self.groups.all()])
        return (
            OrderStatus.OPEN if GroupStatus.NEW in statuses
            else OrderStatus.CLOSED)

    @property
    def is_open(self):
        return self.status == OrderStatus.OPEN

    def get_status_display(self):
        """Order status display text."""
        return dict(OrderStatus.CHOICES)[self.status]

    def get_subtotal(self):
        subtotal_iterator = (line.get_total() for line in self.get_lines())
        return sum(subtotal_iterator, ZERO_TAXED_MONEY)

    def can_cancel(self):
        return self.status == OrderStatus.OPEN
示例#18
0
class OrderLine(models.Model):
    order = models.ForeignKey(Order,
                              related_name="lines",
                              editable=False,
                              on_delete=models.CASCADE)
    variant = models.ForeignKey(
        "product.ProductVariant",
        related_name="order_lines",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    # max_length is as produced by ProductVariant's display_product method
    product_name = models.CharField(max_length=386)
    translated_product_name = models.CharField(max_length=386,
                                               default="",
                                               blank=True)
    product_sku = models.CharField(max_length=32)
    is_shipping_required = models.BooleanField()
    quantity = models.IntegerField(validators=[MinValueValidator(1)])
    quantity_fulfilled = models.IntegerField(validators=[MinValueValidator(0)],
                                             default=0)

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )

    unit_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    unit_price_net = MoneyField(amount_field="unit_price_net_amount",
                                currency_field="currency")

    unit_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    unit_price_gross = MoneyField(amount_field="unit_price_gross_amount",
                                  currency_field="currency")

    unit_price = TaxedMoneyField(
        net_amount_field="unit_price_net_amount",
        gross_amount_field="unit_price_gross_amount",
        currency="currency",
    )

    tax_rate = models.DecimalField(max_digits=5,
                                   decimal_places=2,
                                   default=Decimal("0.0"))

    objects = OrderLineQueryset.as_manager()

    class Meta:
        ordering = ("pk", )

    def __str__(self):
        return self.product_name

    def get_total(self):
        return self.unit_price * self.quantity

    @property
    def quantity_unfulfilled(self):
        return self.quantity - self.quantity_fulfilled

    @property
    def is_digital(self) -> bool:
        """Check if a variant is digital and contains digital content."""
        is_digital = self.variant.is_digital()
        has_digital = hasattr(self.variant, "digital_content")
        return is_digital and has_digital
示例#19
0
class Campaign(PublishableModel, TimeStampMixin):
    campaign_type = models.ForeignKey(CampaignType,
                                      related_name="campaigns",
                                      on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    slug = models.SlugField(max_length=255, unique=True)
    description = models.TextField(blank=True)
    story = models.TextField(blank=True)

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )

    total_investment_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    total_investment = MoneyField(amount_field="total_investment_amount",
                                  currency_field="currency")

    investment_raised_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    investment_raised = MoneyField(amount_field="investment_raised_amount",
                                   currency_field="currency")

    mininum_allowed_investment_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    mininum_allowed_investment = MoneyField(
        amount_field="mininum_allowed_investment_amount",
        currency_field="currency")

    location = models.OneToOneField(Address,
                                    related_name="location",
                                    on_delete=models.CASCADE,
                                    blank=True,
                                    null=True)

    estimated_investment_date = models.DateTimeField(blank=False)

    posted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="campaigns_owened",
        on_delete=models.SET_NULL,
    )

    investors = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                       blank=True,
                                       related_name="campaigns_invested_in")

    status = models.CharField(max_length=32,
                              default=CampaignStatus.UNFULFILLED,
                              choices=CampaignStatus.CHOICES)

    class Meta:
        app_label = "campaign"
        ordering = (
            "name",
            "total_investment_amount",
        )
        permissions = ((CampaignPermissions.MANAGE_CAMPAIGNS.codename,
                        "Manage campaigns."), )

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else None

    def get_first_image_url(self):
        images = list(self.images.all())
        return images[
            0].image.url if images else "/static/assets/img/placeholder800x470.png"

    def get_first_image_alt(self):
        images = list(self.images.all())
        return images[0].alt if images else "placeholder image"

    def get_campaign_progress(self):
        return round(((self.investment_raised_amount) /
                      (self.total_investment_amount)) * 100, 1)

    def investment_raised_amount_formatted(self):
        return round(self.investment_raised_amount, 0)

    def total_investment_amount_formatted(self):
        return round(self.total_investment_amount, 0)

    def get_absolute_url(self):
        return reverse('campaign-detail', args=[self.id, self.slug])

    def __repr__(self) -> str:
        class_ = type(self)
        return "<%s.%s(pk=%r, name=%r)>" % (
            class_.__module__,
            class_.__name__,
            self.pk,
            self.name,
        )

    def __str__(self) -> str:
        return self.name
示例#20
0
class Cart(models.Model):
    """A shopping cart."""

    status = models.CharField(max_length=32,
                              choices=CartStatus.CHOICES,
                              default=CartStatus.OPEN)
    created = models.DateTimeField(auto_now_add=True)
    last_status_change = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             blank=True,
                             null=True,
                             related_name='carts',
                             on_delete=models.CASCADE)
    email = models.EmailField(blank=True, null=True)
    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    voucher = models.ForeignKey('discount.Voucher',
                                null=True,
                                related_name='+',
                                on_delete=models.SET_NULL)
    checkout_data = JSONField(null=True, editable=False)
    total = MoneyField(currency=settings.DEFAULT_CURRENCY,
                       max_digits=12,
                       decimal_places=2,
                       default=0)
    quantity = models.PositiveIntegerField(default=0)

    objects = CartQueryset.as_manager()

    class Meta:
        ordering = ('-last_status_change', )

    def __init__(self, *args, **kwargs):
        self.discounts = kwargs.pop('discounts', None)
        super().__init__(*args, **kwargs)

    def update_quantity(self):
        """Recalculate cart quantity based on lines."""
        total_lines = self.count()['total_quantity']
        if not total_lines:
            total_lines = 0
        self.quantity = total_lines
        self.save(update_fields=['quantity'])

    def change_status(self, status):
        """Change cart status."""
        # FIXME: investigate replacing with django-fsm transitions
        if status not in dict(CartStatus.CHOICES):
            raise ValueError('Not expected status')
        if status != self.status:
            self.status = status
            self.last_status_change = now()
            self.save()

    def change_user(self, user):
        """Assign cart to a user.

        If the user already has an open cart assigned, cancel it.
        """
        open_cart = find_open_cart_for_user(user)
        if open_cart is not None:
            open_cart.change_status(status=CartStatus.CANCELED)
        self.user = user
        self.save(update_fields=['user'])

    def is_shipping_required(self):
        """Return `True` if any of the lines requires shipping."""
        return any(line.is_shipping_required() for line in self.lines.all())

    def __repr__(self):
        return 'Cart(quantity=%s)' % (self.quantity, )

    def __len__(self):
        return self.lines.count()

    def get_total(self, discounts=None):
        """Return the total cost of the cart prior to shipping."""
        subtotals = [line.get_total(discounts) for line in self.lines.all()]
        if not subtotals:
            raise AttributeError('Calling get_total() on an empty cart')
        return sum_prices(subtotals)

    def count(self):
        """Return the total quantity in cart."""
        lines = self.lines.all()
        return lines.aggregate(total_quantity=models.Sum('quantity'))

    def clear(self):
        """Remove the cart."""
        self.delete()

    def create_line(self, variant, quantity, data):
        """Create a cart line for given variant, quantity and optional data.

        The `data` parameter may be used to differentiate between items with
        different customization options.
        """
        return self.lines.create(variant=variant,
                                 quantity=quantity,
                                 data=data or {})

    def get_line(self, variant, data=None):
        """Return a line matching the given variant and data if any."""
        all_lines = self.lines.all()
        if data is None:
            data = {}
        line = [
            line for line in all_lines
            if line.variant_id == variant.id and line.data == data
        ]
        if line:
            return line[0]
        return None

    def add(self,
            variant,
            quantity=1,
            data=None,
            replace=False,
            check_quantity=True):
        """Add a product vartiant to cart.

        The `data` parameter may be used to differentiate between items with
        different customization options.

        If `replace` is truthy then any previous quantity is discarded instead
        of added to.
        """
        cart_line, dummy_created = self.lines.get_or_create(variant=variant,
                                                            defaults={
                                                                'quantity': 0,
                                                                'data': data
                                                                or {}
                                                            })
        if replace:
            new_quantity = quantity
        else:
            new_quantity = cart_line.quantity + quantity

        if new_quantity < 0:
            raise ValueError('%r is not a valid quantity (results in %r)' %
                             (quantity, new_quantity))

        if check_quantity:
            variant.check_quantity(new_quantity)

        cart_line.quantity = new_quantity

        if not cart_line.quantity:
            cart_line.delete()
        else:
            cart_line.save(update_fields=['quantity'])
        self.update_quantity()

    def partition(self):
        """Split the cart into a list of groups for shipping."""
        grouper = (lambda p: 'physical'
                   if p.is_shipping_required() else 'digital')
        subject = sorted(self.lines.all(), key=grouper)
        for _, lines in groupby(subject, key=grouper):
            yield ProductGroup(lines)
示例#21
0
文件: models.py 项目: vonq/saleor
class Order(ModelWithMetadata):
    created = models.DateTimeField(default=now, editable=False)
    updated_at = models.DateTimeField(auto_now=True,
                                      editable=False,
                                      db_index=True)
    status = models.CharField(max_length=32,
                              default=OrderStatus.UNFULFILLED,
                              choices=OrderStatus.CHOICES)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    language_code = models.CharField(max_length=35,
                                     choices=settings.LANGUAGES,
                                     default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(max_length=36,
                                          blank=True,
                                          editable=False)
    billing_address = models.ForeignKey(
        "account.Address",
        related_name="+",
        editable=False,
        null=True,
        on_delete=models.SET_NULL,
    )
    shipping_address = models.ForeignKey(
        "account.Address",
        related_name="+",
        editable=False,
        null=True,
        on_delete=models.SET_NULL,
    )
    user_email = models.EmailField(blank=True, default="")
    original = models.ForeignKey("self",
                                 null=True,
                                 blank=True,
                                 on_delete=models.SET_NULL)
    origin = models.CharField(max_length=32, choices=OrderOrigin.CHOICES)

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, )

    shipping_method = models.ForeignKey(
        ShippingMethod,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    collection_point = models.ForeignKey(
        "warehouse.Warehouse",
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    shipping_method_name = models.CharField(max_length=255,
                                            null=True,
                                            default=None,
                                            blank=True,
                                            editable=False)
    collection_point_name = models.CharField(max_length=255,
                                             null=True,
                                             default=None,
                                             blank=True,
                                             editable=False)

    channel = models.ForeignKey(
        Channel,
        related_name="orders",
        on_delete=models.PROTECT,
    )
    shipping_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_net = MoneyField(amount_field="shipping_price_net_amount",
                                    currency_field="currency")

    shipping_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_gross = MoneyField(
        amount_field="shipping_price_gross_amount", currency_field="currency")

    shipping_price = TaxedMoneyField(
        net_amount_field="shipping_price_net_amount",
        gross_amount_field="shipping_price_gross_amount",
        currency_field="currency",
    )
    shipping_tax_rate = models.DecimalField(max_digits=5,
                                            decimal_places=4,
                                            default=Decimal("0.0"))

    token = models.CharField(max_length=36, unique=True, blank=True)
    # Token of a checkout instance that this order was created from
    checkout_token = models.CharField(max_length=36, blank=True)

    total_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    undiscounted_total_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )

    total_net = MoneyField(amount_field="total_net_amount",
                           currency_field="currency")
    undiscounted_total_net = MoneyField(
        amount_field="undiscounted_total_net_amount",
        currency_field="currency")

    total_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    undiscounted_total_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )

    total_gross = MoneyField(amount_field="total_gross_amount",
                             currency_field="currency")
    undiscounted_total_gross = MoneyField(
        amount_field="undiscounted_total_gross_amount",
        currency_field="currency")

    total = TaxedMoneyField(
        net_amount_field="total_net_amount",
        gross_amount_field="total_gross_amount",
        currency_field="currency",
    )
    undiscounted_total = TaxedMoneyField(
        net_amount_field="undiscounted_total_net_amount",
        gross_amount_field="undiscounted_total_gross_amount",
        currency_field="currency",
    )

    total_paid_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    total_paid = MoneyField(amount_field="total_paid_amount",
                            currency_field="currency")

    voucher = models.ForeignKey(Voucher,
                                blank=True,
                                null=True,
                                related_name="+",
                                on_delete=models.SET_NULL)
    gift_cards = models.ManyToManyField(GiftCard,
                                        blank=True,
                                        related_name="orders")

    display_gross_prices = models.BooleanField(default=True)
    customer_note = models.TextField(blank=True, default="")
    weight = MeasurementField(
        measurement=Weight,
        unit_choices=WeightUnits.CHOICES,  # type: ignore
        default=zero_weight,
    )
    redirect_url = models.URLField(blank=True, null=True)
    search_document = models.TextField(blank=True, default="")

    objects = models.Manager.from_queryset(OrderQueryset)()

    class Meta:
        ordering = ("-pk", )
        permissions = ((OrderPermissions.MANAGE_ORDERS.codename,
                        "Manage orders."), )
        indexes = [
            *ModelWithMetadata.Meta.indexes,
            GinIndex(
                name="order_search_gin",
                # `opclasses` and `fields` should be the same length
                fields=["search_document"],
                opclasses=["gin_trgm_ops"],
            ),
            GinIndex(
                name="order_email_search_gin",
                # `opclasses` and `fields` should be the same length
                fields=["user_email"],
                opclasses=["gin_trgm_ops"],
            ),
        ]

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def is_fully_paid(self):
        return self.total_paid >= self.total.gross

    def is_partly_paid(self):
        return self.total_paid_amount > 0

    def get_customer_email(self):
        return self.user.email if self.user else self.user_email

    def update_total_paid(self):
        self.total_paid_amount = (sum(
            self.payments.values_list("captured_amount", flat=True)) or 0)
        self.save(update_fields=["total_paid_amount", "updated_at"])

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __repr__(self):
        return "<Order #%r>" % (self.id, )

    def __str__(self):
        return "#%d" % (self.id, )

    def get_last_payment(self):
        # Skipping a partial payment is a temporary workaround for storing a basic data
        # about partial payment from Adyen plugin. This is something that will removed
        # in 3.1 by introducing a partial payments feature.
        payments = [
            payment for payment in self.payments.all() if not payment.partial
        ]
        return max(payments, default=None, key=attrgetter("pk"))

    def is_pre_authorized(self):
        return (self.payments.filter(
            is_active=True,
            transactions__kind=TransactionKind.AUTH,
            transactions__action_required=False,
        ).filter(transactions__is_success=True).exists())

    def is_captured(self):
        return (self.payments.filter(
            is_active=True,
            transactions__kind=TransactionKind.CAPTURE,
            transactions__action_required=False,
        ).filter(transactions__is_success=True).exists())

    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self.lines.all())

    def get_subtotal(self):
        return get_subtotal(self.lines.all(), self.currency)

    def get_total_quantity(self):
        return sum([line.quantity for line in self.lines.all()])

    def is_draft(self):
        return self.status == OrderStatus.DRAFT

    def is_unconfirmed(self):
        return self.status == OrderStatus.UNCONFIRMED

    def is_open(self):
        statuses = {OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED}
        return self.status in statuses

    def can_cancel(self):
        statuses_allowed_to_cancel = [
            FulfillmentStatus.CANCELED,
            FulfillmentStatus.REFUNDED,
            FulfillmentStatus.REPLACED,
            FulfillmentStatus.REFUNDED_AND_RETURNED,
            FulfillmentStatus.RETURNED,
        ]
        return (not self.fulfillments.exclude(
            status__in=statuses_allowed_to_cancel).exists()
                ) and self.status not in {
                    OrderStatus.CANCELED, OrderStatus.DRAFT
                }

    def can_capture(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_capture() and order_status_ok

    def can_void(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_void()

    def can_refund(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_refund()

    def can_mark_as_paid(self, payments=None):
        if not payments:
            payments = self.payments.all()
        return len(payments) == 0

    @property
    def total_authorized(self):
        return get_total_authorized(self.payments.all(), self.currency)

    @property
    def total_captured(self):
        return self.total_paid

    @property
    def total_balance(self):
        return self.total_captured - self.total.gross

    def get_total_weight(self, *_args):
        return self.weight
示例#22
0
class Checkout(models.Model):
    """A shopping checkout."""

    created = models.DateTimeField(auto_now_add=True)
    last_change = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="checkouts",
        on_delete=models.CASCADE,
    )
    email = models.EmailField()
    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    quantity = models.PositiveIntegerField(default=0)
    billing_address = models.ForeignKey(Address,
                                        related_name="+",
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name="+",
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    shipping_method = models.ForeignKey(
        ShippingMethod,
        blank=True,
        null=True,
        related_name="checkouts",
        on_delete=models.SET_NULL,
    )
    note = models.TextField(blank=True, default="")
    discount_amount = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=zero_money,
    )
    discount_name = models.CharField(max_length=255, blank=True, null=True)
    translated_discount_name = models.CharField(max_length=255,
                                                blank=True,
                                                null=True)
    voucher_code = models.CharField(max_length=12, blank=True, null=True)
    gift_cards = models.ManyToManyField(GiftCard,
                                        blank=True,
                                        related_name="checkouts")

    objects = CheckoutQueryset.as_manager()

    class Meta:
        ordering = ("-last_change", )

    def __repr__(self):
        return "Checkout(quantity=%s)" % (self.quantity, )

    def __iter__(self):
        return iter(self.lines.all())

    def __len__(self):
        return self.lines.count()

    def is_shipping_required(self):
        """Return `True` if any of the lines requires shipping."""
        return any(line.is_shipping_required() for line in self)

    def get_shipping_price(self):
        return (self.shipping_method.get_total() if self.shipping_method
                and self.is_shipping_required() else zero_money())

    def get_subtotal(self, discounts=None):
        """Return the total cost of the checkout prior to shipping."""
        subtotals = (line.get_total(discounts) for line in self)
        return sum(subtotals, zero_money(currency=settings.DEFAULT_CURRENCY))

    def get_total(self, discounts=None):
        """Return the total cost of the checkout."""
        total = (self.get_subtotal(discounts) + self.get_shipping_price() -
                 self.discount_amount)
        return max(total, zero_money(total.currency))

    def get_total_gift_cards_balance(self):
        """Return the total balance of the gift cards assigned to the checkout."""
        balance = self.gift_cards.aggregate(
            models.Sum("current_balance"))["current_balance__sum"]
        return balance or zero_money(currency=settings.DEFAULT_CURRENCY)

    def get_total_weight(self):
        # Cannot use `sum` as it parses an empty Weight to an int
        weights = zero_weight()
        for line in self:
            weights += line.variant.get_weight() * line.quantity
        return weights

    def get_line(self, variant):
        """Return a line matching the given variant and data if any."""
        matching_lines = (line for line in self
                          if line.variant.pk == variant.pk)
        return next(matching_lines, None)

    def get_last_active_payment(self):
        payments = [
            payment for payment in self.payments.all() if payment.is_active
        ]
        return max(payments, default=None, key=attrgetter("pk"))
示例#23
0
class Checkout(ModelWithMetadata):
    """A shopping checkout."""

    created = models.DateTimeField(auto_now_add=True)
    last_change = models.DateTimeField(auto_now=True)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="checkouts",
        on_delete=models.CASCADE,
    )
    email = models.EmailField()
    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    quantity = models.PositiveIntegerField(default=0)
    channel = models.ForeignKey(
        Channel,
        related_name="checkouts",
        on_delete=models.PROTECT,
    )
    billing_address = models.ForeignKey(Address,
                                        related_name="+",
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name="+",
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    shipping_method = models.ForeignKey(
        ShippingMethod,
        blank=True,
        null=True,
        related_name="checkouts",
        on_delete=models.SET_NULL,
    )
    note = models.TextField(blank=True, default="")

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, )
    country = CountryField(default=get_default_country)

    discount_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    discount = MoneyField(amount_field="discount_amount",
                          currency_field="currency")
    discount_name = models.CharField(max_length=255, blank=True, null=True)

    translated_discount_name = models.CharField(max_length=255,
                                                blank=True,
                                                null=True)
    voucher_code = models.CharField(max_length=12, blank=True, null=True)
    gift_cards = models.ManyToManyField(GiftCard,
                                        blank=True,
                                        related_name="checkouts")

    redirect_url = models.URLField(blank=True, null=True)
    tracking_code = models.CharField(max_length=255, blank=True, null=True)

    class Meta:
        ordering = ("-last_change", "pk")
        permissions = ((CheckoutPermissions.MANAGE_CHECKOUTS.codename,
                        "Manage checkouts"), )

    def __repr__(self):
        return "Checkout(quantity=%s)" % (self.quantity, )

    def __iter__(self):
        return iter(self.lines.all())

    def get_customer_email(self) -> str:
        return self.user.email if self.user else self.email

    def is_shipping_required(self) -> bool:
        """Return `True` if any of the lines requires shipping."""
        return any(line.is_shipping_required() for line in self)

    def get_total_gift_cards_balance(self) -> Money:
        """Return the total balance of the gift cards assigned to the checkout."""
        balance = self.gift_cards.aggregate(
            models.Sum(
                "current_balance_amount"))["current_balance_amount__sum"]
        if balance is None:
            return zero_money(currency=self.currency)
        return Money(balance, self.currency)

    def get_total_weight(
            self,
            lines: Optional[Iterable["CheckoutLineInfo"]] = None) -> "Weight":
        # Cannot use `sum` as it parses an empty Weight to an int
        weights = zero_weight()
        # TODO: we should use new data structure for lines in order like in checkout
        if lines is None:
            for line in self:
                weights += line.variant.get_weight() * line.quantity
        else:
            for checkout_line_info in lines:
                line = checkout_line_info.line
                weights += line.variant.get_weight() * line.quantity
        return weights

    def get_line(self, variant: "ProductVariant") -> Optional["CheckoutLine"]:
        """Return a line matching the given variant and data if any."""
        matching_lines = (line for line in self
                          if line.variant.pk == variant.pk)
        return next(matching_lines, None)

    def get_last_active_payment(self) -> Optional["Payment"]:
        payments = [
            payment for payment in self.payments.all() if payment.is_active
        ]
        return max(payments, default=None, key=attrgetter("pk"))

    def set_country(self,
                    country_code: str,
                    commit: bool = False,
                    replace: bool = True):
        """Set country for checkout."""
        if not replace and self.country is not None:
            return
        self.country = Country(country_code)
        if commit:
            self.save(update_fields=["country"])

    def get_country(self):
        address = self.shipping_address or self.billing_address
        saved_country = self.country
        if address is None or not address.country:
            return saved_country.code

        country_code = address.country.code
        if not country_code == saved_country.code:
            self.set_country(country_code, commit=True)
        return country_code
示例#24
0
class ShippingMethod(models.Model):
    name = models.CharField(max_length=100)
    type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )
    price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    price = MoneyField(amount_field="price_amount", currency_field="currency")
    shipping_zone = models.ForeignKey(
        ShippingZone, related_name="shipping_methods", on_delete=models.CASCADE
    )

    minimum_order_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        blank=True,
        null=True,
    )
    minimum_order_price = MoneyField(
        amount_field="minimum_order_price_amount", currency_field="currency"
    )

    maximum_order_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    maximum_order_price = MoneyField(
        amount_field="maximum_order_price_amount", currency_field="currency"
    )

    minimum_order_weight = MeasurementField(
        measurement=Weight,
        unit_choices=WeightUnits.CHOICES,
        default=zero_weight,
        blank=True,
        null=True,
    )
    maximum_order_weight = MeasurementField(
        measurement=Weight, unit_choices=WeightUnits.CHOICES, blank=True, null=True
    )

    meta = JSONField(blank=True, default=dict, encoder=CustomJsonEncoder)

    objects = ShippingMethodQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        ordering = ("pk",)

    def __str__(self):
        return self.name

    def __repr__(self):
        if self.type == ShippingMethodType.PRICE_BASED:
            minimum = "%s%s" % (
                self.minimum_order_price.amount,
                self.minimum_order_price.currency,
            )
            max_price = self.maximum_order_price
            maximum = (
                "%s%s" % (max_price.amount, max_price.currency)
                if max_price
                else "no limit"
            )
            return "ShippingMethod(type=%s min=%s, max=%s)" % (
                self.type,
                minimum,
                maximum,
            )
        return "ShippingMethod(type=%s weight_range=(%s)" % (
            self.type,
            _get_weight_type_display(
                self.minimum_order_weight, self.maximum_order_weight
            ),
        )

    def get_total(self):
        return self.price
示例#25
0
class Order(ModelWithMetadata):
    created = models.DateTimeField(default=now, editable=False)
    status = models.CharField(max_length=32,
                              default=OrderStatus.UNFULFILLED,
                              choices=OrderStatus.CHOICES)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    language_code = models.CharField(max_length=35,
                                     choices=settings.LANGUAGES,
                                     default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(max_length=36,
                                          blank=True,
                                          editable=False)
    billing_address = models.ForeignKey(Address,
                                        related_name="+",
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name="+",
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    user_email = models.EmailField(blank=True, default="")

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, )

    shipping_method = models.ForeignKey(
        ShippingMethod,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    shipping_method_name = models.CharField(max_length=255,
                                            null=True,
                                            default=None,
                                            blank=True,
                                            editable=False)
    channel = models.ForeignKey(
        Channel,
        related_name="orders",
        on_delete=models.PROTECT,
    )
    shipping_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_net = MoneyField(amount_field="shipping_price_net_amount",
                                    currency_field="currency")

    shipping_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_gross = MoneyField(
        amount_field="shipping_price_gross_amount", currency_field="currency")

    shipping_price = TaxedMoneyField(
        net_amount_field="shipping_price_net_amount",
        gross_amount_field="shipping_price_gross_amount",
        currency_field="currency",
    )
    shipping_tax_rate = models.DecimalField(max_digits=5,
                                            decimal_places=4,
                                            default=Decimal("0.0"))

    token = models.CharField(max_length=36, unique=True, blank=True)
    # Token of a checkout instance that this order was created from
    checkout_token = models.CharField(max_length=36, blank=True)

    total_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    undiscounted_total_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )

    total_net = MoneyField(amount_field="total_net_amount",
                           currency_field="currency")
    undiscounted_total_net = MoneyField(
        amount_field="undiscounted_total_net_amount",
        currency_field="currency")

    total_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    undiscounted_total_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )

    total_gross = MoneyField(amount_field="total_gross_amount",
                             currency_field="currency")
    undiscounted_total_gross = MoneyField(
        amount_field="undiscounted_total_gross_amount",
        currency_field="currency")

    total = TaxedMoneyField(
        net_amount_field="total_net_amount",
        gross_amount_field="total_gross_amount",
        currency_field="currency",
    )
    undiscounted_total = TaxedMoneyField(
        net_amount_field="undiscounted_total_net_amount",
        gross_amount_field="undiscounted_total_gross_amount",
        currency_field="currency",
    )

    voucher = models.ForeignKey(Voucher,
                                blank=True,
                                null=True,
                                related_name="+",
                                on_delete=models.SET_NULL)
    gift_cards = models.ManyToManyField(GiftCard,
                                        blank=True,
                                        related_name="orders")

    display_gross_prices = models.BooleanField(default=True)
    customer_note = models.TextField(blank=True, default="")
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              default=zero_weight)
    redirect_url = models.URLField(blank=True, null=True)
    objects = OrderQueryset.as_manager()

    class Meta(ModelWithMetadata.Meta):
        ordering = ("-pk", )
        permissions = ((OrderPermissions.MANAGE_ORDERS.codename,
                        "Manage orders."), )

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def is_fully_paid(self):
        total_paid = self._total_paid()
        return total_paid.gross >= self.total.gross

    def is_partly_paid(self):
        total_paid = self._total_paid()
        return total_paid.gross.amount > 0

    def get_customer_email(self):
        return self.user.email if self.user else self.user_email

    def _total_paid(self):
        # Get total paid amount from partially charged,
        # fully charged and partially refunded payments
        payments = self.payments.filter(charge_status__in=[
            ChargeStatus.PARTIALLY_CHARGED,
            ChargeStatus.FULLY_CHARGED,
            ChargeStatus.PARTIALLY_REFUNDED,
        ])
        total_captured = [
            payment.get_captured_amount() for payment in payments
        ]
        total_paid = sum(total_captured,
                         zero_taxed_money(currency=self.currency))
        return total_paid

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __repr__(self):
        return "<Order #%r>" % (self.id, )

    def __str__(self):
        return "#%d" % (self.id, )

    def get_last_payment(self):
        return max(self.payments.all(), default=None, key=attrgetter("pk"))

    def is_pre_authorized(self):
        return (self.payments.filter(
            is_active=True,
            transactions__kind=TransactionKind.AUTH,
            transactions__action_required=False,
        ).filter(transactions__is_success=True).exists())

    def is_captured(self):
        return (self.payments.filter(
            is_active=True,
            transactions__kind=TransactionKind.CAPTURE,
            transactions__action_required=False,
        ).filter(transactions__is_success=True).exists())

    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self.lines.all())

    def get_subtotal(self):
        return get_subtotal(self.lines.all(), self.currency)

    def get_total_quantity(self):
        return sum([line.quantity for line in self.lines.all()])

    def is_draft(self):
        return self.status == OrderStatus.DRAFT

    def is_unconfirmed(self):
        return self.status == OrderStatus.UNCONFIRMED

    def is_open(self):
        statuses = {OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED}
        return self.status in statuses

    def can_cancel(self):
        statuses_allowed_to_cancel = [
            FulfillmentStatus.CANCELED,
            FulfillmentStatus.REFUNDED,
            FulfillmentStatus.REPLACED,
            FulfillmentStatus.REFUNDED_AND_RETURNED,
            FulfillmentStatus.RETURNED,
        ]
        return (not self.fulfillments.exclude(
            status__in=statuses_allowed_to_cancel).exists()
                ) and self.status not in {
                    OrderStatus.CANCELED, OrderStatus.DRAFT
                }

    def can_capture(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_capture() and order_status_ok

    def can_void(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_void()

    def can_refund(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_refund()

    def can_mark_as_paid(self, payments=None):
        if not payments:
            payments = self.payments.all()
        return len(payments) == 0

    @property
    def total_authorized(self):
        return get_total_authorized(self.payments.all(), self.currency)

    @property
    def total_captured(self):
        return get_total_captured(self.payments.all(), self.currency)

    @property
    def total_balance(self):
        return self.total_captured - self.total.gross

    def get_total_weight(self, *_args):
        return self.weight
示例#26
0
class Voucher(models.Model):
    type = models.CharField(max_length=20,
                            choices=VoucherType.CHOICES,
                            default=VoucherType.ENTIRE_ORDER)
    name = models.CharField(max_length=255, null=True, blank=True)
    code = models.CharField(max_length=12, unique=True, db_index=True)
    usage_limit = models.PositiveIntegerField(null=True, blank=True)
    used = models.PositiveIntegerField(default=0, editable=False)
    start_date = models.DateTimeField(default=timezone.now)
    end_date = models.DateTimeField(null=True, blank=True)
    # this field indicates if discount should be applied per order or
    # individually to every item
    apply_once_per_order = models.BooleanField(default=False)
    apply_once_per_customer = models.BooleanField(default=False)
    discount_value_type = models.CharField(
        max_length=10,
        choices=DiscountValueType.CHOICES,
        default=DiscountValueType.FIXED,
    )
    discount_value = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    # not mandatory fields, usage depends on type
    countries = CountryField(multiple=True, blank=True)
    min_amount_spent = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        null=True,
        blank=True,
    )
    min_checkout_items_quantity = models.PositiveIntegerField(null=True,
                                                              blank=True)
    products = models.ManyToManyField("product.Product", blank=True)
    collections = models.ManyToManyField("product.Collection", blank=True)
    categories = models.ManyToManyField("product.Category", blank=True)

    objects = VoucherQueryset.as_manager()
    translated = TranslationProxy()

    def __str__(self):
        if self.name:
            return self.name
        discount = "%s %s" % (
            self.discount_value,
            self.get_discount_value_type_display(),
        )
        if self.type == VoucherType.SHIPPING:
            if self.is_free:
                return pgettext("Voucher type", "Free shipping")
            return pgettext("Voucher type", "%(discount)s off shipping") % {
                "discount": discount
            }
        if self.type == VoucherType.PRODUCT:
            products = len(self.products.all())
            if products:
                return pgettext(
                    "Voucher type",
                    "%(discount)s off %(product_num)d products") % {
                        "discount": discount,
                        "product_num": products
                    }
        if self.type == VoucherType.COLLECTION:
            collections = len(self.collections.all())
            if collections:
                return pgettext(
                    "Voucher type",
                    "%(discount)s off %(collections_num)d collections") % {
                        "discount": discount,
                        "collections_num": collections
                    }
        if self.type == VoucherType.CATEGORY:
            categories = len(self.categories.all())
            if categories:
                return pgettext(
                    "Voucher type",
                    "%(discount)s off %(categories_num)d categories") % {
                        "discount": discount,
                        "categories_num": categories
                    }
        return pgettext("Voucher type", "%(discount)s off") % {
            "discount": discount
        }

    @property
    def is_free(self):
        return (self.discount_value == Decimal(100)
                and self.discount_value_type == DiscountValueType.PERCENTAGE)

    def get_discount(self):
        if self.discount_value_type == DiscountValueType.FIXED:
            discount_amount = Money(self.discount_value,
                                    settings.DEFAULT_CURRENCY)
            return partial(fixed_discount, discount=discount_amount)
        if self.discount_value_type == DiscountValueType.PERCENTAGE:
            return partial(percentage_discount, percentage=self.discount_value)
        raise NotImplementedError("Unknown discount type")

    def get_discount_amount_for(self, price):
        discount = self.get_discount()
        after_discount = discount(price)
        if after_discount.amount < 0:
            return price
        return price - after_discount

    def validate_min_amount_spent(self, value):
        min_amount_spent = self.min_amount_spent
        if min_amount_spent and value < min_amount_spent:
            msg = pgettext(
                "Voucher not applicable",
                "This offer is only valid for orders over %(amount)s.",
            )
            raise NotApplicable(
                msg % {"amount": amount(min_amount_spent)},
                min_amount_spent=min_amount_spent,
            )

    def validate_min_checkout_items_quantity(self, quantity):
        min_checkout_items_quantity = self.min_checkout_items_quantity
        if min_checkout_items_quantity and min_checkout_items_quantity > quantity:
            msg = pgettext(
                "Voucher not applicable",
                ("This offer is only valid for orders with a minimum of "
                 "%(min_checkout_items_quantity)d quantity."),
            )
            raise NotApplicable(
                msg %
                {"min_checkout_items_quantity": min_checkout_items_quantity},
                min_checkout_items_quantity=min_checkout_items_quantity,
            )

    def validate_once_per_customer(self, customer_email):
        voucher_customer = VoucherCustomer.objects.filter(
            voucher=self, customer_email=customer_email)
        if voucher_customer:
            msg = pgettext("Voucher not applicable",
                           "This offer is valid only once per customer.")
            raise NotApplicable(msg)
示例#27
0
class ProductVariant(ModelWithMetadata):
    sku = models.CharField(max_length=255, unique=True)
    name = models.CharField(max_length=255, blank=True)
    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
        blank=True,
        null=True,
    )
    price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    price = MoneyField(amount_field="price_amount", currency_field="currency")
    product = models.ForeignKey(Product,
                                related_name="variants",
                                on_delete=models.CASCADE)
    images = models.ManyToManyField("ProductImage", through="VariantImage")
    track_inventory = models.BooleanField(default=True)

    cost_price_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    cost_price = MoneyField(amount_field="cost_price_amount",
                            currency_field="currency")
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)

    objects = ProductVariantQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        ordering = ("sku", )
        app_label = "product"

    def __str__(self) -> str:
        return self.name or self.sku

    @property
    def is_visible(self) -> bool:
        return self.product.is_visible

    def get_price(
            self,
            discounts: Optional[Iterable[DiscountInfo]] = None) -> "Money":
        return calculate_discounted_price(
            product=self.product,
            price=self.price,
            collections=self.product.collections.all(),
            discounts=discounts,
        )

    def get_weight(self):
        return self.weight or self.product.weight or self.product.product_type.weight

    def is_shipping_required(self) -> bool:
        return self.product.product_type.is_shipping_required

    def is_digital(self) -> bool:
        is_digital = self.product.product_type.is_digital
        return not self.is_shipping_required() and is_digital

    def display_product(self, translated: bool = False) -> str:
        if translated:
            product = self.product.translated
            variant_display = str(self.translated)
        else:
            variant_display = str(self)
            product = self.product
        product_display = (f"{product} ({variant_display})"
                           if variant_display else str(product))
        return smart_text(product_display)

    def get_first_image(self) -> "ProductImage":
        images = list(self.images.all())
        return images[0] if images else self.product.get_first_image()
示例#28
0
文件: models.py 项目: Chhunneng/VKBA
class ShippingMethod(models.Model):
    name = models.CharField(max_length=100)
    type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
    price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    shipping_zone = models.ForeignKey(ShippingZone,
                                      related_name="shipping_methods",
                                      on_delete=models.CASCADE)
    minimum_order_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        blank=True,
        null=True,
    )
    maximum_order_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    minimum_order_weight = MeasurementField(
        measurement=Weight,
        unit_choices=WeightUnits.CHOICES,
        default=zero_weight,
        blank=True,
        null=True,
    )
    maximum_order_weight = MeasurementField(measurement=Weight,
                                            unit_choices=WeightUnits.CHOICES,
                                            blank=True,
                                            null=True)
    # <ADD
    percentage = models.DecimalField(
        null=True,
        blank=True,
        default=None,
        max_digits=5,
        decimal_places=2,
        validators=[MinValueValidator(0),
                    MaxValueValidator(100)])
    # ADD>
    objects = ShippingMethodQueryset.as_manager()
    translated = TranslationProxy()

    class Meta:
        ordering = ("pk", )

    def __str__(self):
        return self.name

    def __repr__(self):
        if self.type == ShippingMethodType.PRICE_BASED:
            minimum = "%s%s" % (
                self.minimum_order_price.amount,
                self.minimum_order_price.currency,
            )
            max_price = self.maximum_order_price
            maximum = ("%s%s" % (max_price.amount, max_price.currency)
                       if max_price else "no limit")
            return "ShippingMethod(type=%s min=%s, max=%s)" % (
                self.type,
                minimum,
                maximum,
            )
        # <ADD
        elif self.type == ShippingMethodType.PERCENTAGE_BASED:
            minimum = '%s%s' % (self.minimum_order_price.amount,
                                self.minimum_order_price.currency)
            max_price = self.maximum_order_price
            maximum = ('%s%s' % (max_price.amount, max_price.currency)
                       if max_price else 'no limit')
            return 'ShippingMethod(type=%s min=%s, max=%s)' % (
                self.type, minimum, maximum)
        # ADD>
        return "ShippingMethod(type=%s weight_range=(%s)" % (
            self.type,
            get_weight_type_display(self.minimum_order_weight,
                                    self.maximum_order_weight),
        )

    def get_total(self, taxes=None):
        return get_taxed_shipping_price(self.price, taxes)

    def get_ajax_label(self):
        price_html = format_money(self.price)
        label = mark_safe("%s %s" % (self, price_html))
        return label

    def get_type_display(self):
        # <ADD or self.type == ShippingMethodType.PERCENTAGE_BASED in if
        if self.type == ShippingMethodType.PRICE_BASED or self.type == ShippingMethodType.PERCENTAGE_BASED:
            return get_price_type_display(self.minimum_order_price,
                                          self.maximum_order_price)
        return get_weight_type_display(self.minimum_order_weight,
                                       self.maximum_order_weight)
示例#29
0
class ProductVariant(ModelWithMetadata):
    sku = models.CharField(max_length=32, unique=True)
    name = models.CharField(max_length=255, blank=True)
    price_override = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    product = models.ForeignKey(Product,
                                related_name="variants",
                                on_delete=models.CASCADE)
    attributes = FilterableJSONBField(default=dict,
                                      blank=True,
                                      validators=[validate_attribute_json])
    images = models.ManyToManyField("ProductImage", through="VariantImage")
    track_inventory = models.BooleanField(default=True)
    quantity = models.IntegerField(validators=[MinValueValidator(0)],
                                   default=Decimal(1))
    quantity_allocated = models.IntegerField(validators=[MinValueValidator(0)],
                                             default=Decimal(0))
    cost_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        blank=True,
        null=True,
    )
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              blank=True,
                              null=True)
    translated = TranslationProxy()

    class Meta:
        app_label = "product"

    def __str__(self):
        return self.name or self.sku

    @property
    def quantity_available(self):
        return max(self.quantity - self.quantity_allocated, 0)

    @property
    def is_visible(self):
        return self.product.is_visible

    @property
    def is_available(self):
        return self.product.is_available

    def check_quantity(self, quantity):
        """Check if there is at least the given quantity in stock.

        If stock handling is disabled, it simply run no check.
        """
        if self.track_inventory and quantity > self.quantity_available:
            raise InsufficientStock(self)

    @property
    def base_price(self):
        return (self.price_override
                if self.price_override is not None else self.product.price)

    def get_price(self, discounts: Iterable[DiscountInfo] = None):
        return calculate_discounted_price(self.product, self.base_price,
                                          discounts)

    def get_weight(self):
        return self.weight or self.product.weight or self.product.product_type.weight

    def get_absolute_url(self):
        slug = self.product.get_slug()
        product_id = self.product.id
        return reverse("product:details",
                       kwargs={
                           "slug": slug,
                           "product_id": product_id
                       })

    def is_shipping_required(self):
        return self.product.product_type.is_shipping_required

    def is_digital(self):
        is_digital = self.product.product_type.is_digital
        return not self.is_shipping_required() and is_digital

    def is_in_stock(self):
        return self.quantity_available > 0

    def display_product(self, translated=False):
        if translated:
            product = self.product.translated
            variant_display = str(self.translated)
        else:
            variant_display = str(self)
            product = self.product
        product_display = ("%s (%s)" % (product, variant_display)
                           if variant_display else str(product))
        return smart_text(product_display)

    def get_first_image(self):
        images = list(self.images.all())
        return images[0] if images else self.product.get_first_image()

    def get_ajax_label(self, discounts=None):
        price = self.get_price(discounts)
        return "%s, %s, %s" % (
            self.sku,
            self.display_product(),
            prices_i18n.amount(price),
        )
示例#30
0
class Order(models.Model):
    created = models.DateTimeField(default=now, editable=False)
    status = models.CharField(max_length=32,
                              default=OrderStatus.UNFULFILLED,
                              choices=OrderStatus.CHOICES)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             blank=True,
                             null=True,
                             related_name='orders',
                             on_delete=models.SET_NULL)
    language_code = models.CharField(max_length=35,
                                     default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(max_length=36,
                                          blank=True,
                                          editable=False)
    billing_address = models.ForeignKey(Address,
                                        related_name='+',
                                        editable=False,
                                        on_delete=models.PROTECT)
    shipping_address = models.ForeignKey(Address,
                                         related_name='+',
                                         editable=False,
                                         null=True,
                                         on_delete=models.PROTECT)
    user_email = models.EmailField(blank=True, default='', editable=False)
    shipping_price_net = MoneyField(currency=settings.DEFAULT_CURRENCY,
                                    max_digits=12,
                                    decimal_places=2,
                                    default=0,
                                    editable=False)
    shipping_price_gross = MoneyField(currency=settings.DEFAULT_CURRENCY,
                                      max_digits=12,
                                      decimal_places=2,
                                      default=0,
                                      editable=False)
    shipping_price = TaxedMoneyField(net_field='shipping_price_net',
                                     gross_field='shipping_price_gross')
    shipping_method_name = models.CharField(max_length=255,
                                            null=True,
                                            default=None,
                                            blank=True,
                                            editable=False)
    token = models.CharField(max_length=36, unique=True)
    total_net = MoneyField(currency=settings.DEFAULT_CURRENCY,
                           max_digits=12,
                           decimal_places=2,
                           default=0)
    total_gross = MoneyField(currency=settings.DEFAULT_CURRENCY,
                             max_digits=12,
                             decimal_places=2,
                             default=0)
    total = TaxedMoneyField(net_field='total_net', gross_field='total_gross')
    voucher = models.ForeignKey(Voucher,
                                null=True,
                                related_name='+',
                                on_delete=models.SET_NULL)
    discount_amount = MoneyField(currency=settings.DEFAULT_CURRENCY,
                                 max_digits=12,
                                 decimal_places=2,
                                 blank=True,
                                 null=True)
    discount_name = models.CharField(max_length=255, default='', blank=True)

    class Meta:
        ordering = ('-pk', )
        permissions = (('view_order',
                        pgettext_lazy('Permission description',
                                      'Can view orders')),
                       ('edit_order',
                        pgettext_lazy('Permission description',
                                      'Can edit orders')))

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def is_fully_paid(self):
        total_paid = sum([
            payment.get_total_price()
            for payment in self.payments.filter(status=PaymentStatus.CONFIRMED)
        ],
                         TaxedMoney(net=Money(0, settings.DEFAULT_CURRENCY),
                                    gross=Money(0, settings.DEFAULT_CURRENCY)))
        return total_paid.gross >= self.total.gross

    def get_user_current_email(self):
        return self.user.email if self.user else self.user_email

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __iter__(self):
        return iter(self.lines.all())

    def __repr__(self):
        return '<Order #%r>' % (self.id, )

    def __str__(self):
        return '#%d' % (self.id, )

    def get_absolute_url(self):
        return reverse('order:details', kwargs={'token': self.token})

    def get_last_payment_status(self):
        last_payment = self.payments.last()
        if last_payment:
            return last_payment.status
        return None

    def get_last_payment_status_display(self):
        last_payment = self.payments.last()
        if last_payment:
            return last_payment.get_status_display()
        return None

    def is_pre_authorized(self):
        return self.payments.filter(status=PaymentStatus.PREAUTH).exists()

    @property
    def quantity_fulfilled(self):
        return sum([line.quantity_fulfilled for line in self])

    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self)

    def get_status_display(self):
        """Order status display text."""
        return dict(OrderStatus.CHOICES)[self.status]

    def get_subtotal(self):
        subtotal_iterator = (line.get_total() for line in self)
        return sum(subtotal_iterator, ZERO_TAXED_MONEY)

    def get_total_quantity(self):
        return sum([line.quantity for line in self])

    def can_edit(self):
        return self.status == OrderStatus.UNFULFILLED

    def can_fulfill(self):
        statuses = {OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED}
        return self.status in statuses

    def can_cancel(self):
        return self.status != OrderStatus.CANCELED
示例#31
0
class Order(ModelWithMetadata):
    created = models.DateTimeField(default=now, editable=False)
    status = models.CharField(max_length=32,
                              default=OrderStatus.UNFULFILLED,
                              choices=OrderStatus.CHOICES)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    language_code = models.CharField(max_length=35,
                                     default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(max_length=36,
                                          blank=True,
                                          editable=False)
    billing_address = models.ForeignKey(Address,
                                        related_name="+",
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name="+",
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    user_email = models.EmailField(blank=True, default="")

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
        default=settings.DEFAULT_CURRENCY,
    )

    shipping_method = models.ForeignKey(
        ShippingMethod,
        blank=True,
        null=True,
        related_name="orders",
        on_delete=models.SET_NULL,
    )
    shipping_method_name = models.CharField(max_length=255,
                                            null=True,
                                            default=None,
                                            blank=True,
                                            editable=False)

    shipping_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_net = MoneyField(amount_field="shipping_price_net_amount",
                                    currency_field="currency")

    shipping_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False,
    )
    shipping_price_gross = MoneyField(
        amount_field="shipping_price_gross_amount", currency_field="currency")

    shipping_price = TaxedMoneyField(
        net_amount_field="shipping_price_net_amount",
        gross_amount_field="shipping_price_gross_amount",
        currency_field="currency",
    )

    token = models.CharField(max_length=36, unique=True, blank=True)
    # Token of a checkout instance that this order was created from
    checkout_token = models.CharField(max_length=36, blank=True)

    total_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    total_net = MoneyField(amount_field="total_net_amount",
                           currency_field="currency")

    total_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    total_gross = MoneyField(amount_field="total_gross_amount",
                             currency_field="currency")

    total = TaxedMoneyField(
        net_amount_field="total_net_amount",
        gross_amount_field="total_gross_amount",
        currency_field="currency",
    )

    voucher = models.ForeignKey(Voucher,
                                blank=True,
                                null=True,
                                related_name="+",
                                on_delete=models.SET_NULL)
    gift_cards = models.ManyToManyField(GiftCard,
                                        blank=True,
                                        related_name="orders")
    discount_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    discount = MoneyField(amount_field="discount_amount",
                          currency_field="currency")
    discount_name = models.CharField(max_length=255, default="", blank=True)
    translated_discount_name = models.CharField(max_length=255,
                                                default="",
                                                blank=True)
    display_gross_prices = models.BooleanField(default=True)
    customer_note = models.TextField(blank=True, default="")
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              default=zero_weight)
    objects = OrderQueryset.as_manager()

    class Meta:
        ordering = ("-pk", )
        permissions = ((
            "manage_orders",
            pgettext_lazy("Permission description", "Manage orders."),
        ), )

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def is_fully_paid(self):
        total_paid = self._total_paid()
        return total_paid.gross >= self.total.gross

    def is_partly_paid(self):
        total_paid = self._total_paid()
        return total_paid.gross.amount > 0

    def get_customer_email(self):
        return self.user.email if self.user else self.user_email

    def _total_paid(self):
        # Get total paid amount from partially charged,
        # fully charged and partially refunded payments
        payments = self.payments.filter(charge_status__in=[
            ChargeStatus.PARTIALLY_CHARGED,
            ChargeStatus.FULLY_CHARGED,
            ChargeStatus.PARTIALLY_REFUNDED,
        ])
        total_captured = [
            payment.get_captured_amount() for payment in payments
        ]
        total_paid = sum(total_captured, zero_taxed_money())
        return total_paid

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __iter__(self):
        return iter(self.lines.all())

    def __repr__(self):
        return "<Order #%r>" % (self.id, )

    def __str__(self):
        return "#%d" % (self.id, )

    def get_absolute_url(self):
        return reverse("order:details", kwargs={"token": self.token})

    def get_last_payment(self):
        return max(self.payments.all(), default=None, key=attrgetter("pk"))

    def get_payment_status(self):
        last_payment = self.get_last_payment()
        if last_payment:
            return last_payment.charge_status
        return ChargeStatus.NOT_CHARGED

    def get_payment_status_display(self):
        last_payment = self.get_last_payment()
        if last_payment:
            return last_payment.get_charge_status_display()
        return dict(ChargeStatus.CHOICES).get(ChargeStatus.NOT_CHARGED)

    def is_pre_authorized(self):
        return (self.payments.filter(
            is_active=True, transactions__kind=TransactionKind.AUTH).filter(
                transactions__is_success=True).exists())

    @property
    def quantity_fulfilled(self):
        return sum([line.quantity_fulfilled for line in self])

    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self)

    def get_subtotal(self):
        subtotal_iterator = (line.get_total() for line in self)
        return sum(subtotal_iterator, zero_taxed_money())

    def get_total_quantity(self):
        return sum([line.quantity for line in self])

    def is_draft(self):
        return self.status == OrderStatus.DRAFT

    def is_open(self):
        statuses = {OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED}
        return self.status in statuses

    def can_cancel(self):
        return self.status not in {OrderStatus.CANCELED, OrderStatus.DRAFT}

    def can_capture(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_capture() and order_status_ok

    def can_charge(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_charge() and order_status_ok

    def can_void(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_void()

    def can_refund(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_refund()

    def can_mark_as_paid(self):
        return len(self.payments.all()) == 0

    @property
    def total_authorized(self):
        payment = self.get_last_payment()
        if payment:
            return payment.get_authorized_amount()
        return zero_money()

    @property
    def total_captured(self):
        payment = self.get_last_payment()
        if payment and payment.charge_status in (
                ChargeStatus.PARTIALLY_CHARGED,
                ChargeStatus.FULLY_CHARGED,
                ChargeStatus.PARTIALLY_REFUNDED,
        ):
            return Money(payment.captured_amount, payment.currency)
        return zero_money()

    @property
    def total_balance(self):
        return self.total_captured - self.total.gross

    def get_total_weight(self):
        return self.weight
示例#32
0
class ProductVariant(models.Model):
    sku = models.CharField(max_length=32, unique=True)
    name = models.CharField(max_length=255, blank=True)
    price_override = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)
    product = models.ForeignKey(
        Product, related_name='variants', on_delete=models.CASCADE)
    attributes = HStoreField(default={}, blank=True)
    images = models.ManyToManyField('ProductImage', through='VariantImage')
    quantity = models.IntegerField(
        validators=[MinValueValidator(0)], default=Decimal(1))
    quantity_allocated = models.IntegerField(
        validators=[MinValueValidator(0)], default=Decimal(0))
    cost_price = MoneyField(
        currency=settings.DEFAULT_CURRENCY, max_digits=12,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)

    class Meta:
        app_label = 'product'

    def __str__(self):
        return self.name

    @property
    def quantity_available(self):
        return max(self.quantity - self.quantity_allocated, 0)

    def get_total(self):
        if self.cost_price:
            return TaxedMoney(net=self.cost_price, gross=self.cost_price)

    def check_quantity(self, quantity):
        if quantity > self.quantity_available:
            raise InsufficientStock(self)

    def get_price_per_item(self, discounts=None):
        price = self.price_override or self.product.price
        price = TaxedMoney(net=price, gross=price)
        price = calculate_discounted_price(self.product, price, discounts)
        return price

    def get_absolute_url(self):
        slug = self.product.get_slug()
        product_id = self.product.id
        return reverse('product:details',
                       kwargs={'slug': slug, 'product_id': product_id})

    def as_data(self):
        return {
            'product_name': str(self),
            'product_id': self.product.pk,
            'variant_id': self.pk,
            'unit_price': str(self.get_price_per_item().gross)}

    def is_shipping_required(self):
        return self.product.product_type.is_shipping_required

    def is_in_stock(self):
        return self.quantity_available > 0

    def display_product(self):
        variant_display = str(self)
        product_display = (
            '%s (%s)' % (self.product, variant_display)
            if variant_display else str(self.product))
        return smart_text(product_display)

    def get_first_image(self):
        return self.product.get_first_image()
示例#33
0
class ProductVariant(models.Model):
    sku = models.CharField(max_length=32, unique=True)
    name = models.CharField(max_length=255, blank=True)
    price_override = MoneyField(currency=settings.DEFAULT_CURRENCY,
                                max_digits=12,
                                decimal_places=settings.DEFAULT_DECIMAL_PLACES,
                                blank=True,
                                null=True)
    product = models.ForeignKey(Product,
                                related_name='variants',
                                on_delete=models.CASCADE)
    attributes = HStoreField(default={}, blank=True)
    images = models.ManyToManyField('ProductImage', through='VariantImage')
    track_inventory = models.BooleanField(default=True)
    quantity = models.IntegerField(validators=[MinValueValidator(0)],
                                   default=Decimal(1))
    quantity_allocated = models.IntegerField(validators=[MinValueValidator(0)],
                                             default=Decimal(0))
    cost_price = MoneyField(currency=settings.DEFAULT_CURRENCY,
                            max_digits=12,
                            decimal_places=settings.DEFAULT_DECIMAL_PLACES,
                            blank=True,
                            null=True)

    class Meta:
        app_label = 'product'

    def __str__(self):
        return self.name or self.sku

    @property
    def quantity_available(self):
        return max(self.quantity - self.quantity_allocated, 0)

    def check_quantity(self, quantity):
        """ Check if there is at least the given quantity in stock
        if stock handling is enabled.
        """
        if self.track_inventory and quantity > self.quantity_available:
            raise InsufficientStock(self)

    @property
    def base_price(self):
        return self.price_override or self.product.price

    def get_price(self, discounts=None, taxes=None):
        price = calculate_discounted_price(self.product, self.base_price,
                                           discounts)
        if not self.product.charge_taxes:
            taxes = None
        tax_rate = (self.product.tax_rate
                    or self.product.product_type.tax_rate)
        return apply_tax_to_price(taxes, tax_rate, price)

    def get_absolute_url(self):
        slug = self.product.get_slug()
        product_id = self.product.id
        return reverse('product:details',
                       kwargs={
                           'slug': slug,
                           'product_id': product_id
                       })

    def is_shipping_required(self):
        return self.product.product_type.is_shipping_required

    def is_in_stock(self):
        return self.quantity_available > 0

    def display_product(self):
        variant_display = str(self)
        product_display = ('%s (%s)' % (self.product, variant_display)
                           if variant_display else str(self.product))
        return smart_text(product_display)

    def get_first_image(self):
        return self.product.get_first_image()

    def get_ajax_label(self, discounts=None):
        price = self.get_price(discounts).gross
        return '%s, %s, %s' % (self.sku, self.display_product(),
                               prices_i18n.amount(price))
示例#34
0
class Order(models.Model):
    created = models.DateTimeField(default=now, editable=False)
    status = models.CharField(max_length=32,
                              default=OrderStatus.UNFULFILLED,
                              choices=OrderStatus.CHOICES)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             blank=True,
                             null=True,
                             related_name='orders',
                             on_delete=models.SET_NULL)
    language_code = models.CharField(max_length=35,
                                     default=settings.LANGUAGE_CODE)
    tracking_client_id = models.CharField(max_length=36,
                                          blank=True,
                                          editable=False)
    billing_address = models.ForeignKey(Address,
                                        related_name='+',
                                        editable=False,
                                        null=True,
                                        on_delete=models.SET_NULL)
    shipping_address = models.ForeignKey(Address,
                                         related_name='+',
                                         editable=False,
                                         null=True,
                                         on_delete=models.SET_NULL)
    user_email = models.EmailField(blank=True, default='')
    shipping_method = models.ForeignKey(ShippingMethod,
                                        blank=True,
                                        null=True,
                                        related_name='orders',
                                        on_delete=models.SET_NULL)
    shipping_price_net = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False)
    shipping_price_gross = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
        editable=False)
    shipping_price = TaxedMoneyField(net_field='shipping_price_net',
                                     gross_field='shipping_price_gross')
    shipping_method_name = models.CharField(max_length=255,
                                            null=True,
                                            default=None,
                                            blank=True,
                                            editable=False)
    token = models.CharField(max_length=36, unique=True, blank=True)
    total_net = MoneyField(currency=settings.DEFAULT_CURRENCY,
                           max_digits=settings.DEFAULT_MAX_DIGITS,
                           decimal_places=settings.DEFAULT_DECIMAL_PLACES,
                           default=zero_money)
    total_gross = MoneyField(currency=settings.DEFAULT_CURRENCY,
                             max_digits=settings.DEFAULT_MAX_DIGITS,
                             decimal_places=settings.DEFAULT_DECIMAL_PLACES,
                             default=zero_money)
    total = TaxedMoneyField(net_field='total_net', gross_field='total_gross')
    voucher = models.ForeignKey(Voucher,
                                blank=True,
                                null=True,
                                related_name='+',
                                on_delete=models.SET_NULL)
    discount_amount = MoneyField(
        currency=settings.DEFAULT_CURRENCY,
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=zero_money)
    discount_name = models.CharField(max_length=255, default='', blank=True)
    translated_discount_name = models.CharField(max_length=255,
                                                default='',
                                                blank=True)
    display_gross_prices = models.BooleanField(default=True)
    customer_note = models.TextField(blank=True, default='')
    weight = MeasurementField(measurement=Weight,
                              unit_choices=WeightUnits.CHOICES,
                              default=zero_weight)
    objects = OrderQueryset.as_manager()

    class Meta:
        ordering = ('-pk', )
        permissions = (('manage_orders',
                        pgettext_lazy('Permission description',
                                      'Manage orders.')), )

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = str(uuid4())
        return super().save(*args, **kwargs)

    def is_fully_paid(self):
        payments = self.payments.filter(charge_status=ChargeStatus.CHARGED)
        total_captured = [
            payment.get_captured_amount() for payment in payments
        ]
        total_paid = sum(total_captured, ZERO_TAXED_MONEY)
        return total_paid.gross >= self.total.gross

    def get_user_current_email(self):
        return self.user.email if self.user else self.user_email

    def _index_billing_phone(self):
        return self.billing_address.phone

    def _index_shipping_phone(self):
        return self.shipping_address.phone

    def __iter__(self):
        return iter(self.lines.all())

    def __repr__(self):
        return '<Order #%r>' % (self.id, )

    def __str__(self):
        return '#%d' % (self.id, )

    def get_absolute_url(self):
        return reverse('order:details', kwargs={'token': self.token})

    def get_last_payment(self):
        return max(self.payments.all(), default=None, key=attrgetter('pk'))

    def get_last_payment_status(self):
        last_payment = self.get_last_payment()
        if last_payment:
            return last_payment.charge_status
        return None

    def get_last_payment_status_display(self):
        last_payment = self.get_last_payment()
        if last_payment:
            return last_payment.get_charge_status_display()
        return None

    def is_pre_authorized(self):
        return self.payments.filter(
            is_active=True, transactions__kind=TransactionKind.AUTH).filter(
                transactions__is_success=True).exists()

    @property
    def quantity_fulfilled(self):
        return sum([line.quantity_fulfilled for line in self])

    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self)

    def get_subtotal(self):
        subtotal_iterator = (line.get_total() for line in self)
        return sum(subtotal_iterator, ZERO_TAXED_MONEY)

    def get_total_quantity(self):
        return sum([line.quantity for line in self])

    def is_draft(self):
        return self.status == OrderStatus.DRAFT

    def is_open(self):
        statuses = {OrderStatus.UNFULFILLED, OrderStatus.PARTIALLY_FULFILLED}
        return self.status in statuses

    def can_cancel(self):
        return self.status not in {OrderStatus.CANCELED, OrderStatus.DRAFT}

    def can_capture(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_capture() and order_status_ok

    def can_charge(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        order_status_ok = self.status not in {
            OrderStatus.DRAFT, OrderStatus.CANCELED
        }
        return payment.can_charge() and order_status_ok

    def can_void(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_void()

    def can_refund(self, payment=None):
        if not payment:
            payment = self.get_last_payment()
        if not payment:
            return False
        return payment.can_refund()

    def can_mark_as_paid(self):
        return len(self.payments.all()) == 0

    @property
    def total_authorized(self):
        payment = self.get_last_payment()
        if payment:
            return payment.get_authorized_amount()
        return zero_money()

    @property
    def total_captured(self):
        payment = self.get_last_payment()
        if payment and payment.charge_status == ChargeStatus.CHARGED:
            return Money(payment.captured_amount, payment.currency)
        return zero_money()

    @property
    def total_balance(self):
        return self.total_captured - self.total.gross

    def get_total_weight(self):
        # Cannot use `sum` as it parses an empty Weight to an int
        weights = Weight(kg=0)
        for line in self:
            weights += line.variant.get_weight() * line.quantity
        return weights
示例#35
0
文件: models.py 项目: uzunnet/saleor
class OrderLine(models.Model):
    order = models.ForeignKey(Order,
                              related_name="lines",
                              editable=False,
                              on_delete=models.CASCADE)
    variant = models.ForeignKey(
        "product.ProductVariant",
        related_name="order_lines",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    # max_length is as produced by ProductVariant's display_product method
    product_name = models.CharField(max_length=386)
    variant_name = models.CharField(max_length=255, default="", blank=True)
    translated_product_name = models.CharField(max_length=386,
                                               default="",
                                               blank=True)
    translated_variant_name = models.CharField(max_length=255,
                                               default="",
                                               blank=True)
    product_sku = models.CharField(max_length=255)
    is_shipping_required = models.BooleanField()
    quantity = models.IntegerField(validators=[MinValueValidator(1)])
    quantity_fulfilled = models.IntegerField(validators=[MinValueValidator(0)],
                                             default=0)

    currency = models.CharField(
        max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, )

    unit_discount_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    unit_discount = MoneyField(amount_field="unit_discount_amount",
                               currency_field="currency")
    unit_discount_type = models.CharField(
        max_length=10,
        choices=DiscountValueType.CHOICES,
        default=DiscountValueType.FIXED,
    )
    unit_discount_reason = models.TextField(blank=True, null=True)

    unit_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    # stores the value of the applied discount. Like 20 of %
    unit_discount_value = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
        default=0,
    )
    unit_price_net = MoneyField(amount_field="unit_price_net_amount",
                                currency_field="currency")

    unit_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    unit_price_gross = MoneyField(amount_field="unit_price_gross_amount",
                                  currency_field="currency")

    unit_price = TaxedMoneyField(
        net_amount_field="unit_price_net_amount",
        gross_amount_field="unit_price_gross_amount",
        currency="currency",
    )

    total_price_net_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    total_price_net = MoneyField(
        amount_field="total_price_net_amount",
        currency_field="currency",
    )

    total_price_gross_amount = models.DecimalField(
        max_digits=settings.DEFAULT_MAX_DIGITS,
        decimal_places=settings.DEFAULT_DECIMAL_PLACES,
    )
    total_price_gross = MoneyField(
        amount_field="total_price_gross_amount",
        currency_field="currency",
    )

    total_price = TaxedMoneyField(
        net_amount_field="total_price_net_amount",
        gross_amount_field="total_price_gross_amount",
        currency="currency",
    )

    tax_rate = models.DecimalField(max_digits=5,
                                   decimal_places=4,
                                   default=Decimal("0.0"))

    objects = OrderLineQueryset.as_manager()

    class Meta:
        ordering = ("pk", )

    def __str__(self):
        return (f"{self.product_name} ({self.variant_name})"
                if self.variant_name else self.product_name)

    @property
    def undiscounted_unit_price(self) -> "TaxedMoney":
        return self.unit_price + self.unit_discount

    @property
    def quantity_unfulfilled(self):
        return self.quantity - self.quantity_fulfilled

    @property
    def is_digital(self) -> Optional[bool]:
        """Check if a variant is digital and contains digital content."""
        if not self.variant:
            return None
        is_digital = self.variant.is_digital()
        has_digital = hasattr(self.variant, "digital_content")
        return is_digital and has_digital
示例#36
0
def test_money_field_init():
    field = MoneyField(
        currency='BTC', default='5', max_digits=9, decimal_places=2)
    assert field.get_default() == Money(5, 'BTC')