Django ManyToMany field update

I have trouble with updating calculating_special_order_items, calculating_floor_special_order_items, order_total, ship_total, confirm_total, real_total and calculating_total. When I save a new instance with ship_special_order_items through a view it saves normally, calculating_special_order_items is updated and all totals are updated correctly and it is showed in django admin site correctly, but when I try to update calculating_special_order_items with saving confirm_special_order_items it is not updating correctly.

class Order(models.Model):
    STATUS_CHOICES = [("naruceno","naruceno"), ("poslato","poslato"), ("stiglo","stiglo")]
    ordering_facility = models.ForeignKey(Facility, on_delete=models.PROTECT, related_name='order_create_facility')
    dispatching_facility = models.ForeignKey(Facility, on_delete=models.PROTECT, related_name='order_dispatch_facility')
    order_timestamp = models.DateTimeField(auto_now_add=True)
    ship_timestamp = models.DateTimeField(blank=True, null=True)
    confirm_timestamp = models.DateTimeField(blank=True, null=True)
    real_timestamp = models.DateTimeField(blank=True, null=True)
    order_user = models.CharField(max_length=100, blank=True)
    ship_user = models.CharField(max_length=100, blank=True)
    confirm_user = models.CharField(max_length=100, blank=True)
    real_user = models.CharField(max_length=100, blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_CHOICES[0][0])    
    product_variations = models.ManyToManyField(ProductVariation, through='OrderItem')
    order_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    ship_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    confirm_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    real_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    calculating_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    ship_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='ship_special_order_items')
    ship_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='ship_floor_special_order_items')
    confirm_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='confirm_special_order_items')
    confirm_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='confirm_floor_special_order_items')
    real_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='real_special_order_items')
    real_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='real_floor_special_order_items')
    calculating_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='calculating_special_order_items')
    calculating_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='calculating_floor_special_order_items')
    billed = models.BooleanField(default=False)

    
    def calculate_calculating_special_order_items(self):
        if self.real_special_order_items.exists():
            return self.real_special_order_items.all()
        elif self.confirm_special_order_items.exists():
            return self.confirm_special_order_items.all()
        elif self.ship_special_order_items.exists():
            return self.ship_special_order_items.all()
        else:
            return []

    
    
    def calculate_calculating_floor_special_order_items(self):
        if self.real_floor_special_order_items.exists():
            return self.real_floor_special_order_items.all()
        elif self.confirm_floor_special_order_items.exists():
            return self.confirm_floor_special_order_items.all()        
        elif self.ship_floor_special_order_items.exists():       
            return self.ship_floor_special_order_items.all()        
        else:
            return []     
        

    def update_total(self):
        self.calculating_special_order_items.set(self.calculate_calculating_special_order_items())
        self.calculating_floor_special_order_items.set(self.calculate_calculating_floor_special_order_items())
        ship_special_order_items_total = self.ship_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.ship_special_order_items.exists() else 0
        ship_floor_special_order_items_total = self.ship_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.ship_floor_special_order_items.exists() else 0
        ship_specials_total = ship_special_order_items_total + ship_floor_special_order_items_total
        confirm_special_order_items_total = self.confirm_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.confirm_special_order_items.exists() else 0
        confirm_floor_special_order_items_total = self.confirm_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.confirm_floor_special_order_items.exists() else 0
        confirm_specials_total = confirm_special_order_items_total + confirm_floor_special_order_items_total
        real_special_order_items_total = self.real_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.real_special_order_items.exists() else 0
        real_floor_special_order_items_total = self.real_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.real_floor_special_order_items.exists() else 0
        real_specials_total = real_special_order_items_total + real_floor_special_order_items_total
        calculating_special_order_items_total = self.calculating_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.calculating_special_order_items.exists() else 0
        calculating_floor_special_order_items_total = self.calculating_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.calculating_floor_special_order_items.exists() else 0
        calculating_specials_total = calculating_special_order_items_total + calculating_floor_special_order_items_total
        self.order_total = self.orderitem_set.aggregate(models.Sum('order_subtotal'))['order_subtotal__sum']
        self.ship_total = self.orderitem_set.aggregate(models.Sum('ship_subtotal'))['ship_subtotal__sum'] + ship_specials_total if ship_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('ship_subtotal'))['ship_subtotal__sum'] is not None else ship_specials_total if ship_specials_total>0 and self.orderitem_set.aggregate(models.Sum('ship_subtotal'))['ship_subtotal__sum'] is None else self.orderitem_set.aggregate(models.Sum('ship_subtotal'))['ship_subtotal__sum']
        self.confirm_total = self.orderitem_set.aggregate(models.Sum('confirm_subtotal'))['confirm_subtotal__sum'] + confirm_specials_total if confirm_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('confirm_subtotal'))['confirm_subtotal__sum'] is not None else confirm_specials_total if confirm_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('confirm_subtotal'))['confirm_subtotal__sum'] is None else self.orderitem_set.aggregate(models.Sum('confirm_subtotal'))['confirm_subtotal__sum']
        self.real_total = self.orderitem_set.aggregate(models.Sum('real_subtotal'))['real_subtotal__sum'] + real_specials_total if real_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('real_subtotal'))['real_subtotal__sum'] is not None else  real_specials_total if real_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('real_subtotal'))['real_subtotal__sum'] is None else self.orderitem_set.aggregate(models.Sum('real_subtotal'))['real_subtotal__sum']
        self.calculating_total = self.orderitem_set.aggregate(models.Sum('calculating_subtotal'))['calculating_subtotal__sum'] + calculating_specials_total if calculating_specials_total > 0 and self.orderitem_set.aggregate(models.Sum('calculating_subtotal'))['calculating_subtotal__sum'] is not None else calculating_specials_total if calculating_specials_total>0 and self.orderitem_set.aggregate(models.Sum('calculating_subtotal'))['calculating_subtotal__sum'] is None else self.orderitem_set.aggregate(models.Sum('calculating_subtotal'))['calculating_subtotal__sum']
        
        

    def save(self, *args, **kwargs):    
        super().save(*args, **kwargs)
        self.update_total()
        super().save(*args, **kwargs)
    

        
    def __str__(self):
        return f"{self.ordering_facility} {str(self.ship_timestamp)[0:19]} {self.calculating_total}"



