Inline ModelFormSet - How to avoid the creation of an object for the extra line

In a view, I have:

        InvoiceFormSet = inlineformset_factory(Business,Invoice,extra=1, form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business, data= request.POST) 
   def post(self, request, *args, **kwargs):
        pk = self.kwargs['pk'] 
        business = get_object_or_404(Business, pk=pk)      
        business_form = BusinessDetailForm(instance=business, data=request.POST)
        InvoiceFormSet = inlineformset_factory(Business,Invoice,extra=1, form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business, data= request.POST)      

        if 'update' in request.POST:
            #messages.warning(request, request.POST)
            if business_form.has_changed():
                if business_form.is_valid():
                    business_form.save()
                    messages.success(request, 'Les données Affaires ont été mises à jour.')
                else:
                    messages.warning(request, 'les données Affaires sont incorrectes.'+ str(business_form.errors))
            if invoice_formset.has_changed():
                if invoice_formset.is_valid():
                    for form in invoice_formset.forms:
                        if form.has_changed():
                            form.save()

InvoiceForm looks like this:

class InvoiceForm(ModelForm):       
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['number'].initial = self.instance.number

    number = forms.CharField(disabled=True, required=False)    
    class Meta:
        model = Invoice
        fields= ['label','issuance_date','due_date','type','amount', 'is_issued', 'is_paid','payment_date']
        widgets = {'issuance_date': DatePickerInput, 'due_date': DatePickerInput,'payment_date':DatePickerInput}

In the model Invoice is as follows, with several fields with default value

class Invoice(models.Model):
    number = models.CharField('Numéro de facture', max_length=20, null=True, blank=True)
    label = models.CharField('libellé de facture', max_length=200, null=True,blank = True)
    issuance_date = models.DateField('Date de facture', default=datetime.date.today)
    due_date = models.DateField('Date d\'échéance', default=default_due_date)
    PREVISION = 'PR'
    FACTURE = 'FA'
    AVOIR = 'AV'
    INVOICE_TYPE_CHOICES = [
        (PREVISION, 'Echéance'),
        (FACTURE, 'Facture'),
        (AVOIR, 'Avoir')
        ]
    type = models.CharField('Type de facture',max_length=2, 
        choices=INVOICE_TYPE_CHOICES, default=PREVISION)
    amount = models.DecimalField('Montant',max_digits=9, decimal_places=2, default=0)
    is_issued = models.BooleanField('Emise', default=False)
    is_paid = models.BooleanField('Réglée', default=False)
    payment_date = models.DateField('Date de règlement', null=True,blank=True) 
    business = models.ForeignKey(Business, on_delete=models.CASCADE, verbose_name='Commande')
    market = models.ForeignKey(Market, on_delete=models.RESTRICT, null=True,blank=True)
    purchase_order = models.ForeignKey(Purchase_order, on_delete=models.RESTRICT, null=True,blank=True)
    customer_ref = models.CharField('Référence client', max_length=250, 
            null=True, blank=True)
    customer_contact = models.ForeignKey(Contact, on_delete=models.DO_NOTHING, null=True, blank=True,
        verbose_name='Suivi par')
    addressee = models.CharField('Destinataire', max_length=60, null=True, blank=True)
    address = models.ForeignKey(Address, on_delete=models.RESTRICT,
        verbose_name='Adresse de facturation', null=True, blank = True)

When the forms are displayed, there is an extra line with some fields beign filled with default values.
When the form is submited, a new invoice a created even if I do not enter anything in the last line (this form has not changed).
How to prevent this creation?

Il will add that I don’t understand why the following test doesn’t work

            if invoice_formset.has_changed():
                if invoice_formset.is_valid():
                    for form in invoice_formset.forms:
                        if form.has_changed():
                            form.save()
                    # A insérer: la sauvegarde des données dans l'historique
                    #invoice_formset.save()
                    messages.success(request, 'Les données Prévisions ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Prévisions sont incorrectes.'+ str(invoice_formset.errors))
            else:
                messages.success(request,'Aucune modification des prévisions.')

Even if I don’t enter anything on the form, the message ‘Les données Prévisions ont été mises à jour’ i displayerd

You need to remove the extra=1 parameter from your data binding in the post section of the view. You’re instructing Django to add an additional instance in addition to whatever has been posted.

In this case, I cannot add new invoice, if I need to.
I would like to be able to change existing invoices and add one if needed

You’re fine with having it on the get part of the view, but not in the post side.

Unfortunately, it does not make it:
I have the business form on the top which allows me to change the business attributes. If I just change one of them just after the get - not adding an invoice, il will record the business changes and record and empty invoice.
Besides that, that means that I couldn’add several invoices, just by submitting the form several times.

I do not understand why testing the form.has_changed() does not work :frowning: I guess it works not the way I understand. But it is quiet surprising that even if I do not enter anything on the form, “form.has_changed()” is True…

Please post the complete, revised view.

Here is the view:

class BusinessUpdateView(UpdateView):
    template_name = 'gestcom/business_update_form.html'
    model = Business
    fields = '__all__'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        business = get_object_or_404(Business, pk=self.kwargs['pk']) 
        context['business_name']=business.name 
        context['business_form'] = BusinessDetailForm(instance = business)
        InvoiceFormSet = inlineformset_factory(Business,Invoice,extra=1, form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business)
        context['invoice_formset'] = invoice_formset     
        return context
    def post(self, request, *args, **kwargs):
        pk = self.kwargs['pk'] 
        business = get_object_or_404(Business, pk=pk)      
        business_form = BusinessDetailForm(instance=business, data=request.POST)
        InvoiceFormSet = inlineformset_factory(Business,Invoice,extra=1,form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business, data= request.POST)      

        if 'update' in request.POST:
            #messages.warning(request, request.POST)
            if business_form.has_changed():
                if business_form.is_valid():
                    business_form.save()
                    messages.success(request, 'Les données Affaires ont été mises à jour.')
                else:
                    messages.warning(request, 'les données Affaires sont incorrectes.'+ str(business_form.errors))
            if invoice_formset.has_changed():
                if invoice_formset.is_valid():
                    for form in invoice_formset.forms:
                        if form.has_changed():
                            form.save()
                    # A insérer: la sauvegarde des données dans l'historique
                    #invoice_formset.save()
                    messages.success(request, 'Les données Prévisions ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Prévisions sont incorrectes.'+ str(invoice_formset.errors))
            else:
                messages.success(request,'Aucune modification des prévisions.')
        #return HttpResponseRedirect(reverse('business_update_form', args=(business.id,)))
        return render(request, 'gestcom/business_update_form.html',\
                {'business_form':business_form, 'invoice_formset':invoice_formset})     

And the invoice form

class InvoiceForm(ModelForm):
    def clean(self):
        cleaned_data = super().clean()
        label = cleaned_data.get('label')
        amount = cleaned_data.get('amount')
        number = cleaned_data.get('number')
        issuance_date = cleaned_data.get('issuance_date')
        due_date = cleaned_data.get('due_date')
        type = cleaned_data.get('type')
        is_issued = cleaned_data.get('is_issued')
        is_paid = cleaned_data.get('is_paid')
        payment_date = cleaned_data.get('payment_date')
        if label or amount !=0 :
            if not (label and amount!=0):
                msg = 'Si un des champs d\'affaire est saisi, tous les champs doivent l\'être'
                self.add_error('label', msg)
        # si une ligne de facture n'a pas été saisie, on met tous les champs à zéro pour éviter
        # l'enregistrement d'une facture "vide"
        if not(label) and amount==0:
            amount = None
            number = None
            issuance_date = None
            due_date = None
            type = None
            is_issued = None
            is_paid = None
            payment_date = None
        
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['number'].initial = self.instance.number

    number = forms.CharField(disabled=True, required=False)    
    class Meta:
        model = Invoice
        fields= ['label','issuance_date','due_date','type','amount', 'is_issued', 'is_paid','payment_date']
        widgets = {'issuance_date': DatePickerInput, 'due_date': DatePickerInput,'payment_date':DatePickerInput}

You may notice in the form clean method,
An invoice should be created if a label is entered and the amount is !=0 (the other fields have default value). If the label is empty and the amount = 0, I clear all the fieds, thinking it would prevent the creation of the object… :thinking:

I’ve underlined above the extra=1 that is causing the problem.

You want your post function to only handle what was submitted (what you’re binding with the request.POST clause). You do not want your post method to add additional forms beyond what was submitted.

I’ve just made the change. When I submit the form not changing anything in the invoice part, it still ends with the creation of a new invoice :frowning:

The other items I can see so far:

This is going to create a form for you.

But, you have:

This is going to create a second form in the view.

Review the docs at Generic editing views | Django documentation | Django, and, if you’re not familiar with the sequence of events within the generic CBVs, I always encourage people to become familiar with the Classy Class-Based Views site - in this case, you’ll want to see UpdateView -- Classy CBV

Additionally, and separate from this, you’re creating two different forms (or more specifically, a form and a formset), and you are not currently using the prefix attribute for either - leading to possible confusion within Django for the data between forms.

I don’t know off-hand to what degree these issues are affecting this situation, but they should be cleaned up.

Something else I see -

You have:

    def clean(self):
        cleaned_data = super().clean()
        label = cleaned_data.get('label')
        amount = cleaned_data.get('amount')
*        number = cleaned_data.get('number')
*        issuance_date = cleaned_data.get('issuance_date')
*        due_date = cleaned_data.get('due_date')
*        type = cleaned_data.get('type')
*        is_issued = cleaned_data.get('is_issued')
*        is_paid = cleaned_data.get('is_paid')
*        payment_date = cleaned_data.get('payment_date')
        if label or amount !=0 :
            if not (label and amount!=0):
                msg = 'Si un des champs d\'affaire est saisi, tous les champs doivent l\'être'
                self.add_error('label', msg)
        # si une ligne de facture n'a pas été saisie, on met tous les champs à zéro pour éviter
        # l'enregistrement d'une facture "vide"
*        if not(label) and amount==0:
*            amount = None
*            number = None
*            issuance_date = None
*            due_date = None
*            type = None
*            is_issued = None
*            is_paid = None
*            payment_date = None

All the lines marked with * are not doing anything within your code. They can all be removed.

What it is not doing is:

You are not clearing the fields in the form. You are clearing the local copies of the variables that you are getting from the cleaned_data dict.

(But it’s not necessary for you to do this anyway - Django will do the right thing.)


On a successful submission, you’re not returning a redirect. If you want to redisplay the current set of forms, you’ll want to return a redirect to this same url to allow the forms to be properly regenerated.
The only time you want to return the posted forms is if there are errors that you want displayed on the form to be corrected.

Side note: I’ve created a simplified version of the structures you’ve created here based on what you’ve provided, making the appropriate corrections I’ve identified so far, and it works as expected. (Note, you have not yet posted the templates involved, so I had to create a generic template for this.)
If this is the first time you’re working with a combination of a form and formset inside a CBV, I suggest you take a step back and try a simplified version first.

Hi Ken,

Sorry for the late feedback, but I was traveling the last 2 days.

I’ve made the changes you recommend. Unfortunately, it’s still does not work. When I submit the foms without any changes (I only strike on the button “Sauvegarder”), it still records a new “empty” invoice. That means the if “invoice_formset.has_changed” is true (the message ‘Les données Prévisions ont été mises à jour’ is displayed ), while “business_form.has_changed” is false,(the message ‘Aucune modification du dossier affaire.’ is displayed?). How is that possible?

You’ll find hereafter the code up to date.
Forms

class InvoiceForm(ModelForm):
    def clean(self):
        cleaned_data = super().clean()
        label = cleaned_data.get('label')
        amount = cleaned_data.get('amount')
        if label or amount !=0 :
            if not (label and amount!=0):
                msg = 'Si un des champs d\'affaire est saisi, tous les champs doivent l\'être'
                self.add_error('label', msg)
        # si une ligne de facture n'a pas été saisie, on met tous les champs à zéro pour éviter
        # l'enregistrement d'une facture "vide"

        
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['number'].initial = self.instance.number

    number = forms.CharField(disabled=True, required=False)    
    class Meta:
        model = Invoice
        fields= ['label','issuance_date','due_date','type','amount', 'is_issued', 'is_paid','payment_date']
        widgets = {'issuance_date': DatePickerInput, 'due_date': DatePickerInput,'payment_date':DatePickerInput}

Views.py

class BusinessUpdateView(UpdateView):
    template_name = 'gestcom/business_update_form.html'
    model = Business
    fields = '__all__'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        business = get_object_or_404(Business, pk=self.kwargs['pk']) 
        context['business_form'] = BusinessDetailForm(instance = business)
        InvoiceFormSet = inlineformset_factory(Business,Invoice,extra=1, form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business, prefix='invoice')
        context['invoice_formset'] = invoice_formset     
        return context
    def post(self, request, *args, **kwargs):
        pk = self.kwargs['pk'] 
        business = get_object_or_404(Business, pk=pk)      
        business_form = BusinessDetailForm(instance=business, data=request.POST)
        InvoiceFormSet = inlineformset_factory(Business,Invoice,form = InvoiceForm)
        invoice_formset = InvoiceFormSet(instance=business, data= request.POST, prefix='invoice')      

        if 'update' in request.POST:
            #messages.warning(request, request.POST)
            if business_form.has_changed():
                if business_form.is_valid():
                    business_form.save()
                    messages.success(request, 'Les données Affaires ont été mises à jour.')
                else:
                    messages.warning(request, 'les données Affaires sont incorrectes.'+ str(business_form.errors))
            else:
                messages.success(request,'Aucune modification du dossier affaire.')
            if invoice_formset.has_changed():
                if invoice_formset.is_valid():
                    for form in invoice_formset.forms:
                        if form.has_changed():
                            form.save()
                    # A insérer: la sauvegarde des données dans l'historique
                    #invoice_formset.save()
                    messages.success(request, 'Les données Prévisions ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Prévisions sont incorrectes.'+ str(invoice_formset.errors))
            else:
                messages.success(request,'Aucune modification des prévisions.')
        return render(request, 'gestcom/business_update_form.html',\
                {'business_form':business_form, 'invoice_formset':invoice_formset})     

Template

