Search code examples
pythondjangoerpinvoice

What is the "Best Practice" for invoice calculations in Django 1.10+?


If one has two models:

class Invoice(models.Model):
    class Meta:
        ordering = ('-date_added', )

    number = models.CharField(max_length=10,)
    comments = models.TextField(blank=True, help_text="Notes about this invoice." )
    total = models.DecimalField(max_digits=9, decimal_places=2, default="0" )
    date_added = models.DateTimeField(_('date added'), auto_now_add=True)
    date_modified = models.DateTimeField(_('date modified'), auto_now=True)

def __unicode__(self):
    return "%s: total %s" % (self.number, self.total)

class Part(models.Model):
    for_invoice = models.ForeignKey(Invoice)
    description = models.CharField(max_length=200, blank=True, help_text=_("Briefly describe the part.") )
    supplier = models.CharField(max_length=100, blank=True, help_text=_("Supplier Name.") )
    supplier_number = models.CharField(max_length=100, blank=False, help_text=_("Supplier's order number.") )
    qty = models.DecimalField(max_digits=3, decimal_places=0, blank=False, null=False, help_text=_("How many are needed?") )
    cost = models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True, help_text=_("Price paid per unit") )
    line_total = models.DecimalField(max_digits=9, decimal_places=2, default="0")
    date_added = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

def __unicode__(self):
    return "%s: total %s" % (self.for_invoice, self.line_total)

First choice I see is one could implement "line_total" or "total" as a calculated model field. But if you do that, then you cannot ever sort the change list by "line_total" or "total", and the users want to be able to do that. So I made them a saved field on the model.

Reading the Django 1.10 docs I see 4 places where code can be defined to calculate and update the "total" and "line_total" fields:

  1. ModelAdmin.save_model(request, obj, form, change) and ModelAdmin.save_formset(request, form, formset, change)
  2. ModelAdmin.save_related(request, form, formsets, change)
  3. model.save(self, *args, **kwargs))
  4. Save Signal

It seems to me that the Save Signal is too low-level - tied to a single model save event, and it will never have access to the request or session. So it would be "annoying" to try and round up all the "Part" records?

Similarly, it seems the model.save method would be also be too granular. It would be "handy" if I could use the Invoice.save method since that has easy access to all the associated parts through Invoice.part_set.all(), looping over that queryset would be an easy way to update line_total, then the main total. But again, would all the new/changed Part records be saved at this point? And it would not have access to the HttpRequest.

I think the same applies to the Admin's save_model. However, I have a vague memory that its possible to ensure the related parts are saved first. I list the parts using an inline on the Invoice add/edit page in Admin.

I just added in ModelAdmin.save_related! I forgot about that one. Since the main invoice would be saved at this point, maybe this is the best place to update all the Part records, then update the parent total?

Thanks in advance!


Solution

  • Thanks to Alasdair's suggestion, but, this client does not want a full custom solution. So I am sticking to Admin tweaks.

    I am testing using option 2: save_related function of the Admin. Code below:

    from decimal import Decimal
    
      def save_related(self, request, form, formsets, change):
        total = Decimal('0.00')
        for myform in formsets:
          for mf in myform.forms:
            # Skip empty rows (if any):
            if len(mf.cleaned_data) > 0:
              # Update item line total from item quantity and price:
              mf.instance.line_total = mf.instance.qty * mf.instance.cost
              # Add line item total to grand total:
              total += mf.instance.line_total
              # re-save line item:
              mf.instance.save()
        # Update grand total on the invoice:
        form.instance.total = total
        # re-save the invoice record:
        form.instance.save()
        # Chain to inherited code:
        super(InvoiceAdmin, self).save_related(request, form, formsets, change)
    

    Sorry for the poor variable names. I was caught off guard by the inline forms being 2 levels deep. I guess the first layer is a group of formsets (only one), and inside the first formset are my forms. Everything has .instance objects, which I think indicates they have already been saved. A surprise since I thought I read that the default action of this function is to save the formsets (the main form is already saved).

    Whatever, it works so I'll assume I misunderstood the docs.