class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    product_variation = models.ForeignKey(ProductVariation, on_delete=models.PROTECT)
    order_quantity = models.PositiveIntegerField(blank=True, null=True)
    ship_quantity = models.PositiveIntegerField(blank=True, null=True)
    confirm_quantity = models.PositiveIntegerField(blank=True, null=True)
    real_quantity = models.PositiveIntegerField(blank=True, null=True)
    calculating_quantity = models.PositiveIntegerField(blank=True, null=True)
    order_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    ship_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    confirm_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    real_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
    calculating_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)


    @property
    def calculate_calculating_quantity(self):
        if self.real_quantity is not None:
            return self.real_quantity
        elif self.confirm_quantity is not None:
            return self.confirm_quantity
        elif self.ship_quantity is not None:
            return self.ship_quantity
        elif self.order_quantity is not None:
            return self.order_quantity
        else:
            return 0

    

    @property
    def calculate_order_subtotal(self):
        return self.product_variation.price * self.order_quantity if self.order_quantity is not None else None

    @property
    def calculate_ship_subtotal(self):
        return self.product_variation.price * self.ship_quantity if self.ship_quantity is not None else None

    @property
    def calculate_confirm_subtotal(self):
        return self.product_variation.price * self.confirm_quantity if self.confirm_quantity is not None else None
    
    @property
    def calculate_real_subtotal(self):
        return self.product_variation.price * self.real_quantity if self.real_quantity is not None else None

    @property
    def calculate_calculating_subtotal(self):
        return self.product_variation.price * self.calculating_quantity if self.calculating_quantity is not None else None

    def save(self, *args, **kwargs):
        self.order_subtotal = self.calculate_order_subtotal
        self.ship_subtotal = self.calculate_ship_subtotal
        self.confirm_subtotal = self.calculate_confirm_subtotal
        self.real_subtotal = self.calculate_real_subtotal
        self.calculating_quantity = self.calculate_calculating_quantity
        self.calculating_subtotal = self.calculate_calculating_subtotal
        super().save(*args, **kwargs)

        self.order.update_total()

I have tried everything for 2 days with code inside model, even with post-save signal, even with custom modelform for django admin, I am out of solutions, please help me if you encountered same problem and if you resolved it successfully. Thank You.

with code inside model, even with post-save signal, even with custom modelform for django admin.

I believe that. All these methods will not work, because Django populates or updates the ManyToManyField [Django-doc] after it has saved the object, so by the time your def save(..) runs, or your post_save signal [Django-doc], since at that time, there is no data yet, or the data is outdated.

