Shopify-like product variants in Django online store

In this blog post, we will discuss how to create Shopify-like product variants in Django. Our goal was to have a product with multiple variants, each with its own price. We liked the approach used by Shopify, which uses product options and variants.

A product option represents a choice that a customer can make when purchasing a product. For example, a t-shirt product might have the option of "Size" with the values "Small", "Medium", and "Large". A product variant is a specific combination of options that a product can have. In the example of the t-shirt, a variant would be "Small - Red" or "Large - Blue".

One of the key components of this approach is the use of the save_related method in the product admin class. This method is called after the parent product and its related options and variants have been saved, and it allows us to perform additional actions on the related objects. In our case, we use save_related to automatically create variants for each option value, and to delete any variants that are no longer associated with the product.

Here's an example of how we implemented this in our project:

class ProductAdmin(admin.ModelAdmin):
    list_display = ("title", "price", "order")
    list_display_links = ("title",)
    prepopulated_fields = {"slug": ("title",)}
    inlines = (ProductOptionInline, ProductVariantInline)
    form = ProductAdminForm

    def save_related(self, request, form, formsets, change):
        super().save_related(request, form, formsets, change)
        instance = form.instance
        instance.refresh_from_db()
        if instance.option1:
            for option_value in instance.option1.values:
                instance.variants.get_or_create(option1=option_value, defaults={"price": instance.price})

            instance.variants.exclude(option1__in=instance.option1.values).delete()

In this example, we've overridden the save_related method in the ProductAdmin class. The super().save_related(request, form, formsets, change) line calls the parent method so that the parent product and related options and variants are saved as usual. Then, we refresh the instance from the database using instance.refresh_from_db(). Next, we use the get_or_create method to create a variant for each option value, with the price set to the product's price. We also use the exclude method to delete any variants that are no longer associated with the product.

With this approach, we were able to easily create and manage product variants with different prices, pretty close to Shopify. Below you can see all the parts.