Id field is missing in a POST request...

Hello
There is something wrong with my code. I don’t know what. When I display the page, everything is ok. Company’data are ok. Related businesses are ok. But when I hit the “update” key, even if I do not change anthing, bussiness data show issues. The link to the details is not displayed anymore, and amount_pondered and amount_to_plan are set to 0 (when their value are 10 ans 100). Could someone tell me what is wrong. Here is the code:

Models.py

class Company(models.Model):
    name = models.CharField('Nom',max_length=200,)
    SIRET = models.CharField('SIRET',max_length=16, null = True, blank = True)

    creation_date = models.DateField('Date de création',default=datetime.date.today, 
        null = True,blank = True)
    last_modification_date = models.DateTimeField('Date de dernière modification',
        auto_now= True,null = True, blank = True,)

    class Meta:
        ordering = ['name']
        verbose_name = 'Client'
        
    def total_business(self):
        # Renvoie le montant total de chiffre d'affaire signé avec ce client
        business=self.customer.all()
        total_business = 0
        for b in business:
            b_ponderation = b.ponderation
            if b_ponderation.weight == 100:
                total_business += b.amount
        return total_business
    
    def total_to_bill(self):
        # Renvoie le montant total à facturer à ce client
        business=self.customer.all()
        total_to_bill = 0
        for b in business:
            b_ponderation = b.ponderation
            if b_ponderation.weight == 100:
                total_to_bill += b.amount_to_bill()
        return total_to_bill
    
    def total_under_negotiation(self):
        # Renvoie le montant pondéré des affaires en cours de négociation
        business=self.customer.all()
        total_under_negotiation = 0
        for b in business:
            b_ponderation = b.ponderation
            if b_ponderation.weight != 100:
                total_under_negotiation += b.amount_pondered()
        return total_under_negotiation            
        
    def __str__(self):
        """Cette retourne une chaîne de caractère pour identifier l'instance de la classe d'objet."""
        return self.name
    
    def get_absolute_url(self):
        return reverse('customer_detail_form', kwarg={'pk':self.pk})
    

class Framework_agreement(models.Model):
    number = models.CharField('Numéro',max_length=30,)
    company=models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='Client')
    signature_date= models.DateField('Date de signature',null=True,blank=True)

    def __str__(self):
        """String for representing the Model object (in Admin site etc.)"""
        return self.number
     
    class Meta:
        ordering = ['signature_date']
        verbose_name = 'Accord cadre'    

class PO_market(models.Model):
    number = models.CharField('Numéro',max_length=30,)
    company=models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='Client')
    signature_date= models.DateField('Date de signature',null=True,blank=True)
    framework_agreement = models.ForeignKey(Framework_agreement, on_delete=models.RESTRICT, null=True, blank=True)

    def __str__(self):
        """String for representing the Model object (in Admin site etc.)"""
        return self.number
     
    class Meta:
        ordering = ['signature_date']
        verbose_name = 'Marché à bons de commande' 
     