You could work with a m2m_changed signal [Django-doc] and subscribe this on all ManyToManyFields, but I am not a fan of this either: if the data of a ProductVariation itself for example changes, than this could also have impact, without the Order, or the .product_variations changing.

I think it makes more sense to work with a @property [python-doc], or .annotate(…) [Django-doc] when you need to do this in bulk or to filter on the aggregate.

Keeping an "aggregate" in a model object is often not a good idea. The main problem is that a lot of scenarios can trigger the aggregate to get outdated, and catching all these cases is almost impossible. I summarized some problems with signals in this article [django-antipatterns].

I didnt find any good enough solution to actually save automatically computed calculating_special_order_items and calculating_floor_special_order_items, so for me it is enough that i have read only field with @property and my solution is:

class Order(models.Model):
STATUS_CHOICES = [("naruceno","naruceno"), ("poslato","poslato"), ("stiglo","stiglo")]
ordering_facility = models.ForeignKey(Facility, on_delete=models.PROTECT, related_name='order_create_facility')
dispatching_facility = models.ForeignKey(Facility, on_delete=models.PROTECT, related_name='order_dispatch_facility')
order_timestamp = models.DateTimeField(auto_now_add=True)
ship_timestamp = models.DateTimeField(blank=True, null=True)
confirm_timestamp = models.DateTimeField(blank=True, null=True)
real_timestamp = models.DateTimeField(blank=True, null=True)
order_user = models.CharField(max_length=100, blank=True)
ship_user = models.CharField(max_length=100, blank=True)
confirm_user = models.CharField(max_length=100, blank=True)
real_user = models.CharField(max_length=100, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_CHOICES[0][0])    
product_variations = models.ManyToManyField(ProductVariation, through='OrderItem')

order_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
ship_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
confirm_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
real_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
calculating_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)

order_items_order_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
order_items_ship_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
order_items_confirm_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
order_items_real_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
order_items_calculating_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)

ship_specials_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
confirm_specials_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
real_specials_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
calculating_specials_total = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)

possible_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='possible_special_order_items')
possible_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='possible_floor_special_order_items')
ship_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='ship_special_order_items')
ship_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='ship_floor_special_order_items')
confirm_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='confirm_special_order_items')
confirm_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='confirm_floor_special_order_items')
real_special_order_items = models.ManyToManyField(SpecialOrderItem, blank=True, related_name='real_special_order_items')
real_floor_special_order_items = models.ManyToManyField(FloorSpecialOrderItem, blank=True, related_name='real_floor_special_order_items')


billed = models.BooleanField(default=False)

    @property
def calculating_special_order_items(self):
    if self.real_timestamp is not None:
        return self.real_special_order_items.all()
    elif self.confirm_timestamp is not None:
        return self.confirm_special_order_items.all()
    elif self.ship_timestamp is not None:
        return self.ship_special_order_items.all()
    else:
        return SpecialOrderItem.objects.none()   



@property
def calculating_floor_special_order_items(self):
    if self.real_timestamp is not None:
        return self.real_floor_special_order_items.all()
    elif self.confirm_timestamp is not None:
        return self.confirm_floor_special_order_items.all()        
    elif self.ship_timestamp is not None:       
        return self.ship_floor_special_order_items.all()        
    else:
        return FloorSpecialOrderItem.objects.none()   

def update_order_item_total(self):
    self.order_items_order_total = self.orderitem_set.aggregate(models.Sum('order_subtotal'))['order_subtotal__sum']
    self.order_items_ship_total = self.orderitem_set.aggregate(models.Sum('ship_subtotal'))['ship_subtotal__sum']
    self.order_items_confirm_total = self.orderitem_set.aggregate(models.Sum('confirm_subtotal'))['confirm_subtotal__sum']
    self.order_items_real_total = self.orderitem_set.aggregate(models.Sum('real_subtotal'))['real_subtotal__sum'] 
    self.order_items_calculating_total = self.orderitem_set.aggregate(models.Sum('calculating_subtotal'))['calculating_subtotal__sum']

