Totaling inside a cart model the subtotal of each TabularInline entry in Django Admin

2.4k Views Asked by At

Not sure if this is possible, but in my Cart (model class) detail view in Django Admin, I'd like to total up all of my TabularInline Entry model class subtotals, and save that value to my Cart's total_price attribute. Is there a way to do this or somehow use a filter or form widget to add all the subtotals together for the same purpose? Thanks in advance for any help!

Below is an example of what I'd like to do. You can see in the total price field I manually entered 130 (subtotal's: 90 + 20 + 20 = 130). I'd like this to be calculated automatically each time an entry is added from inventory, and its quantity edited.

enter image description here

So far in my admin.py I have a TabularInline admin class that returns the subtotal for each entry by multiplying its quantity by its respective price. Then my CartAdmin class displays that inline relationship inside the cart model detail view.

admin.py

class EntryInline(admin.TabularInline):
    model = Entry
    extra = 0
    readonly_fields = ['subtotal']
    exclude = ['price']
    def subtotal(self, obj):
        return "$" + str(obj.quantity * obj.inventory.price)


class CartAdmin(admin.ModelAdmin):
    inlines = [EntryInline]
    fieldsets = (
        (None, {
            'fields':('user', 'total_price')
            }),
    )

models.py

class Inventory(models.Model):
    quantity        = models.IntegerField()
    price           = models.DecimalField(max_digits=5, decimal_places=2)


class Cart(models.Model):
    user            = models.OneToOneField(User)
    total_price     = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True)


class Entry(models.Model):
    cart            = models.ForeignKey(Cart, related_name="entries")
    inventory       = models.ForeignKey(Inventory, related_name="entries")
    quantity        = models.PositiveSmallIntegerField(default=1)
    price           = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True)
3

There are 3 best solutions below

8
On

You can try fetching the total_price in cart admin and populating the field as:

class CartAdmin(admin.ModelAdmin):
    inlines = [EntryInline]
    fieldsets = (
    (None, {
        'fields':('user', 'total_price')
        }),
    )

    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        # check if the cart object exists
        if obj:
            try:
                _price = Entry.objects.filter(cart=obj).aggregate(sum=Sum(F('quantity')*F('inventory__price'), output_field=FloatField()))
                total = _price['sum']
                obj.total_price = total
            except:
                pass
    return form

Regarding your Import error , import F and Sum as:

from django.db.models import Sum, F

Or if you want more dynamic control, so that whenever a user edits the quantity in the entry inline, the total_price should update automatically you can write custom javascript for it.

Hope it helps.

0
On

Welp, I'm still working on a way to refresh the browser making an AJAX request when I edit the quantity in admin detail view to see the updated changes to the cart's total price server-side. Not sure how long that will take, but I'll update this answer as soon as I figure it out.

In the meantime, here's how I was able to get the subtotals and total:


In models.py I added a the field 'subtotal' to my Entry model:

subtotal = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)

In admin.py:

class EntryInline(admin.TabularInline):
    model = Entry
    extra = 0
    # calls my helper method
    readonly_fields = ['get_subtotal']
    # exclude redundant field being replaced by helper method
    exclude = ['subtotal']


    # gets subtotal of each entry
    def get_subtotal(self, obj):
        # for a given entry, find that id
        entry = Entry.objects.get(id=obj.id)
        # subtotal the entry quantity * its related price
        entry_subtotal = obj.quantity * obj.inventory.price
        # check if entry obj has a subtotal and try to save to db
        if entry.subtotal != entry_subtotal:
            try:
                entry.subtotal = entry_subtotal
                entry.save()
            except DecimalException as e:
                print(e)
        # return a str representation of entry subtotal
        return "$" + str(entry_subtotal)
    # set helper method's description display
    get_subtotal.short_description = 'subtotal'


# initialization of variable
total_price = 0

class CartAdmin(admin.ModelAdmin):
    model = Cart
    inlines = [EntryInline]
    # calls get_total helper method below
    readonly_fields = ['get_total']
    exclude = ['total_price']


    def get_total(self, obj):
        # extend scope of variable
        global total_price
        # get all entries for the given cart
        entries = Entry.objects.filter(cart=Cart.objects.get(id=obj.id))
        # iterate through entries
        for entry in entries:
            # if entry obj has a subtotal add it to total_price var
            if entry.subtotal:
                total_price += entry.subtotal
        print(obj.total_price)
        # assign cart obj's total_price field to total_price var
        obj.total_price = total_price
        # save to db
        obj.save()
        # reset total_price var
        total_price = 0
        # return cart's total price to be displayed
        return obj.total_price
    # give the helper method a description to be displayed
    get_total.short_description = 'total'

**One thing to note is that the subtotals load dynamically when I edit the quantity and save because it's using a helper method. I still have to refresh the browser one more time to save to the database, however, the display is still there. I'm not sure why get_total() isn't working the same way; there's no display AND I have to refresh the browser to save to the database. Logic seems inconsistent...

0
On

Below is what I used to refresh the page after I save in admin to update the total:

cart.js

if (!$) {
    $ = django.jQuery;
 }
function addSecs(d, s) {
    return new Date(d.valueOf() + s * 1000);
}
function doRun() {
    document.getElementById("msg").innerHTML = "Processing JS...";
    setTimeout(function() {
        start = new Date();
        end = addSecs(start, 5);
        do {
            start = new Date();
        } while (end - start > 0);
        document.getElementById("msg").innerHTML = "Finished Processing";
    }, 10);
 }
$(function() {
    $(".change_form_save").click(doRun);

    if (window.localStorage) {
        if (!localStorage.getItem("firstLoad")) {
            localStorage["firstLoad"] = true;
            window.location.reload();
        } else localStorage.removeItem("firstLoad");
    }
});

Then in my admin.py under my class for CartAdmin(admin.ModelAdmin):

class Media:
    js = ('js/cart.js',)