Search code examples
djangodjango-modelsdjango-forms

How to make on update record pass the signal in Django Model?


Hello I'm trying to make on update trigger in Django Model.

Model.py

class Item(models.Model):
    status_type = (
        ('a','Active'),
        ('d','Deactive')
    )
    code = models.CharField(max_length=100, null=True, blank=True, unique=True)
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    gsts = models.ForeignKey(Gst, on_delete=models.CASCADE)
    sgst = models.DecimalField(max_digits=10, decimal_places=2,null=True,blank=True)
    cgst = models.DecimalField(max_digits=10, decimal_places=2,null=True,blank=True)
    net_amount = models.DecimalField(max_digits=10, decimal_places=2,null=True,blank=True)
    status = models.CharField(max_length=1, choices=status_type, default = 'a')
    create_at = models.DateField(auto_now_add=True)
    update_at = models.DateField(auto_now=True)
    create_by = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.name
    
def ItemGenerator(sender, instance, created, *args, **kwargs):
    if created:
        id = instance
        query = Item.objects.get(id = int(id.id))
        #gst calculation
        gst_cal = query.price * query.gsts.gst/100
        #net amount calculation
        net_amounts = query.price + gst_cal        
        #item number generate
        temp_count = Item.objects.all().count()
        no = str('ITEM') + '/' +str(temp_count)
        query = Item.objects.filter(id = id.id).update(code = str(no), cgst = round(gst_cal/2), sgst=round(gst_cal/2), net_amount = round(net_amounts))

When I'm insert to the data post_save signal is call and my price and gst calculation save into data base but problem is that when I'm update the record post_save signal is not working


Solution

  • This is not a bug, this is expected behavior, your if created checks if you run the signal for a created object, hence it will not run in case the record is updated.

    You can omit this with:

    def ItemGenerator(sender, instance, created, *args, **kwargs):
        if created:
            temp_count = Item.objects.all().count()
            no = str('ITEM') + '/' + str(temp_count)
        else:
            no = instance.code
        gst_cal = instance.price * instance.gsts.gst / 100
        net_amounts = instance.price + gst_cal
        query = Item.objects.filter(pk=instance.pk).update(
            code=no,
            cgst=round(gst_cal / 2),
            sgst=round(gst_cal / 2),
            net_amount=round(net_amounts),
        )

    But the signal is not necessary. Signals are often a pattern to avoid. Signals for example don't run in case of bulk updates or bulk creates, can easily result in infinite recursion, etc. Here is an article I wrote that discusses potential problems with signals.

    To make matters worse, this is just data duplication, which means that you repeat the same data, but in another shape or form.

    from django.conf import settings
    
    
    class Item(models.Model):
        status_type = (('a', 'Active'), ('d', 'Deactive'))
        name = models.CharField(max_length=100)
        price = models.DecimalField(max_digits=10, decimal_places=2)
        gsts = models.ForeignKey(Gst, on_delete=models.CASCADE)
        status = models.CharField(max_length=1, choices=status_type, default='a')
        create_at = models.DateField(auto_now_add=True)
        update_at = models.DateField(auto_now=True)
        create_by = models.ForeignKey(
            settings.AUTH_USER_MODEL, on_delete=models.CASCADE
        )
    
        @property
        def code(self):
            return f'ITEM/{type(self).objects.filter(create_at__lt=self.create_at).count()}'
    
        @property
        def cgst_unround(self):
            if not hasattr(self, '_cgst'):
                self._cgst = self.price * self.gsts.gsts / 100
            return self._cgst
    
        @property
        def cgst(self):
            return round(self.cgst_unround, 2)
    
        @property
        def sgst(self):
            return self.cgst
    
        @property
        def net_amount(self):
            return round(self.price + self.gst)
    
        def __str__(self):
            return self.name

    This will then determine the values when needed, so we can use some_item.net_amount, and if so, it will fetch the .gsts and do the proper calculations. This will thus also return a different value, if you change the .price or for example the .gst of the .gsts, which is more reliable: if you alter the gst of a Gst object, signals will not run for the Item, although the net_amount depends on that change, by just calculating it when needed, that will not be a problem.

    In case you need to determine the .cgst or .sgst for a large amount of objects, it can cause the so-called N+1 problem. We can omit that by selecting the related .gsts of the items before calculating:

    Item.objects.select_related('gsts')