{% extends "base_generic.html" %}
{% block content %}
<h2>Dossier de l'affaire: {{business_form.name.value}} </h2>
<form method="post">
    <div class="container">
        <div class = "row">
            <div class="col-lg-4">
                <table class="table">
                    <tr>
                        <th>Affaire:</th>
                        <td>{{business_form.name}}</td>
                    </tr>
                    <tr>
                        <th>Client:</th>
                        <td>{{business_form.customer}}</td>
                    </tr>
                    <tr>
                        <th>Chargé d'affaires:</th>
                        <td>{{business_form.account_exec}}</td>
                    </tr>
                    <tr></tr>
                    <tr>
                        <th>Type de contrat:</th>
                        <td>{{business_form.contract_type}}</td>
                    </tr>
                </table>
            </div>
            <div class="col-lg-4">
                <table class="table">
                    <tr>
                        <th>Montant:</th>
                        <td>{{business_form.amount}}</td>
                    </tr>
                    <tr>
                        <th>Pondération:</th>
                        <td>{{business_form.ponderation}}</td>
                    </tr>
                    <tr>
                        <th>Montant pondéré:</th>
                        <td>{{business_form.amount_pondered}}</td>
                    </tr>
                </table>
            </div>
            <div class="col-lg-4">
                <table class="table">
                    <tr>
                        <th>Contact client:</th>
                        <td>{{business_form.contact}}</td>
                    </tr>
                    <tr>
                        <th>Destinataire de la facture:</th>
                        <td>{{business_form.default_addressee}}</td>
                    </tr>
                    <tr>
                        <th>Adresse de facturation:</th>
                        <td>{{business_form.default_address}}</td>
                    </tr>
                </table>
            </div>            
        </div>

    </div>
    <br>
    <h3>Les échéances de facturation révisionnelles</h3>
    <Div class="row">
        {% csrf_token %}
        {{ invoice_formset.management_form }}

        <table class="table table-hover table-striped">
        <thead>
        <tr>
            <th>Lien</th>
            <th>Numéro</th>
            <th>Type</th>
            <th>Libellé</th>
            <th>Montant HT</th>
            <th>Emise</th>
            <th>Le</th>
            <th>Echue le</th>
            <th>Réglée</th>
            <th>Le</th>
        </tr>
        </thead>
        <tbody>
            {% for form in invoice_formset %}
            <tr>
                <td style = "display:none">{{ form.id }}</td>
                {% if form.id.value %}
                    <td><a class="article-title" href="/gestcom/invoices/{{form.id.value}}"><i class='bx bx-detail nav_icon'></a></td>
                {%else%}
                    <td></td>
                {% endif %}
                <td>{{ form.number }}</a></td>
                <td>{{ form.type }}</a></td>
                <td>{{ form.label }}</a></td>
                <td>{{ form.amount}}</td>
                <td>{{ form.is_issued}}</td>
                <td>{{ form.issuance_date}}</td>
                <td>{{ form.due_date}}</a></td>
                <td>{{ form.is_paid }}</a></td>
                <td>{{ form.payment_date}}</a></td>              
                </tr>
            {% endfor %}
        </tbody>
        </table>
        <br>
    </Div>
    <div>
        <button id = "update" name="update" type="submit" class="btn btn-primary">
        <i class='bx bx-save nav_icon'></i>
        Sauvegarder
        </button>
    </div>
</form>

{% endblock content %}

I’m not sure what to tell you here.

I don’t see this behavior in what I’m testing.

Do you have any JavaScript being used that is interacting with these forms? Do you have any other functions overridden in the UpdateView or InvoiceForm classes?

The best that I can suggest to you at this point is to either run this in a debugger or add a bunch of print statements to try and understand why has_changed is True. It should help if you can identify what instances of the form, and which fields or values are causing this to occur.
See the docs at The Forms API | Django documentation | Django

You might also want to double-check the html as it has been rendered by the browser to ensure that the html was created properly and that the html is structurally correct.

Something else to try - change your template to not render these forms as a table. For testing purposes, change this to render as divs. I know there are some limitations and “strangeness” when mixing forms and html tables. If things aren’t exactly right, you will get unexpected results.

Side note: You’re still ending up with two instances of your BusinessDetailForm being created in your view. These lines of code:

are replicating what’s being done by:

along with the other basic functions being called by UpdateView.

I’m pointing this out as a side note, because this is not causing a problem, it’s just something you should be aware of.

Thanks Ken,

I will dig this and keep you posted.

Regarding your side note, if I remove

        business = get_object_or_404(Business, pk=self.kwargs['pk']) 
        context['business_form'] = BusinessDetailForm(instance = business)

the form is empty, the “business” related field are empty…

Hi Ken,

I followed up on tour hint regarding javascript. I remove the “DatePickerInput” widget related to date fields… and guess what, it works!!!

That’s a first step, but a date picking widget is really helpfull. I see two possibilities:

  1. To use a date picking widget which would not have the side effect of changing the form. Any idea of whoat I could use?

  2. Find a way of not saving the data if the only fields having been changed are the date’s one. How would you do that?

Thanks,

Richard

Correct, in the Django provided CBVs, the form being rendered is named form. That’s what you would be referring to in the template.

If you’re going to use those CBVs, you really do want to spend some time in the docs and with Classy Class-Based Views and the CBV diagrams to understand how they work.

Sorry, that’s not an area I’m familiar with.

I’d start with the information available at Checking which form data has changed

Finally, I’m saving the form only if amount !=0. It seems to be ok.

Thanks for your help