Validating Parent model by accessing Children models

Take the folllowing simplified models

class Bill(models.Model):
    name = models.CharField(max_length=100)
    total_amount = models.DecimalField()

class Payment(models.Model):
    service = models.ForeignKey(Bill, on_delete=models.CASCADE, related_name='payments')
    amount = models.IntegerField()

It is essential for retaining data integrity that the sum of a Bill’s Payments never exceed the BIlls’ total amount.

I tried to implement this validation rule within the Bill clean method:

class Bill(models.Model):
    name = models.CharField(max_length=100)
    total_amount = models.DecimalField()

   def clean(self):
     super.clean()
     if sum([p.amount for p in self.payments.all()]) > self.total_amount:
        raise ValidationError("Payments exceed bill amount")

But I am having trouble since the set of related Payments will not always be accessible to the Bill instance if the Payment instances have not been commited to the database. (for example, when creating both Bill and Payments from a BillAdmin with PaymentInlines).

I found that I could perform this validation using a FormSet, but if I ever forget to use this FormSet the data integrity could be compromised.
Also I found that I could use signals to trigger validation after the objects have been commited to the database, but I would have to deal with deleting the objects later if they are not valid.

I feel like this is a very vanilla use case yet I am struggling to find a simple solution.
What am I missing here? Is it a wrong approach to try to implement this kind of validation within the Bill model?

Welcome @amez !

Yes, because changes can be made to Payment that don’t affect Bill at all. (e.g. Adding a new Payment.) This is something that should be done associated with the Payment model.

If it is absolutely critical that this constraint is never violated, then your only guaranteed solution is something done on the database level, such as a trigger or a stored procedure. All application-level protections can be bypassed one way or another.

I think you need to fix views.py.

I don’t know how you want to implement it, but it seems like a simple thing to do:
1.create all the payments,
2.and compare them to bill.total_amount,
3.and then save the two models.

When using form, form.save(commit=false),
If not used, you can use the data without saving it to the db through model(**kwargs).

@KenWhitesell @white-seolpyo Thanks for the quick replies!

Yeah, the Payment model looks like the best place to the trigger the total_amount validation.

Turns out I got a little bit tripped with the default flow of calls that the Django Admin performs the save and validation calls. Long story short, in some cases I was trying to call bill.payments.allI() before actually assigning an id to the bill instances, which was causing an error.

At the end I implemented the validation on the Bill model and triggered it on Payment save events. Ideally the Bill and Payment save operations are ran under a single transaction so I do not have to deal with weird edge cases.

Thanks a lot again, I am closing the issue :smiley: