Django models for creating multiple variations of t-shirts

Viewed 358

I'm creating an ecommerce store that sells T-shirts, hoodies, mugs, shot glasses, etc. For the t-shirts and hoodies there are sizes and sometimes color associated with each product. I'm trying to add multiple variations for each product. Here's my model.py code:

class Category(models.Model):
    name = models.CharField(max_length = 255, db_index=True, null=True, blank=True)
    slug = models.SlugField(max_length=255, unique=True, default='')
    class Meta:
        verbose_name_plural = 'categories'

    def get_absolute_url(self):
        return reverse('main:category_list', args=[self.slug])
    def __str__(self):
        return self.name

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True)
    name = models.CharField(max_length = 255, default='')
    description = models.TextField(blank=True)
    image = models.ImageField(upload_to='products/', null=True, blank=True)
    slug = models.SlugField(max_length = 255, default='')
    price = models.DecimalField(max_digits=4,decimal_places=2)
    update_defaults = models.BooleanField(default=False)
    #inventory = models.DecimalField(max_digits=5, decimal_places=None)

    class Meta:
        ordering=(['name'])

    def get_absolute_url(self):
        return reverse('main:product_detail', args=[self.slug])

    def __str__(self):
        return self.name

class VariationManager(models.Manager):
    def all(self):
        return super(VariationManager, self).filter(active=True)

    def sizes(self):
        return self.all().filter(category='size')

    def colors(self):
        return self.all().filter(category='color')


VAR_CATEGORIES = (
    ('size', 'size'),
    ('color', 'color'),
    )


class Variation(models.Model):
    product = models.ForeignKey(Product, related_name="product_attrs", on_delete=models.CASCADE)
    category = models.CharField(max_length=120, choices=VAR_CATEGORIES, default='size')
    title = models.CharField(max_length=120)
    price = models.DecimalField(max_digits=100, decimal_places=2, null=True, blank=True)
    updated = models.DateTimeField(auto_now_add=False, auto_now=True)
    active = models.BooleanField(default=True)

    objects = VariationManager()

    def __str__(self):
        return self.category + " " + self.title

def product_defaults(sender, instance, created, *args, **kwargs):
    if instance.update_defaults:
        categories = Category.objects.all()
        print (categories)
        for cat in categories:
            print (cat.id)
            if cat.id == 1: #for t-shirts
                small_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='Small')
                medium_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='Medium')
                large_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='Large')
                XL_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='XL')
                DoubleXL_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='2XL')
                TripleXL_size = Variation.objects.get_or_create(product=instance,
                                            category='size',
                                            title='3XL')
        instance.update_defaults = False
        instance.save()

post_save.connect(product_defaults, sender=Product)

The way it appears right now in my admin interface there is a name, attribute, value, and price (only named relevant fields for clarity). If I add a product like such: "t-shirt_1, size, sm, 17.98", then the next item I need to add is "t-shirt_1, size, med, 17.98" and so forth (2xl and above, price goes up). Is there a way to simplify this where I just enter the product name once, then add all sizes and associated pricing, as well as inventory tracking (haven't created field yet) for each size within the product?

edit: I've edited my code. I got it figured out on the variations. Now I can't figure out how I could tie inventory quantities into it. If I put it in Product class, it's not specifying what size (i.e. 10 small, 8 medium, 12 Large).

2 Answers

Remove corresponding attribute fields from Product model and create OneToMany relationship between Product and ProductAttribute. Then create separate Size and Color models, and relate them to ProductAttribute with ManyToMany relationship:

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True)
    name = models.CharField(max_length = 255, default='')
    description = models.TextField(blank=True)
    image = models.ImageField(upload_to='products/', null=True, blank=True)
    slug = models.SlugField(max_length = 255, default='')
    price = models.DecimalField(max_digits=4,decimal_places=2)
    has_attributes = models.BooleanField(default=False)
    ...


class ProductAttribute(models.Model):
    product = models.ForeignKey('Product', related_name="product_attrs", on_delete=models.CASCADE)
    sizes = models.ManyToManyField('Size', related_name="product_sizes", null=True, blank=True)
    colors = models.ManyToManyField('Product', related_name="product_colors", null=True, blank=True)
    ...


class Size(models.Model):
    size_num = models.CharField(max_length=10)
    ...


class Color(models.Model):
    color_name = models.CharField(max_length=15)
    ...

Now you can create Product object, then go to ProductAttribute to relate corresponding product object with product attribute model and add each attribute of that product (sizes or colors). Whenever you need to get product size or color you can do it as follows:

# let's say you need to get sizes of last product
p = Product.objects.last()
sizes = p.product_attrs.sizes.all()
# ↑ returns queryset containing all sizes of product

Not required

class Attribute(models.Model):
    name = models.CharField(max_length = 255,   default='')

    def __str__(self):
        return self.name
Related