2

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)
Devin B.
  • 433
  • 4
  • 18

3 Answers3

1

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.

not2acoder
  • 1,142
  • 11
  • 17
  • That sounds awesome! Dynamic control is exactly what I'd like to have eventually. I'm still a bit new to Django though. I put the above code inside the cart admin and used a few print statements to try and better understand the code and found out the try block wasn't getting run. So I figured there was no obj and took the code out of the try block. Got an error saying Sum wasn't defined, so I lowered the case of the S and tried again. Got an error that 'F' isn't defined. Is this short for 'form' or something that's built-in or do I have to import the function? Thanks again for your help! – Devin B. Oct 03 '18 at 21:44
  • I appreciate the quick response, thank you! So I tried it again and couldn't get it to work. You gave me an idea however, and I found a work around which I'll post below. The only problem is that after I change the quantity and save, I have to refresh the admin detail view one more time to see the changes for the total price to kick in. I'm trying to figure out the next piece that you foresaw and thats using JavaScript to refresh the browser by making an AJAX request. That was what you meant by dynamic control right? If you could help shed more light on this I'd appreciate it. Cheers. – Devin B. Oct 04 '18 at 20:49
  • 1
    You can fetch the subtotal via ajax request, so that whenever you add a new inventory object in the entry inline, the subtotal gets populated. After this you can easily calculate total price by fetching all the subtotals and adding them. This can easily be done via javascript. Hope it helps! – not2acoder Oct 05 '18 at 05:28
  • Hmm how do I do that within the Django Admin? I figured out how to do it with Django views, but admin seems like a different beast altogether. Can you give me an example por favor? – Devin B. Oct 05 '18 at 20:32
  • Have you written the Js for this I need this kind of functionality and I can't proper javascript this is a nice idea. – Martins Oct 29 '18 at 19:40
  • Not exactly. What I wrote in JS, was code to refresh one more time after I save in admin to update the total. Not the cleanest solution, but it appears to work for me. Check below for my updated code. – Devin B. Oct 30 '18 at 05:12
  • where did you put the Js – Martins Oct 30 '18 at 13:12
  • In 'app_name'/static/js – Devin B. Nov 01 '18 at 21:45
0

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...

Devin B.
  • 433
  • 4
  • 18
0

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',)
Devin B.
  • 433
  • 4
  • 18