Exemple #1
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)
    available_for_purchase = models.DateField(blank=True, null=True)
    visible_in_listings = models.BooleanField(default=False)
    default_variant = models.OneToOneField(
        "ProductVariant",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    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"]

    def is_available_for_purchase(self):
        return (self.available_for_purchase is not None
                and datetime.date.today() >= self.available_for_purchase)
Exemple #2
0
class ProductVariant(SortableModel, 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 = ("sort_order", "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()

    def get_ordering_queryset(self):
        return self.product.variants.all()
Exemple #3
0
class Order(ModelWithMetadata):
    store = models.ForeignKey(
        Store,
        related_name="orders",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    tenant_id = 'store_id'
    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, )

    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 __iter__(self):
        return iter(self.lines.all())

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

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

    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):
        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):
        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(self.currency)

    @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(self.currency)

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

    def get_total_weight(self, *_args):
        return self.weight