class Business(models.Model):
    name = models.CharField('Affaire',max_length=200, null = True, blank=True)
    customer = models.ForeignKey(Company,related_name = 'customer',on_delete=models.CASCADE,verbose_name='Client')
    beneficiary = models.ForeignKey(Company,related_name = 'beneficiary', on_delete=models.CASCADE,verbose_name='Bénéficiaire',
        null = True,blank = True)
    customer_ref = models.CharField('Référence client', max_length=250, 
        null=True, blank=True)
    framework_agreement = models.ForeignKey(Framework_agreement, on_delete=models.SET_NULL, verbose_name='Accord-cadre', null=True, blank=True)
    po_market = models.ForeignKey(PO_market, on_delete=models.SET_NULL, verbose_name='Marché à bons de commande', null=True, blank=True)
    default_contact = models.ForeignKey(Contact, related_name = 'set_business_contact', on_delete=models.SET_NULL, \
        null=True, blank=True, verbose_name='Contact client par défaut:')   
    default_addressee = models.ForeignKey(Contact, related_name="set_business_adressee" ,on_delete=models.SET_NULL, \
        null=True, blank=True, verbose_name='Destinataire par défaut:')
    default_address =models.ForeignKey(Address, on_delete=models.SET_NULL, \
        null=True, blank=True, verbose_name='Adresse par défaut:')
    account_exec = models.ForeignKey(Account_exec,on_delete=models.SET_NULL, null = True, blank = True, verbose_name='Chargé d\'affaires')
    amount = models.DecimalField('Montant',max_digits=9, decimal_places=2, null=True,blank=True)
    ponderation = models.ForeignKey(BusinessStatus, on_delete=models.RESTRICT, default=1)
    creation_date = models.DateField('Date de création', default=datetime.date.today)
    last_modification_date = models.DateTimeField('Date de dernière modification',
        auto_now= True,null = True, blank = True,)       
    
    def amount_pondered(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        if taux:
            amount = self.amount * taux / 100
        else:
            amount = 0
        return amount
    
    def amount_to_plan(self):
        invoices = self.invoice_set.all()
        a = self.amount
        for i in invoices:
            if i.type == 'FA' or i.type == 'PR': 
                a -= i.amount
            if i.type == 'AV':
                a += i.amount
        return a
    
    def amount_to_bill(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        invoices = self.invoice_set.all()
        amount_to_bill = self.amount
        if taux == 100:
            for i in invoices:
                if i.is_issued == True:
                    amount_to_bill -= i.amount
        return amount_to_bill
           
    def status(self):
        return BusinessStatus.objects.get(pk=self.ponderation_id).text()
        
    def weight(self):
        return BusinessStatus.objects.get(pk=self.ponderation_id).weight        

    def __str__(self):
        """String for representing the Model object (in Admin site etc.)"""
        return self.name
    
    def get_absolute_url(self):
        return reverse('customer_detail_form', kwarg={'pk':self.pk})
    
    class Meta:
        ordering = ['ponderation','amount']
        verbose_name = 'Affaire'

Piece of forms.py

class BusinessForm(ModelForm):
    def clean(self):
        cleaned_data = super().clean()
        name = cleaned_data.get('name')
        account_exec = cleaned_data.get('account_exec')
        amount = cleaned_data.get('amount')
        if name:
            """if not(account_exec):
                msg = 'Le responsable d\'affaires doit être saisi. account_exec='+str(account_exec)
                self.add_error('account_exec', msg)"""
            if not (amount):
                msg = 'Le montant de l\'affaire doit être saisi'
                self.add_error('amount', msg)
        elif amount:
            if not(name):
                msg = 'Si un montant est indiqué, le libellé de l\'affaire doit être saisi'
                self.add_error('name', msg)   
            if not(account_exec):
                msg = 'Si un montant est indiqué, le responsable d\'affaires doit être saisi'
                self.add_error('account_exec', msg)             
            
    def init_amount_pondered(self):
        if self.instance.pk:
            # si le business existe, on affiche le montant pondéréx²
            amount = self.instance.amount_pondered()
        else:
            # si le business n'existe pas (pas encore créé), on affiche 0
            amount = 0
        return amount

    def init_amount_to_plan(self):
        if self.instance.pk:
            amount = self.instance.amount_to_plan()
        else:
            amount = 0
        return amount
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # On initialise les montants calculés (montant pondéré et montant restant à planifier)
        self.fields['amount_to_plan'].initial = self.init_amount_to_plan()
        self.fields['amount_pondered'].initial = self.init_amount_pondered()
        for _, value in self.fields.items():
           value.widget.attrs['class'] = 'form-control'
        # Restreindre la liste de choix des po_market au client en cours
        if self.instance.pk:
            customer = self.instance.customer
            self.fields['framework_agreement'].queryset = Framework_agreement.objects.filter(company=customer)
            self.fields['po_market'].queryset = PO_market.objects.filter(company=customer)
        else:
            self.fields['framework_agreement'].queryset = Framework_agreement.objects.none()
            self.fields['po_market'].queryset = PO_market.objects.none()
           # On vérifie si l'affaire est déjà associée à un framework agreement
        if self.instance.framework_agreement:
            # Si oui, on réordonne les choix pour mettre le framework agreement associé en premier
            framework_agreement = self.instance.framework_agreement
            #choices = [(framework_agreement.id, str(framework_agreement))] + self.fields['framework_agreement'].choices
            #self.fields['framework_agreement'].choices = choices
            self.fields['framework_agreement'].initial = framework_agreement.id
        if self.instance.po_market:
            # Si oui, on réordonne les choix pour mettre le framework agreement associé en premier
            po_market = self.instance.po_market
            #choices = [(po_market.id, str(po_market))] + self.fields['po_market'].choices
            #self.fields['po_market'].choices = choices
            self.fields['po_market'].initial = po_market.id          

    amount_pondered = forms.CharField(disabled=True)
    amount_to_plan = forms.CharField(disabled = True)
    class Meta:
        model = Business
        fields = ['name', 'customer', 'beneficiary','framework_agreement','po_market','account_exec', 'amount','ponderation',]
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}


type or paste code here

Views.py

class CompanyUpdateView(UpdateView):
    template_name = 'salesforecast/company_update_form.html'
    model = Company
    fields = '__all__'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        company = get_object_or_404(Company, pk=self.kwargs['pk']) 
        context['company_form'] = CompanyForm(instance = company)
        BusinessFormSet = inlineformset_factory(Company,Business,fk_name= "customer" ,can_delete=False,extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=company)
        context['business_formset'] = business_formset
        ContactFormSet = inlineformset_factory(Company,Contact,can_delete=True,extra=1, form = ContactForm)
        contact_formset = ContactFormSet(instance=company)
        context['contact_formset'] = contact_formset
        AddressFormSet = inlineformset_factory(Company,Address,can_delete=True,extra=1, form = AddressForm)
        address_formset = AddressFormSet(instance=company)
        context['address_formset'] = address_formset        
        return context
    def post(self, request, *args, **kwargs):
        #messages.warning(request,'post')
        pk = self.kwargs['pk'] 
        company = get_object_or_404(Company, pk=pk)      
        company_form = CompanyForm(instance=company, data=request.POST)
        BusinessFormSet = inlineformset_factory(Company, Business, fk_name= "customer" ,can_delete=False, extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=company, data= request.POST)      
        ContactFormSet = inlineformset_factory(Company,Contact,can_delete=True,extra=1, form = ContactForm)
        contact_formset = ContactFormSet(instance=company, data=request.POST)
        AddressFormSet = inlineformset_factory(Company,Address,can_delete=True,extra=1, form = AddressForm)
        address_formset = AddressFormSet(instance=company, data=request.POST)
        if 'update' in request.POST:
            messages.warning(request, request.POST)
            if company_form.has_changed():
                if company_form.is_valid():
                    company_form.save()
                    messages.success(request, 'Les données Client ont été mises à jour.')
                else:
                    messages.warning(request, 'les données Client sont incorrectes.')
            if business_formset.has_changed():
                messages.warning(request, 'business form has changed')
                if business_formset.is_valid():
                    # A insérer: la sauvegarde des données dans l'historique
                    business_formset.save()
                    # On recharge la page avec les données mises à jour (pour faire apparaître une nouvelle ligne de saisie)
                    business_formset = BusinessFormSet(instance=company)
                    messages.success(request, 'Les données Affaires ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Affaires sont incorrectes.'+ str(business_formset.errors))
            if contact_formset.has_changed():
                if contact_formset.is_valid():
                    # A insérer: la sauvegarde des données dans l'historique
                    contact_formset.save()
                    # On recharge la page avec les données mises à jour (pour faire apparaître une nouvelle ligne de saisie)
                    contact_formset = ContactFormSet(instance=company)
                    messages.success(request, 'Les données Contacts ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Contacts sont incorrectes.')
            if address_formset.has_changed():
                if address_formset.is_valid():
                    # A insérer: la sauvegarde des données dans l'historique
                    address_formset.save()
                    # On recharge la page avec les données mises à jour (pour faire apparaître une nouvelle ligne de saisie)
                    address_formset = AddressFormSet(instance=company)
                    messages.success(request, 'Les données Adresses ont été mises à jour')
                else:
                    messages.warning(request, 'Les données Adresses sont incorrectes.')                           

        return render(request, 'salesforecast/company_update_form.html',\
                {'company_form':company_form, 'business_formset':business_formset, \
                 'contact_formset':contact_formset,'address_formset': address_formset   }) 

And the html code:

{% extends "base_generic.html" %}
{% block content %}
{% include 'navbar.html' with active='companies' %}
<div class="bloc"></div>
<form method="post">
  <div class="formulaire shadow">
    <h2>Dossier du client: {{company_form.name.value}} </h2>
    <table class="table-hover"> 
      {{ company_form }}
    </table>
  </div>
  <div class="bloc"></div>

  <div class="accordion" id="accordion">
    <div class="accordion-item shadow" >
      <h3 class="accordion-header" id="headingBusinesses">
        <button class="accordion-button custom-color" type="button" data-bs-toggle="collapse" data-bs-target="#collapseBusinesses" aria-expanded="true" aria-controls="collapseBusinesses">
          <h5>Les affaires</h5>
        </button>
      </h3>
      <div id="collapseBusinesses" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordion">
        <div class="accordion-body">
          {% csrf_token %}
          {{ business_formset.management_form }}
          {% for form in business_formset %}
          <div class="row">
            <div class="col-md-1">
              {% if form.id.value %}
              <a href="/salesforecast/businesses/{{form.id.value}}">Lien</a>
              {% endif %}
              <label for="{{form.name.id_for_label}}">{{form.name.label}}</label>
            </div>
            <div class="col-md-2">{{form.name}}<div class="fieldWrapper">{{form.name.errors}}</div></div>
            <div class="col-md-1">
              <label for="{{form.beneficiary.id_for_label}}" class="mx-2">{{form.beneficiary.label}}</label>
            </div>
            <div class="col-md-1">{{ form.beneficiary }}<div class="fieldWrapper">{{form.beneficary.errors}}</div></div>
            <div class=" col-md-1">
              <label for="{{form.framework_agreement.id_for_label}}" class="mx-2">{{form.framework_agreement.label}}</label>
            </div>
            <div class="col-md-1">{{ form.framework_agreement }}<div class="fieldWrapper">{{form.framework_agreement.errors}}</div></div>
            <div class="col-md-1">
              <label for="{{form.po_market.id_for_label}}" class="mx-2">Marhé à BC</label>
            </div>
            <div class="col-md-1">{{ form.po_market }}<div class="fieldWrapper">{{form.po_market.errors}}</div></div>
            <div class="col-md-1">
              <label for="{{form.account_exec.id_for_label}}" class="mx-2">{{form.account_exec.label}}</label></div>
            <div class="col-md-1">{{ form.account_exec }}<div class="fieldWrapper">{{form.account_exec.errors}}</div></div>
        </div>
        
          <div class="row"> 
            <div class="col-md-3 form-group">
              <label for="{{form.amount.id_for_label}}" class="mr-2">{{form.amount.label}}</label>          
              {{ form.amount }}
            </div>
            <div class="col-md-3 form-group">
              <label for="{{form.ponderation.id_for_label}}" class="mx-2">{{form.ponderation.label}}</label>          
              {{ form.ponderation }}
            </div>
            <div class="col-md-3 form-group">
              <label for="{{form.amount_pondered.id_for_label}}" class="mx-2">{{form.amount_pondered.label}}</label>          
              {{ form.amount_pondered }}
            </div>
            <div class="col-md-3">
              <label for="{{form.amount_to_plan.id_for_label}}" class="mx-2">{{form.amount_to_plan.label}}</label>          
              {{ form.amount_to_plan }}
            </div>  
          </div>    
          <div class="line"></div>
          {% endfor %}
          <!--
          <table class="table table-hover ">
          <tbody>
              {% for form in business_formset %}
              <tr>
                  <td class="no-display">{{ form.id }}</td>
                  {% if form.id.value %}
                    <td><a aria-label="lien vers l'affaire" class="article-title" href="/salesforecast/businesses/{{form.id.value}}"><i class='bx bx-detail nav_icon'></a></td>
                  {%else%}
                    <td></td>
                  {% endif %}
                  <td>{{ form.name }}</a></td>
                  <td>{{ form.beneficiary}}</td>
                  <td>{{ form.amount}}</td>
                  <td>{{ form.ponderation}}</td>
                  <td>{{ form.amount_pondered}}</td>
                  <td>{{ form.account_exec}}</td>
                  <td>{{ form.contract_type}}</td> 
                  <td>{{ form.amount_to_plan}}</td>                 
                </tr>
              {% endfor %}
          </tbody>
          </table>
          -->
        </div>
      </div>
    </div>
    <div class="bloc"></div>
    <div class="accordion-item shadow" >
      <h3 class="accordion-header" id="headingBusinesses">
        <button class="accordion-button custom-color" type="button" data-bs-toggle="collapse" data-bs-target="#collapseContacts" aria-expanded="true" aria-controls="collapseContacts">
          <h5>Les contacts</h5>
        </button>
      </h3>
      <div id="collapseContacts" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordion">
        <div class="accordion-body">

            {% csrf_token %}
            {{ contact_formset.management_form }}
            <table class="table table-hover ">
            <thead>
              <tr>
                <th></th>
                <th></th>
                <th>Prénom</th>
                <th>Nom</th>
                <th>Fonction</th>
                <th>Adresse mail</th>
                <th>Téléphone</th>
              </tr>
            </thead>
            <tbody>
                {% for form in contact_formset %}
                <tr>
                  <td clas = "no-display">{{ form.id }}</td>
                  {% if form.id.value %}            
                    <td><i class='bx bx-trash nav_icon'></i>{{ form.DELETE }}</td>
                  {%else%}
                    <td></td>
                  {%endif%}
                  <td>{{ form.first_name }}</td>
                  <td>{{ form.last_name}}</td>
                  <td>{{ form.job}}</td>
                  <td>{{ form.email}}</td>
                  <td>{{ form.phone}}</td>                
                  </tr>
                {% endfor %}
            </tbody>
            </table>

        </div>
      </div>
    </div>
    <div class="bloc"></div>
    <div class="accordion-item shadow" >
      <h5 class="accordion-header" id="headingAddresses">
        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAddresses" aria-expanded="false" aria-controls="collapseAddresses">
          <h5>Les adresses</h5>
        </button>
      </h5>
      <div id="collapseAddresses" class="accordion-collapse collapse" aria-labelledby="headingAddresses" data-bs-parent="#accordionExample">
        <div class="accordion-body">

            {% csrf_token %}
            {{ address_formset.management_form }}
            <table class="table table-hover ">
              <thead>
                <tr>
                  <th></th>
                  <th>Nom abrégé</th>
                  <th>N°</th>
                  <th>Voie</th>
                  <th>Code postal</th>
                  <th>Commune</th>
                  <th>Pays</th>
                </tr>
              </thead>
              <tbody>
                {% for form in address_formset %}
                <tr>
                    <td class = "no-display">{{ form.id }}</td>
                    {% if form.id.value %}            
                      <td><i class='bx bx-trash nav_icon'></i>{{ form.DELETE }}</td>
                    {%else%}
                      <td></td>
                    {%endif%}
                    <td>{{ form.label }}</td>
                    <td>{{ form.street_number}}</td>
                    <td>{{ form.street}} </td>
                    <td>{{ form.postal_code}}</td>
                    <td>{{ form.city}}</td>
                    <td>{{ form.country}}</td>                
                  </tr>
                {% endfor %}
              </tbody>
            </table>
          
        </div>
      </div>
 
    </div>
  </div>
  <div class="bloc"></div>
  <div>
    <button id = "update" name="update" type="submit" class="btn btn-outline-secondary">
    <i class='bx bx-save nav_icon'></i>
    Sauvegarder
    </button>
  </div>

</form>
<style>

/* Header fermé */
.accordion-button.custom-color {
  background-color: #fff;
  color: #000;
}

/* Header ouvert */
.accordion-button.custom-color:not(.collapsed) {
  background-color: #ccc;
  color: #fff;
}

/* Flèche */
.accordion-button.custom-color:not(.collapsed) .accordion-button::after {
  filter: invert(100%) sepia(100%) invert(100%) brightness(200%) grayscale(100%);
}

/* Flèche blanche */
.accordion-button.custom-color:not(.collapsed) .accordion-button::after {
  filter: invert(100%) sepia(100%) invert(100%) brightness(200%) grayscale(100%);
}

/* Flèche par défaut */
.accordion-button.custom-color::after {
  filter: invert(0%) sepia(0%) invert(0%) brightness(100%) grayscale(0%);
}

/* Flèche ouverte blanche */
.accordion-button.custom-color:not(.collapsed)::after {
  filter: invert(100%) sepia(100%) invert(100%) brightness(200%) grayscale(100%);
  color: white;
}

</style>

<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>

<script type="text/javascript">
$(document).ready(function(){
  $("#update").click(function(){
    if ($("input[name*='DELETE']:checked").length > 0) {
      if(confirm("Confirmez-vous vouloir supprimer les lignes sélectionnées?")){
        // Récupérer l'URL dynamique en fonction de l'ID particulier
        var url = "/salesforecast/test/" + $("#themeId").val();
        // Le code pour envoyer la requête POST avec l'URL dynamique
        $.ajax({
          type: "POST",

        });        
      } else {
        return false
      }
    } 
  });
});
</script>
{% endblock content %}

Among the messages I display in the view.py, I get:
business form has changed
and
Les données Affaires sont incorrectes.[{‘id’: [‘Ce champ est obligatoire.’]}, {‘id’: [‘Ce champ est obligatoire.’]}, {‘id’: [‘Ce champ est obligatoire.’]}, {}] -Id field is mandatory, (there are 3 businnesses for the company I am updating)

Any advice and hint is welcome

Richard

Case closed.
I was not using the form.id in the template :frowning: