Django forms

I am trying to get user input for specific fields in a table and submit several rows at the same time, how can i get the user input and store it in teh back end with one submit button.

Each row in this image represents a different item in the database.

this is my model.py

class Item(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    # name = models.CharField(max_length=100)
    description = models.TextField()
    expiryDate = models.DateField(null=True, blank=True)
    quantity = models.IntegerField()
    category_choices = (
    ('مستوي واحد','مستوي واحد'),
    ('مُجْمِعٌ','مُجْمِعٌ'),
    )
    category = models.CharField(
        max_length=15,
        choices=category_choices
    )
    unit_choices = (
    ('صندوق','صندوق'),
    ('متر','متر'),
    ('عدد','عدد'),
    )
    unit = models.CharField(
        max_length=15,
        choices=unit_choices
    )

    def __str__(self):
        return str(self.uuid)

i am trying to create a warehouse system where the user can input multiple items in the database at once.

The facility you are looking for here are Django formsets. From the first paragraph on that page:

A formset is a layer of abstraction to work with multiple forms on the same page. It can be best compared to a data grid.

1 Like

I have tried it but i got this error also to give you full context
I am not using this model directly, i hava another model called “PO” for the purchase order, then another one called “poItem” for each item in the “PO” i am creating the po then adding items to it.

class po(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    number = models.IntegerField(auto_created=True)
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
    warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
    completed = models.BooleanField(default=False)
    po_created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return str(self.uuid)
class poItem(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='items1')
    po = models.ForeignKey(po, on_delete=models.CASCADE, related_name='poitems')
    required_quantity = models.IntegerField()
    added_quantity = models.IntegerField(null=True, blank=True)
    expiryDate = models.DateField(null=True, blank=True)

    def __repr__(self):
        return str(self.uuid)

then in my views.py

from purchase.models import poItem, po
from django.forms import modelform_factory

def inflowA2(request, pk):
    page = "inflowA2"

    employee = Employee.objects.get(uuid = request.user.uuid)

    today = date.today()

    orderNo = po.objects.get(number = pk)

    orderItems = orderNo.poitems.all()

    inflowFormSet = modelform_factory(poItem, fields = ['expiryDate', 'added_quantity'])

    if request.method == "POST":
        form = inflowFormSet(request.POST)
        form.save(commit=False)
        
        for instances in form:
            instances.save()
        return HttpResponseRedirect (reverse_lazy ("warehouse:itemLocation", args=[pk]))
        
    form = inflowFormSet()
    
    context = {
        'page': page,
        'employee': employee,
        'date': today,
        'po': orderNo,
        'items':orderItems,
        'form': form
        }
    
    return render(request, "forms/warehouse/inflow/a2.html", context)

now i am getting this error

This does not create a formset.

From the docs, this line:
AuthorFormSet = modelformset_factory(Author, fields=["name", "title"])
creates a ModelFormset class. (Also note that it’s modelformset_factory and not modelform_factory.)

Then, once you’ve created the ModelFormset class, you still need to create the instance of that formset, such as with formset = AuthorFormSet()

Now, beyond this basic case, since you’re dealing with a “parent/child” relationship between classes, you’re going to want to assign the po field of poItem to the related po.

In this situation, you way want to look at Inline formsets as the best way to have Django assist you with that process.

Side note: I would strongly encourage you to adopt the Python/Django naming conventions for class, field, and method/function names. Not only is it going to help others when looking at your code, but it will actually help you with your comprehension of the docs when you are reading them.

i tried the inlineformset_factory but now i have two problems the first is that its looping over it twice as shoen below and when i click on continue i don’t get any error but the data isn’t saved in the database.

def inflowA2(request, pk):
    page = "inflowA2"

    employee = Employee.objects.get(uuid = request.user.uuid)

    today = date.today()

    ItemFormSet = inlineformset_factory(po, poItem, fields= ['added_quantity', 'expiryDate'])
    orderNo = po.objects.get(number = pk)
    formset = ItemFormSet(instance=orderNo)

        items = orderNo.poitems.all()

    if request.method == "POST":
        formset = ItemFormSet(request.POST, instance=orderNo)
        if formset.is_valid():
            formset.save(commit=False)
            for item, form in zip(items, formset):
                # Update the item quantity
                item.item.quantity += form.instance.added_quantity
                print(item.item.quantity)
                print(form.instance.added_quantity)
                item.save()
                form.save()
            formset.save()
            messages.success(request, 'Transactions added to portfolio')

        else:
            print('ERROR', formset.errors)
            messages.error(request,'ERROR')

        return HttpResponseRedirect (reverse_lazy ("warehouse:itemLocation", args=[pk]))
    
    new = tuple(zip_longest(items, formset))
    
    context = {
        'page': page,
        'employee': employee,
        'date': today,
        'po': orderNo,
        'form': formset,
        'item':items,
        'new':new
        }
    
    return render(request, "forms/warehouse/inflow/a2.html", context)

a2.html

<form action="{% url "inflowA2" po.number %}" method="post">
                    {% csrf_token %}
                            <tbody>
                                {% for x in new %}
                                    <tr class="table-text position">
                                        <td >{{x.0.item.description}}</td>
                                        <td>{{x.0.item.category}}</td>
                                        {% if x.0.item.category == "مُجْمِعٌ" %}
                                                <td>{{x.1.expiryDate}}</td>
                                        {% else %}
                                            <td>-</td>
                                        {% endif %}
                                        <td>{{x.0.item.unit}}</td>
                                        <td>{{x.0.required_quantity}}</td>
                                        <td>{{x.0.item.quantity}}</td>
                                        <td>{{x.1.added_quantity}}</td>
                                    </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                        
                    </div>  
                    
                    <div>
                        <button class="button button-search">تأكيد</button>
                    </div>
                </form>

I fixed the iteration duplication issue by changing zip_longest to zip, but still not able to save the form data to the db

new = tuple(zip(items, formset))

I am getting this error in console

ERROR [ ]
[19/Oct/2023 22:12:11] “POST /PO-Detail/1/ HTTP/1.1” 302 0
[19/Oct/2023 22:12:11] “GET /warehouse/itemLocation/1/ HTTP/1.1” 200 6336
[19/Oct/2023 22:12:11] “GET /static/js/fontawesome/all.js HTTP/1.1” 404 1822

I also updated my form to use a dete picker in case the date format was wrong.

forms.py

class DateInput(forms.DateInput):
    input_type = 'date'

class inflowForm(forms.ModelForm):
    class Meta:
        model = poItem
        fields = ['expiryDate', 'added_quantity']

    expiryDate =  forms.DateField(
        widget=DateInput()
        )
    
    added_quantity = forms.IntegerField(
        widget= forms.NumberInput()
    )

updated views.py

ItemFormSet = inlineformset_factory(po, poItem, form=inflowForm, fields= ['added_quantity', 'expiryDate'])

I am trying to get specific formset error so i updated my code to this

    if request.method == "POST":
        formset = ItemFormSet(request.POST, instance=orderNo)
        if formset.is_valid():
            formset.save(commit=False)
            for item, form in zip(items, formset):
                # Update the item quantity
                item.item.quantity += form.instance.added_quantity
                item.item.save()
                item.save()
                form.save()

            # Save the formset
            formset.save(commit=True)
            messages.success(request, 'Transactions added to portfolio')

        else:
            for i, form in enumerate(formset):
                print(f'Errors for form {i + 1}: {form.errors}')
            messages.error(request, 'Formset has errors')
            # The for loop above doesn't return any errors
            print('ERROR', formset.errors)   # this line returns empty list [ ]

I’ll admit, I’m having a difficult time understanding what you’re trying to do here with the code that you’ve posted. Specifically, I don’t understand why you’re trying to iterate over two sequences in your process.

If I understand this correctly, formset is a list of forms for objects of type poItem.

When you create the instance of the formset, then you should be getting one form for each instance of poItem that is related to orderNo.

What I don’t understand is why you’re creating a list of poItem named items and trying to iterate with it along with the forms.

Can you clarify what your intent is here?

What I don’t understand is why you’re also creating

i am iterating through item to be able to display the data on the template because formset is not displaying any data it only returns the fields i want the user to input [‘added_quantity’, ‘expiryDate’]

If they’re all fields on the same model, then you can include them in the form, but set them as disabled. That will still allow you to display them on the page but they won’t be editable fields.

ok
these are the models i am trying to use i query them with po number which is passed as pk in the url

class po(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    number = models.IntegerField(auto_created=True)
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
    warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
    completed = models.BooleanField(default=False)
    po_created_at = models.DateTimeField(auto_now_add=True)

    class meta:
        ordering = ['number']

    def __str__(self):
        return str(self.uuid)
    
class poItem(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='items1')
    po = models.ForeignKey(po, on_delete=models.CASCADE, related_name='poitems')
    required_quantity = models.IntegerField()
    added_quantity = models.IntegerField(null=True, blank=True)
    expiryDate = models.DateField(null=True, blank=True)

    class meta:
        ordering = ['item']

    def __repr__(self):
        return str(self.uuid)

i want to allow the user to input

added_quantity = models.IntegerField(null=True, blank=True)
expiryDate = models.DateField(null=True, blank=True)

while the rest of the values in poItem be displayed in the table and iterated through based on the number of items in each purchase order

poItem is the related name for the po in poItem class which refers to the po itself, the items are reffered to with related name items1 which returns

AttributeError at /PO-Detail/1/
'QuerySet' object has no attribute 'pk'

models.py

class poItem(models.Model):
    uuid = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36)
    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='items1')
    po = models.ForeignKey(po, on_delete=models.CASCADE, related_name='poitems')

updated code as per your last comment

ItemFormSet = inlineformset_factory(po, poItem, form=inflowForm, fields= ['added_quantity', 'expiryDate'])
    orderNo = po.objects.get(number = pk)
    orderItems = orderNo.poitems.all()
    formset = ItemFormSet(instance=orderItems)

    if request.method == "POST":
        formset = ItemFormSet(request.POST, instance=orderItems)
        if formset.is_valid():
            formset.save(commit=False)
            for instance in formset:
                instance.save()

Whenever you have an error that you are requesting assistance with, please post the complete traceback. It’s really difficult to try and diagnose an issue with just the summary description.

Similarly, it helps if you identify what code may be the cause of the error. The error message you posted:

AttributeError at /PO-Detail/1/
'QuerySet' object has no attribute 'pk'

makes reference to a URL, but we don’t have any way of knowing what view that is.

Also, when you have substantial changes made to a block of code like a view, please post the entire view and not just the updates. It becomes extremely difficult to try and figure out what the view looks like.