def update_specials_totals(self):
    ship_special_order_items_total = self.ship_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.ship_special_order_items.exists() else 0
    ship_floor_special_order_items_total = self.ship_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.ship_floor_special_order_items.exists() else 0
    self.ship_specials_total = ship_special_order_items_total + ship_floor_special_order_items_total
    confirm_special_order_items_total = self.confirm_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.confirm_special_order_items.exists() else 0
    confirm_floor_special_order_items_total = self.confirm_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.confirm_floor_special_order_items.exists() else 0
    self.confirm_specials_total = confirm_special_order_items_total + confirm_floor_special_order_items_total
    real_special_order_items_total = self.real_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.real_special_order_items.exists() else 0
    real_floor_special_order_items_total = self.real_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.real_floor_special_order_items.exists() else 0
    self.real_specials_total = real_special_order_items_total + real_floor_special_order_items_total
    calculating_special_order_items_total = self.calculating_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.calculating_special_order_items.aggregate(models.Sum('total'))['total__sum'] else 0
    calculating_floor_special_order_items_total = self.calculating_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] if self.calculating_floor_special_order_items.aggregate(models.Sum('total'))['total__sum'] else 0
    self.calculating_specials_total = calculating_special_order_items_total + calculating_floor_special_order_items_total


def update_total(self): 
    self.order_total = self.order_items_order_total
    self.ship_total = self.order_items_ship_total + self.ship_specials_total if self.ship_specials_total > 0 and self.order_items_ship_total is not None else self.ship_specials_total if self.order_items_ship_total is None else self.order_items_ship_total
    self.confirm_total = self.order_items_confirm_total + self.confirm_specials_total if self.confirm_specials_total > 0 and self.order_items_confirm_total is not None else self.confirm_specials_total if self.confirm_specials_total > 0 and self.order_items_confirm_total is None else self.order_items_confirm_total
    self.real_total = self.order_items_real_total + self.real_specials_total if self.real_specials_total > 0 and self.order_items_real_total is not None else  self.real_specials_total if self.real_specials_total > 0 and self.order_items_real_total is None else self.order_items_real_total
    self.calculating_total = self.order_items_calculating_total + self.calculating_specials_total if self.calculating_specials_total > 0 and self.order_items_calculating_total is not None else self.calculating_specials_total if self.calculating_specials_total>0 and self.order_items_calculating_total is None else self.order_items_calculating_total
    
    

def save(self, *args, **kwargs):
    
    super().save(*args, **kwargs)
    self.update_specials_totals()
    self.update_total()
    super().save()
    
    


    
def __str__(self):
    return f"{self.ordering_facility} {str(self.ship_timestamp)[0:19]} {self.calculating_total}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product_variation = models.ForeignKey(ProductVariation, on_delete=models.PROTECT)
order_quantity = models.PositiveIntegerField(blank=True, null=True)
ship_quantity = models.PositiveIntegerField(blank=True, null=True)
confirm_quantity = models.PositiveIntegerField(blank=True, null=True)
real_quantity = models.PositiveIntegerField(blank=True, null=True)
calculating_quantity = models.PositiveIntegerField(blank=True, null=True)
order_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
ship_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
confirm_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
real_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
calculating_subtotal = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)


@property
def calculate_calculating_quantity(self):
    if self.real_quantity is not None:
        return self.real_quantity
    elif self.confirm_quantity is not None:
        return self.confirm_quantity
    elif self.ship_quantity is not None:
        return self.ship_quantity
    elif self.order_quantity is not None:
        return self.order_quantity
    else:
        return 0



@property
def calculate_order_subtotal(self):
    return self.product_variation.price * self.order_quantity if self.order_quantity is not None else None

@property
def calculate_ship_subtotal(self):
    return self.product_variation.price * self.ship_quantity if self.ship_quantity is not None else None

@property
def calculate_confirm_subtotal(self):
    return self.product_variation.price * self.confirm_quantity if self.confirm_quantity is not None else None

@property
def calculate_real_subtotal(self):
    return self.product_variation.price * self.real_quantity if self.real_quantity is not None else None

@property
def calculate_calculating_subtotal(self):
    return self.product_variation.price * self.calculating_quantity if self.calculating_quantity is not None else None

def save(self, *args, **kwargs):
    self.order_subtotal = self.calculate_order_subtotal
    self.ship_subtotal = self.calculate_ship_subtotal
    self.confirm_subtotal = self.calculate_confirm_subtotal
    self.real_subtotal = self.calculate_real_subtotal
    self.calculating_quantity = self.calculate_calculating_quantity
    self.calculating_subtotal = self.calculate_calculating_subtotal
    super().save(*args, **kwargs)

    self.order.update_order_item_total()
Back to Top