4

I have the class Invoice, which (simplified) has the following attributes:

class Invoice(models.Model)
    number = models.CharField(verbose_name="Number", max_length=16)
    issue_date = models.DateTimeField(verbose_name="Issue date", default=datetime.now)
    total = models.FloatField(verbose_name="Total", blank=True, null=True)

And then, I have the class InvoiceLine, which represents the line/lines that a invoice can have:

class InvoiceLine(models.Model):
    invoice = models.ForeignKey(Invoice, verbose_name="Invoice")
    description = models.CharField(verbose_name="Description", max_length=64)
    line_total = models.FloatField(verbose_name="Line total")

InvoiceLine is an inline of the Invoice, and what I want to achieve is that, when in the admin somebody save the invoice with its lines (one ore more) the total of the invoice is calculated. I've tried to do it by overriding the method save:

class Invoice(models.Model)
    number = models.CharField(verbose_name="Number", max_length=16)
    issue_date = models.DateTimeField(verbose_name="Issue date", default=datetime.now)
    total = models.FloatField(verbose_name="Total", blank=True, null=True)

    def save(self, *args, **kwargs):
         invoice_lines = InvoiceLine.objects.filter(invoice=self.id)
         self.total = 0
         for line in invoice_lines:
             self.total=self.total+line.line_total
         super(Invoice, self).save(*args, **kwargs)

The problem is that when I add elements in the InvoiceLine, the first time I save and the functionsave is called, the new elements in the inline (InvoiceLine) aren't stored yet, so when I do InvoiceLine.objects.filter(invoice=self.id) , they are not taken into account. So, the only way this works is saving twice. I've also tried:

def save(self, *args, **kwargs):
    super(Invoice, self).save(*args, **kwargs)
    invoice_lines = InvoiceLine.objects.filter(invoice=self.pk)
    self.total = 0
    for line in invoice_lines:
        self.total=self.total+line.line_total
    super(Invoice, self).save(*args, **kwargs)

But has the same result. Any idea? Thanks in advance!

bcap
  • 500
  • 5
  • 14

3 Answers3

9

Finally I've found it on a post Change object after saving all inlines in Django Admin that has helped me a lot. The key is in admin.py where I already had my class InvoiceHeaderAdmin(admin.ModelAdmin), but I have had to put three functions in order to modify the total attribute after all the inlines has been saved: this way, the query invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk) that didn't work well before, now it works perfect. Tue function InvoiceHeaderAdmin has been as follows:

class InvoiceHeaderAdmin(admin.ModelAdmin):
    inlines = [InvoiceLineInline]
    list_filter = ('format_line','issue_date',)
    list_display = ('number','organization','issue_date','total',)
    fields = ('format_line','organization','issue_date',)

    #the following functions are for calculating the total price of the invoice header based on the lines
    def response_add(self, request, new_object):
        obj = self.after_saving_model_and_related_inlines(new_object)
        return super(InvoiceHeaderAdmin, self).response_add(request, obj)

    def response_change(self, request, obj):
        obj = self.after_saving_model_and_related_inlines(obj)
        return super(InvoiceHeaderAdmin, self).response_change(request, obj)

    def after_saving_model_and_related_inlines(self, obj):

        invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk)

        obj.total = 0
        for line in invoice_lines:
            obj.total=obj.total+line.line_total
        obj.save()
        return obj

The clue is the last function, where all the inlines have been saved and now I can calculate the attribute from object (invoice) and modify it.

bcap
  • 500
  • 5
  • 14
  • 1
    You may also override the method [`save_related`](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_related) – Duarte Fernandes Aug 22 '18 at 21:05
1

In order to keep the total up-to-date, I would perform an aggregation of the line_total's when needed. This removes database redundancy, and further prevents any inaccuracies that can be created by multiple people updating invoice lines at the same time.

class Invoice(models.Model)
    @property
    def total(self):
        result = self.invoiceline_set.aggregate(total=Sum('line_total'))
        return result['total']

Explanation:

Given an Invoice instance, you can follow the one to many relationship created by the ForeignKey of InvoiceLine by accessing 'invoiceline_set'. This is actually equivalent to InvoiceLine.filter(invoice_id=self.pk).

When you have a set of rows, you can preform database aggregations like SUM, AVG, COUNT on them without pulling the objects out of the database to do so, which is often much faster.

The property decorator will make this method act as a read-only attribute. If you intend to call the total property multiple times on the same instance, you should actually use @cached_property so it is only calculated one time.

So when you type invoice.total a query will be made to the database which will filter all of the InvoiceLine rows that belong in the relationship defined by the ForeignKey, preform the SUM calculation of the line_total column, and return that value to you as a key,value: eg/ ['total': 1234]

Community
  • 1
  • 1
Mark Galloway
  • 4,050
  • 19
  • 26
  • What does `self.invoiceline_set.aggregate(total=Sum('line_total'))` exactly do? (I'm quite new in Django and Python). And a property is called like a method, right? – bcap Jul 15 '15 at 07:30
0

just some update for other folks who happen to land here: the article you linked to stated that there's a method introduced here that we could override and get the same result. Using your example:

def save_related(self, request, form, formsets, change):
    super(AjudaCustoProjetoAdmin, self).save_related(request, form, formsets, change)
    obj = form.instance
    invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk)

    obj.total = 0
    for line in invoice_lines:
        obj.total=obj.total+line.line_total
    obj.save()