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