Formset Not Saving - "This field is required" error even when all data is filled in

I have a formset that will not save. When I print the error to python, it shows:

<tr>
    <th><label for="id_account">Account:</label></th>
    <td>
      <ul class="errorlist"><li>This field is required.</li></ul>
      <select name="account" class="mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" required id="id_account">
  <option value="" selected>---------</option>

  <optgroup label="Cash and Equivalents">
  <option value="8b0a698b-c1cb-4eb8-b50c-dd7033207c19">Chase</option>

  <option value="e5250016-76ba-42a5-a4db-d450e4b05c05">Discover</option>

  <option value="3a920dc2-225f-4fa6-b67d-303e4107c658">Wells Fargo</option>

  </optgroup>
  <optgroup label="Investments">
  <option value="c30d4b2e-2ca9-4bb2-a4ea-198699b4d408">Vanguard</option>

  <option value="499fadc6-f5ce-4c13-9c4a-06e1394c34c4">Fidelity</option>

  </optgroup>
  <optgroup label="Retirement Accounts">
  <option value="fe55b367-9f39-440f-98aa-ac49fe7299ef">Fidelity 401(k)</option>

  <option value="18eb4069-a883-4229-8ad3-2342eb39a845">Price IRA</option>

  </optgroup>
 
  <optgroup label="Other Items">
  <option value="51f400ac-f21b-43c3-917c-703ceb146429">Vehicles</option>

  </optgroup>
</select>
      
      
    </td>
  </tr>

  <tr>
    <th><label for="id_date">Date:</label></th>
    <td>
      <ul class="errorlist"><li>This field is required.</li></ul>
      <input type="date" name="date" class="mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" required id="id_date">
      
      
    </td>
  </tr>

  <tr>
    <th><label for="id_amount">Amount:</label></th>
    <td>
      <ul class="errorlist"><li>This field is required.</li></ul>
      <input type="number" name="amount" class="mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" required step="0.01" id="id_amount">
      
      
        
      
    </td>
  </tr>

My models look like this:

from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator

import uuid

class Category(models.Model): # categories are used to organize what an account is for (cash, debts, investments, etc.)
    CATEGORTY_TYPE_CHOCIES = (
        ('Asset', 'Asset'),
        ('Liability', 'Liability'),
    )
    id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, primary_key=True)
    category_type = models.CharField(max_length=9, choices=CATEGORTY_TYPE_CHOCIES, null=False, blank=False) # is this category an asset or liability
    category_name = models.CharField(null=False, blank=False, max_length=255, unique=True)
    category_description = models.TextField(null=True, blank=True)
    sort_code = models.IntegerField(null=False, blank=False, unique=True, validators=[MinValueValidator(0), MaxValueValidator(300)])
    active = models.BooleanField(default=True) # is this category still actively being used

    def __str__(self):
        return str(self.category_name)
    
class Account(models.Model):
    id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, primary_key=True)
    category = models.ForeignKey(Category, on_delete=models.PROTECT, null=False, blank=False) # what  category does this account belong under
    account_name = models.CharField(null=False, blank=False, max_length=255, unique=True)
    account_description = models.TextField(null=True, blank=True)
    active = models.BooleanField(default=True) # is this account still actively being used

    def __str__(self):
        return str(self.account_name)
    
class NetWorthEntry(models.Model): # point in time entry of an accounts value
    id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, primary_key=True)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, null=False, blank=False) # what  account does this entry belong under
    date = models.DateField(null=False, blank=False) # what date was this entry for
    amount = models.DecimalField(max_digits=12, decimal_places=2, null=False, blank=False) #how much is the account on the date for the entry

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['account', 'date'], name='unique_account_date')
        ]

    def __str__(self):
        return str(self.account.account_name + " - " + str(self.date))

My form looks like this:

from django import forms
from django.db.models import Q
from django.forms import ModelForm
from .models import Category, Account, NetWorthEntry
from cashflow.functions import GroupedModelChoiceField

class SingleNetWorthEntryForm(ModelForm):
    account = GroupedModelChoiceField(queryset=Account.objects.filter(Q(active=True) & Q(category__active=True)).order_by('category__sort_code', 'account_name'), choices_groupby='category', widget=forms.Select(attrs={'class': 'mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50', 'required': True}), label='Account')
    class Meta:
        model = NetWorthEntry
        fields = [
            'account',
            'date',
            'amount',
        ]
        labels = {
            'account': 'Account',
            'date': 'Date',
            'amount': 'Amount',
        }
        widgets = {
            'account': forms.Select(attrs={'class': 'mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50', 'required': True}),
            'date': forms.DateInput(attrs={'type':'date', 'class': 'mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50', 'required': True}),
            'amount': forms.NumberInput(attrs={'class': 'mb-5 w-full px-3 py-3 border-2 border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50', 'required': True}),
        }

My view looks like this:

@login_required
def NetWorthGroupEntryCreate(request):
    context = {}

    active_accounts = Account.objects.filter(Q(active=True) & Q(category__active=True)).order_by('category__sort_code', 'account_name') # only show/allow accounts/categories that are active in net worth entry form

    initial = []
    todays_date = datetime.today().strftime('%Y-%m-%d')
    for account in active_accounts:
        initial.append({'account': account, 'date': todays_date})

    GroupEntryFormSet = formset_factory(SingleNetWorthEntryForm, extra=0)
    formset = GroupEntryFormSet(initial=initial)

    if request.method == 'POST':
        form = SingleNetWorthEntryForm(request.POST)
        if form.is_valid():
            form.save()
            return reverse('networth-entries')

    context['formset'] = formset
    return render(request, 'networth/group-entry-create.html', context)

My template looks like this:

{% extends 'base.html' %}
{% load static %}

{% block content %}

<section class="pt-10">
    <div class="mb-10">
      <h1 class="text-4xl text-center">Create a New Net Worth Group Entry</h1>
    </div>
    <div class="flex justify-center">
        <form method="POST">
          {% csrf_token %}
          {{ formset.as_p }}
          <button class="px-6 py-3 font-medium text-white bg-blue-500 hover:bg-blue-600 rounded transition duration-200" type="submit">Create Net Worth Entry</button>
        </form>
      </div>
</section>

{% endblock content %}

When I try to submit the formset, it just reloads the page becuase the “form.is_valid()” is not true. However, I am confused why it is not valid. When I print the “request.POST” to python, it shows:

QueryDict: {
    "csrfmiddlewaretoken": [
        "uTFNG3nA5FgUvAmCxVaisod5yHHT7E2o0JU67Cm2NHiJS7QjEDVnr0RFWvW4qIBn"
    ],
    "form-TOTAL_FORMS": ["16"],
    "form-INITIAL_FORMS": ["16"],
    "form-MIN_NUM_FORMS": ["0"],
    "form-MAX_NUM_FORMS": ["1000"],
    "form-0-account": ["8b0a698b-c1cb-4eb8-b50c-dd7033207c19"],
    "form-0-date": ["2024-07-07"],
    "form-0-amount": ["12"],
    "form-1-account": ["e5250016-76ba-42a5-a4db-d450e4b05c05"],
    "form-1-date": ["2024-07-07"],
    "form-1-amount": ["12"],
    "form-2-account": ["3a920dc2-225f-4fa6-b67d-303e4107c658"],
    "form-2-date": ["2024-07-07"],
    "form-2-amount": ["12"],
    "form-3-account": ["be97cac3-fa3d-493f-b913-a1a8dd3a6a86"],
    "form-3-date": ["2024-07-07"],
    "form-3-amount": ["12"],
    "form-4-account": ["68f59857-c35f-4789-aeda-67524c5bb803"],
    "form-4-date": ["2024-07-07"],
    "form-4-amount": ["12"],
    "form-5-account": ["0b902375-f3de-4b8d-97a6-0ab0c0190b72"],
    "form-5-date": ["2024-07-07"],
    "form-5-amount": ["12"],
    "form-6-account": ["488ee534-ee68-4531-9b21-e7e2993766d3"],
    "form-6-date": ["2024-07-07"],
    "form-6-amount": ["12"],
    "form-7-account": ["842fda74-49f6-4ab1-9c75-32ef6173110d"],
    "form-7-date": ["2024-07-07"],
    "form-7-amount": ["12"],
    "form-8-account": ["5356957d-46e4-4534-8b03-73739c7cf257"],
    "form-8-date": ["2024-07-07"],
    "form-8-amount": ["12"],
    "form-9-account": ["c30d4b2e-2ca9-4bb2-a4ea-198699b4d408"],
    "form-9-date": ["2024-07-07"],
    "form-9-amount": ["12"],
    "form-10-account": ["499fadc6-f5ce-4c13-9c4a-06e1394c34c4"],
    "form-10-date": ["2024-07-07"],
    "form-10-amount": ["12"],
    "form-11-account": ["fe55b367-9f39-440f-98aa-ac49fe7299ef"],
    "form-11-date": ["2024-07-07"],
    "form-11-amount": ["12"],
    "form-12-account": ["18eb4069-a883-4229-8ad3-2342eb39a845"],
    "form-12-date": ["2024-07-07"],
    "form-12-amount": ["12"],
    "form-13-account": ["34628569-c19a-4cdc-8ef4-3cca4fb2f7a3"],
    "form-13-date": ["2024-07-07"],
    "form-13-amount": ["12"],
    "form-14-account": ["fbb3ecc0-1d4e-417c-9fa0-5aaa83976862"],
    "form-14-date": ["2024-07-07"],
    "form-14-amount": ["12"],
    "form-15-account": ["51f400ac-f21b-43c3-917c-703ceb146429"],
    "form-15-date": ["2024-07-07"],
    "form-15-amount": ["12"],
}

This shows that the request contains all the forms I am submitting (16 in total) and that none are missing as the error at the top of this post says.

Any help would be appreciated. Thank you

You’re creating and rendering a formset.

However, when you’re processing the POST request, you’re binding the data to the form class and not the formset.

You need to bind the data to the formset, along with calling the is_valid method on the formset.

Review the docs and examples at Using a formset in views and templates.

Thanks for clarifying. I fixed that by changing the code to:

@login_required
def NetWorthGroupEntryCreate(request):
    context = {}

    active_accounts = Account.objects.filter(Q(active=True) & Q(category__active=True)).order_by('category__sort_code', 'account_name') # only show/allow accounts/categories that are active in net worth entry form

    initial = []
    todays_date = datetime.today().strftime('%Y-%m-%d')
    for account in active_accounts:
        initial.append({'account': account, 'date': todays_date})

    GroupEntryFormSet = formset_factory(SingleNetWorthEntryForm, extra=0)
    formset = GroupEntryFormSet(initial=initial)

    if request.method == 'POST':
        form = GroupEntryFormSet(request.POST)
        if form.is_valid():
            form.save()
            return reverse('networth-entries')

    context['formset'] = formset
    return render(request, 'networth/group-entry-create.html', context)

But now I am getting the error:

'SingleNetWorthEntryFormFormSet' object has no attribute 'save'

My form is a model form, so how is “save” not usable?

No, it’s not. The variable you are referring to as form is not a form, it’s a formset. It’s not even a modelformset.

Thanks, Ken. I got it worked out.

For anyone finding this in the future, the solution was to switch to using a modelformset.