SOLVED: Django Admin TabularInline FormSet Model Override

Well, that is quite a confusing post title, isn't it? What the heck does all of that mean?

I have been working on a multi-location inventory management system that requires widgets to move back and forth between multiple locations, receives items into inventory, ship items to customers, return items to multiple vendors and keep track of all of the inventory totals across all locations, including what is currently on order and what items are in transit.

I ended up creating 3 models that are important to this post. There are a couple of other models you'll see in the Gist, ItemCategory, ItemGroup, Vendor, etc.. however, those are not relevant to this topic.

  • Item
  • Location
  • ItemLocation

I'll use these 3 models to build out the initial Django admin interface.

Now that the models look good, I start by creating the ItemLocationInLine in admin.py

class ItemLocationInline(admin.TabularInline):  
    model = ItemLocation
    formset = ItemLocationInlineFormset
    verbose_name_plural = 'Items By Location'
    extra = 1
    can_delete = True
    show_change_link = True

Next, I set up the BaseInlineFormSet in admin.py...

class ItemLocationInlineFormset(BaseInlineFormSet):  
    def save_new(self, form, commit=True):
        return super(ItemLocationInlineFormset, self).save_new(form, commit=commit)

    def save_existing(self, form, instance, commit=True):
        return form.save(commit=commit)

This is looking pretty good so far.

Next, I define the properties for the ItemAdmin by inheriting from admin.ModelAdmin.

As pointed out by Wacky Galang from Disqus, I did not include as part of this original post, the Items' module forms.py. Thanks for the question.

OK, now for the special sauce...

When I save or update my Item, I want to calculate the totals by location for each of these fields:

* total_qty_on_hand
* total_qty_on_order 
* total_qty_in_transit

Since my location inventory totals are part of the ItemLocation model, I have created an inline form that will display a very nicely formatted inline tabular form for all of my active Locations within my Item instance.

Items By Location

alt-text

When I click Save, I need to SUM the totals and save it back to the parent model.

Example. I have an Item - Widget 1, with inventory in 3 locations. If I have 5 widgets in 3 locations, my total inventory on hand is 15. That is the total quantity that should be saved to the Item instance.

This is achieved in two different ways.

The first is a model override in which I create a function to update the Item model's field total inventory.

 def update_totals(self):
        total_qty_on_hand = 0
        total_qty_on_order = 0
        total_qty_in_transit = 0

        for item in self.itemlocation_set.all():
            total_qty_on_hand += item.qty_on_hand
            total_qty_on_order += item.qty_on_order
            total_qty_in_transit += item.qty_in_transit
            self.total_qty_on_hand = total_qty_on_hand
            self.total_qty_on_order = total_qty_on_order
            self.total_qty_in_transit = total_qty_in_transit
            self.save()

The important part of this function is:

for item in self.itemlocation_set.all():  

This query passes in the Item instance's PK and returns a queryset in which I have access to all of the ItemLocation's quantities in order to sum the totals.

The other important part of this workflow is the save__formset override:

# override the save_formset method and update the total qty's
    def save_formset(self, request, form, formset, change):
        instances = formset.save(commit=False)
        for instance in instances:
            instance.save()
            instance.item.update_totals()
        formset.save_m2m()

Here, for each form instance, I am calling the Item models's update_totals() function in the save_formset override before saving the formset.

This works perfectly and each subsequent save on an Item instance updates the Item's fields for total quantities.

Wrapping Up

Everything you want to do in Django that is outside the scope of Django's default behavior is done by overriding a model or formset. This is part of the standard workflow for Django and makes extending Django much easier. It may be difficult to visualize the complexities of the underlying functions, but once you understand this, the concepts become much clearer.

Craig Derington

Espousing the virtues of Secular Humanism, Libertarianism, Free and Open Source Software, Linux, Ubuntu, Terminal Multiplexing, Docker, Python, Flask, Django, Go, MySQL, PostgreSQL, MongoDB and Git.

comments powered by Disqus