Dynamically updating values of a field depending on the choice selected in another field in Django

Question:

I have two tables.
Inventory and Invoice.

InventoryModel:

from django.db import models

class Inventory(models.Model):
  product_number = models.IntegerField(primary_key=True)
  product = models.TextField(max_length=3000, default='', blank=True, null=True)
  title = models.CharField('Title', max_length=120, default='', blank=True, unique=True)
  amount = models.IntegerField('Unit Price', default=0, blank=True, null=True)

  def __str__(self):
    return self.title

InvoiceModel:

from django.db import models
from inventory.models import Inventory

class Invoice(models.Model):
invoice_number = models.IntegerField(blank=True, primary_key=True)

line_one = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+', verbose_name="Line 1", blank=True, default='', null=True)
line_one_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_one_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_one_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_two = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 2", blank=True, default='', null=True)
line_two_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_two_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_two_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_three = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 3", blank=True, default='', null=True)
line_three_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_three_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_three_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_four = models.ForeignKey(Inventory, on_delete=models.CASCADE,related_name='+',  verbose_name="Line 4", blank=True, default='', null=True)
line_four_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_four_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_four_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_five = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 5", blank=True, default='', null=True)
line_five_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_five_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_five_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_six = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 6", blank=True, default='', null=True)
line_six_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_six_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_six_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_seven = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 7", blank=True, default='', null=True)
line_seven_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_seven_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_seven_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_eight = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 8", blank=True, default='', null=True)
line_eight_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_eight_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_eight_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_nine = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 9", blank=True, default='', null=True)
line_nine_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_nine_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_nine_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

line_ten = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='+',  verbose_name="Line 10", blank=True, default='', null=True)
line_ten_quantity = models.IntegerField('Quantity', default=0, blank=True, null=True)
line_ten_unit_price = models.IntegerField('Unit Price(₹)', default=0, blank=True, null=True)
line_ten_total_price = models.IntegerField('Line Total(₹)', default=0, blank=True, null=True)

def __unicode__(self):
    return self.invoice_number

I have a form where the person selects an Inventory.title in the line_one. I want the line_one_unit_price to be automatically filled according to the option selected in the line_one. That is, I want the amount of that product to be displayed.

Screenshot of the forms

I tried using a JSON object and then sending it to the template.

views.py:

def add_invoice(request):
  form = InvoiceForm(request.POST or None)
  data = Inventory.objects.all()
  dict_obj = model_to_dict(data)
  serialized = json.dumps(dict_obj)
  total_invoices = Invoice.objects.count()
  queryset = Invoice.objects.order_by('-invoice_date')[:6]

  if form.is_valid():
    form.save()
    messages.success(request, 'Successfully Saved')
    return redirect('/invoice/list_invoice')
  context = {
    "form": form,
    "title": "New Invoice",
    "total_invoices": total_invoices,
    "queryset": queryset,
    "serialized":serialized,
  }
  return render(request, "entry.html", context)

I am not sure how I can access that JSON file in javascript? what will be the name of that JSON object? Is it possible to access that in JavaScript? How can I use it to update the price of an item in the forms dynamically?
Thanks.

EDIT:

I added this in the HTML file:

    {{ data|json_script:"hello-data" }}
<script type="text/javascript">
    const data = JSON.parse(document.getElementById('hello-data').textContent);

    document.getElementById('id_line_one').onchange = function(){
        document.getElementById('id_line_one_unit_price').value = data[this.value];
    };

</script>

views.py:

def add_invoice(request):
  form = InvoiceForm(request.POST or None)
  data = serializers.serialize("json", Inventory.objects.all())
  total_invoices = Invoice.objects.count()
  queryset = Invoice.objects.order_by('-invoice_date')[:6]

  if form.is_valid():
    form.save()
    messages.success(request, 'Successfully Saved')
    return redirect('/invoice/list_invoice')
  context = {
    "form": form,
    "title": "New Invoice",
    "total_invoices": total_invoices,
    "queryset": queryset,
    "data": data,
  }
  return render(request, "entry.html", context)

The output I get when I select an item is {?
I don’t know why?
screenshot

I guess it’s because the logic I used in the javascript is incorrect?

JSON File:

[{"model": "inventory.inventory", "pk": 1, "fields": {"product": "nice", "title": "Asus", "amount": 56000}}, {"model": "inventory.inventory", "pk": 2, "fields": {"product": "nice", "title": "Lenovo", "amount": 55000}}]

Edit2:

I added this in the script:

<script type="text/javascript">
    const data = JSON.parse(document.getElementById('hello-data').textContent);

    document.getElementById('id_line_one').onchange = function(){
        var line_one=document.getElementById('id_line_one').value;
        document.getElementById('id_line_one_unit_price').value = data.fields[line_one].amount;
    };

</script>

And now I am getting an Uncaught TypeError: Cannot read properties of undefined (reading '1') error. Whenever I select the object from the dropdown, It’s value is stored from 1, etc. So the first object in the list returns it’s primary key, i.e., 1.
I think I am accessing the fields in a wrong way.

Asked By: perpetualdarkness

||

Answers:

when you pass the dict_obj to your template you can get the json with

{{ dict_obj|json_script:"hello-data" }}

const value = JSON.parse(document.getElementById('hello-data').textContent);

https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#json-script

change the value depending on the selected item

const data = {
    "apple": 2.3,
    "orange": 1.7,
    
}

document.getElementById('inventory_item').onchange = function() {
    document.getElementById('price').value = data[this.value];
};

<select name="InventoryDropdown" id="inventory_item">
<option value="" selected disabled>---</option>
<option value="apple">apple</option>
<option value="orange">orange</option>
</select>
<input type="text" id="price" >

Answered By: David Wenzel

if i get it right you get the pk of inventory in this line

var line_one=document.getElementById('id_line_one').value

so you need to iterate over the json, to find it – i tested it with your printed JSON File

function getDictEntryByPk(dict_list, pk) {
   for (let i=0; i < dict_list.length; i++) {
       if (dict_list[i].pk == pk) {
           return dict_list[i]
       }
   }
}

and use it like that:

var line_one=document.getElementById('id_line_one').value;
var line_one_data = getDictEntryByPk(data, line_one);
document.getElementById('id_line_one_unit_price').value = line_one_data.fields.amount

EDIT:
same with less code:

function getDictEntryByPk(dict_list, pk) {
    return dict_list.find(element => element.pk == pk);
}
Answered By: FabianClemenz

An easy alternative would be to redefine your Inventory __str__ method to include the unit price, so the drop down becomes e.g. ‘Inventory #1 – ₹500’. You don’t then need to worry about JS, JSON, or updating the amount. The total amount can be refreshed whenever the form is submitted with the quantity. Otherwise you need JS to get the unit price and to calculate the total price with the quantity.

As an aside, your models have lines manually added to the Invoice – i.e. line_one, line_two, etc. – this isn’t a good way of structuring your models or data. You should add an intermediary model called ‘InvoiceLine’ or something similar that can handle any numbers of invoice lines and connects with FKs between your Inventory and Invoice models. The reason why this is particularly important is because at the moment all of your lines have models.CASCADE as the on_delete option, meaning that an Invoice with ten lines could be completely deleted even if just one of your FK-relation Inventory models is deleted!

Answered By: 0sVoid

I figured out how to achieve this functionality. I wish I could give the credits to a single person but it’s really a combination of many people’s answers.

in the views.py which returns the HTML page where I want this functionality, I wrote this code, It returns the Product objects to the HTML file:

model_data=Inventory.objects.values_list('product_number','amount')
data=[model for model in model_data.values()]
context = {
    "data": data,
}
return render(request, "entry.html", context)

In the entry.html, I access the data using: {{ data|json_script:"hello-data" }}

Then the data I get is:

[{"product_number": 1, "product": "Laptop", "title": "Lenovo", "amount": 50000}, {"product_number": 2, "product": "Single Table Tennis Ball", "title": "Table Tennis Ball Share", "amount": 4}]

In the entry.html I used some javascript:

<script type="text/javascript">
    var data = JSON.parse(document.getElementById('hello-data').textContent);
    document.getElementById('id_line_one').onchange = function(event){
        var data1 = data.find(({product_number}) => product_number == event.target.value);
        document.getElementById('id_line_one_unit_price').value = data1 && data1.amount ? data1.amount : 0;
};

So, I parse the JSON data, and then when the user clicks the drop-down menu whose id is id_line_one, I check if the returned number(i.e., product_number) matches with any product_number present in the variable data. and then I change the amount of that respective field.

Referrence: https://stackoverflow.com/a/72923853/9350154

Answered By: perpetualdarkness