foreign key as hidden input in a formset

Hello,

I have a view which prepares different formsets (using a different relations between a “Business” model and a “Company” model, either “as_customer” or “as_beneficiary” to display businesses depending on the user choice. Using the same template, I get a a behaiviour I do not understand.

When displaying a queryset based on a “customer” relationship, the customer foreign key for the business is an input element with hidden type and the beneficiary element is a “regular” select field.
When displayinf the queryset based on the “beneficiary” relationship, the customer foreign ley is a regular input elemet and the “beneficiary” field is hidden!

Any clue of what is going on?
I would like to get both fields “customer” and “beneficiary” to be regular inputs in both cases (as_customer and as_beneficiary).

Here is an extract of the code:

Models involved are:

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 weighted_amount(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        if taux:
            amount = self.amount * taux / 100
        else:
            amount = 0
        return amount
    weighted_amount.short_description = "Montant pondéré"
    
    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
    amount_to_plan.short_description = "Facturation à planifier"
    
    def billed_amount(self):
        return self.invoice_set.filter(is_issued=True).aggregate(total=Sum('amount'))['total'] or 0
    billed_amount.short_description = 'Total des montants facturés'
    
    def collected_amount(self):
        return self.invoice_set.filter(is_paid=True).aggregate(total=Sum('amount'))['total'] or 0
    collected_amount.short_description = "Total des montants encaissés"
     
    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
    amount_to_bill.short_description = "Montant à facturer"
    
    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'
        verbose_name_plural = "Affaires"

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.weighted_amount()
    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', kwargs={'company_pk': self.pk})

def get_contacts_absolute_url(self):
    return reverse('company_contacts_update', kwargs={'company_pk': self.pk})

def get_addresses_absolute_url(self):
    return reverse('company_addresses_update', kwargs={'company_pk': self.pk})

Extract of the view building the formset:

if request.POST.get('flexRadioCustomerBeneficiary') == 'as_customer':
            as_customer = True
            as_customer_checked = 'checked'
            as_beneficiary_checked = ''
            previous_view = 'as_customer'
            print('as_customer')
            business_queryset = Business.objects.filter(customer=company).order_by(
                Case(
                    When(ponderation__label="Terminée", then=Value(0)),
                    When(ponderation=0, then=Value(1)),
                    default='ponderation__weight',
                    output_field=IntegerField()
                ).desc(),
                '-ponderation__weight'
            )
            BusinessFormSet = inlineformset_factory(Company, Business, fk_name='customer',extra=0, can_delete=False, form = BusinessForm)
            business_formset = BusinessFormSet(instance=company, queryset=business_queryset, data=request.POST)
            print('business formset avec request.POST')
        else:
            as_beneficiary = True 
            as_customer_checked = ''
            as_beneficiary_checked = 'checked'
            previous_view = 'as_beneficiary'
            business_queryset = Business.objects.filter(beneficiary=company).order_by(
                Case(
                    When(ponderation__label="Terminée", then=Value(0)),
                    When(ponderation=0, then=Value(1)),
                    default='ponderation__weight',
                    output_field=IntegerField()
                ).desc(),
                '-ponderation__weight'
            )
            print('as_beneificiary')
            BusinessFormSet = inlineformset_factory(Company, Business, fk_name='beneficiary',extra=0, can_delete=False, form = BusinessForm)
            business_formset = BusinessFormSet(instance=company, queryset=business_queryset, data=request.POST)
          

And the code handling the Business form is:

class BusinessForm(ModelForm):
    def clean(self):
        print('clean business')
        cleaned_data = super().clean()
        name = cleaned_data.get('name')
        account_exec = cleaned_data.get('account_exec')
        amount = cleaned_data.get('amount')
        if not(name):
            msg = ('Veuillez compléter le nom de l\'affaire')
            self.add_error('name', msg)
        if not (account_exec):
            msg = ('Veuillez choisir le reponsable de l\'affaire')
            self.add_error('account_exec', msg)            
        if not (amount):
            msg = ('Veuillez définir le montant de l\'affaire')
            self.add_error('amount', msg)
            print('montant manquant')
        return cleaned_data        
    
    # fonctions d'initialisation des valeurs calculées        
    def init_calculated_fields(self):
        if self.instance.pk:
            self.fields['weighted_amount'].initial = self.instance.weighted_amount()
            self.fields['amount_to_plan'].initial = self.instance.amount_to_plan()
        else:
            self.fields['weighted_amount'].initial = 0
            self.fields['amount_to_plan'].initial = 0
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # On initialise les montants calculés
        self.init_calculated_fields()
        
        # Format d'affichage des champs
        for _, value in self.fields.items():
           value.widget.attrs['class'] = 'form-control text-primary' 
        field_attrs = ['name', 'framework_agreement', 'po_market', 'account_exec', 'ponderation', 'amount',
                       'weighted_amount', 'amount_to_plan']
        for field_attr in field_attrs:
            self.fields[field_attr].widget.attrs.update({'class': 'form-control text-primary w-50'})
        if self.instance.pk:
            if self.instance.amount_to_plan() > 0:
                self.fields['amount_to_plan'].widget.attrs.update({'class': 'form-control text-danger w-50'})
        self.fields['name'].widget.attrs.update({'class': 'form-control text-primary w-200'})
        self.fields['name'].widget.attrs.update({'placeholder': 'Saisir une nouvelle affaire'})
        
        self.fields['weighted_amount'].disabled = True
        self.fields['amount_to_plan'].disabled = True
       
        # 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
            self.fields['framework_agreement'].initial = framework_agreement.id
        if self.instance.po_market:
            # Si oui, on réordonne les choix pour mettre le marché à BC associé en premier
            po_market = self.instance.po_market
            self.fields['po_market'].initial = po_market.id           

    weighted_amount = forms.DecimalField(disabled=True, label="Montant pondéré")
    amount_to_plan = forms.DecimalField(disabled = True, label="Montant à planifier")
    
    class Meta:
        model = Business
        fields = ['name', 'customer', 'beneficiary','framework_agreement','po_market','account_exec', 'amount','ponderation',]
        widgets = {
            'creation_date': DatePickerInput,
            'signature_date': DatePickerInput
        }

Here is the template block rendering the businesses:

<div class="row line-spacing mx-2">
    <div class="col-md-3">
      <div class="d-flex align-items-center">
        <i class="no-display">{{ business_form.id }}</i>
        {% if business_form.instance.id %}
          <a href="/salesforecast/businesses/{{business_form.instance.id}}" title="Gérer les échéances prévisionnelles de facturation">
            <i class="bx bxs-detail nav_icon"></i>
          </a>
        {% endif %}
        <b>{{ business_form.name }}</b>
      </div>
      <div class="fieldWrapper">{{ business_form.name.errors }}</div>
    </div>
    <div class="col-md-3">
      <div class="d-flex align-items-center">
        <label for="{{ business_form.customer.id_for_label }}">{{ business_form.customer.label }}</label>
        {{ business_form.customer }}
      </div>
      <div class="fieldWrapper">{{ business_form.customer.errors }}</div>
    </div>
    <div class="col-md-3">
      <div class="d-flex align-items-center">
        <label for="{{ business_form.beneficiary.id_for_label }}">{{ business_form.beneficiary.label }}</label>
        {{ business_form.beneficiary }}
      </div>
      <div class="fieldWrapper">{{ business_form.beneficiary.errors }}</div>
    </div>
    <div class="col-md-3">
      <div class="d-flex align-items-center">
        <label for="{{ business_form.account_exec.id_for_label }}">Responsable</label>
        {{ business_form.account_exec }}
      </div>
      <div class="fieldWrapper">{{ business_form.account_exec.errors }}</div>
    </div>
</div>
  <div class="row  line-spacing mx-2"> 
    <div class="col-md-3 ">
      <label for="{{business_form.amount.id_for_label}}">{{business_form.amount.label}}</label>          
      {{ business_form.amount }}<div class="fieldWrapper">{{business_form.amount.errors}}</div>
    </div>
    <div class="col-md-3">
      <label for="{{business_form.ponderation.id_for_label}}">{{business_form.ponderation.label}}</label>          
      {{ business_form.ponderation }}<div class="fieldWrapper">{{business_form.ponderation.errors}}</div>
    </div>

    {%if business_form.weighted_amount.value != 0 %}
      <div class="col-md-3">
        <label for="{{business_form.weighted_amount.id_for_label}}">{{business_form.weighted_amount.label}}</label>          
        {{ business_form.weighted_amount }}
      </div>
    {%endif%}
    {%if business_form.amount_to_plan.value != 0 %}
      <div class="col-md-3">
        <label for="{{business_form.amount_to_plan.id_for_label}}">{{business_form.amount_to_plan.label}}</label>          
        {{ business_form.amount_to_plan }}
      </div> 
    {%endif%}
  </div>
  <div class="row line-spacing mx-2">
    <div class="col-md-1 d-flex align-items-center">
      <label for="{{business_form.framework_agreement.id_for_label}}">Accord-cadre</label>
    </div>
    <div class="col-md-5 d-flex align-items-center">
      {{business_form.framework_agreement}}<div class="fieldWrapper">{{business_form.framework_agreement.errors}}</div>
      {% if business_form.instance.id %}
        <!-- icones d'ajout et modification d'accord-cadre -->
      <a href="{% url 'cru_framework_agreement' business_pk=business_form.instance.id framework_agreement_pk=0%}?origin={{origin}}" 
            title="Ajouter un accord-cadre et y associer cette affaire" class="d-inline-block">
          <i class='bx bxs-plus-circle nav_icon'></i>
        </a>
        {% if business_form.framework_agreement.value %}
          <a href="{% url 'cru_framework_agreement' business_pk=business_form.instance.id framework_agreement_pk=business_form.framework_agreement.value %}?origin={{origin}}" 
              title="Modifier l'accord-cadre" class="d-inline-block">
            <i class='bx bxs-pencil nav_icon'></i>
          </a>
        {% endif %}                    
      {% endif %}              
    </div>
    <div class="col-md-2 d-flex align-items-center">
      <label for="{{business_form.po_market.id_for_label}}">Marché à bons de commande</label>
    </div>
    <div class="col-md-4 d-flex align-items-center">
      {{business_form.po_market}}<div class="fieldWrapper">{{business_form.po_market.errors}}</div>
      {% if business_form.instance.id %}
        <!-- icones d'ajout et modification de marché à bon de commande -->
        <a href="{% url 'cru_po_market' business_pk=business_form.instance.id po_market_pk=0%}?origin={{origin}}" 
            title="Ajouter un marché à bons de commande et y associer cette affaire" class="d-inline-block" >
          <i class='bx bxs-plus-circle nav_icon'></i>
        </a>
        {% if business_form.po_market.value %}
          <a href="{% url 'cru_po_market' business_pk=business_form.instance.id po_market_pk=business_form.po_market.value %}?origin={{origin}}"
              title="Modifier le marché à bons de commande" class="d-inline-block">
            <i class='bx bxs-pencil nav_icon'></i>
          </a>
        {% endif %} 
      {% endif %}   
    </div>
    <div class="line"></div>
  </div>

Is that portion of the view you posted to handle the get of the initial display of the formsets or the post of the data from the submitted formsets?

Is this a case where “page 1” has posted some data, and you’re using that posted data to prepare this “page 2” for display?

Note - since this is an inlineformset, the relationship from the “child object” back to the “parent object” is fixed. If you have a Company, then the set of Business related to that Company shown in an inlineformset should not be able to alter that relationship. If you want to be able to alter both FK fields in a set of Business, then you would want to create a regular modelformset for those Business objects.

Hello Ken,

Thanks for your answer. The piece of view code I provided handles the POST. The one handling the GET is the following:

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['origin'] = "customer_update" # permet de revenir à cette page pour les ajouts/modif d'accord cadre et marché à BC
        company = get_object_or_404(Company, pk=self.kwargs['company_pk']) 
        context['company_form'] = CompanyForm(instance = company)
        Framework_agreementFormset = inlineformset_factory(Company, Framework_agreement, form = Framework_agreementForm)
        fa_formset = Framework_agreementFormset(instance=company)
        context['fa_formset']= fa_formset
        PO_marketFormset = inlineformset_factory(Company,PO_market, form = PO_marketForm)
        po__formset = PO_marketFormset(instance=company)
        context['po_formset'] = po__formset
        context['previous_view'] = 'as_customer'
        context['as_customer_checked'] = 'checked'
        context['as_beneficiary_checked'] = ''
        
        BusinessFormSet = inlineformset_factory(Company,Business,fk_name='customer', extra=0, can_delete=False,form = BusinessForm)
        # Obtenir la queryset des Business triés par le poids (ponderation) de BusinessStatus en ordre décroissant
        # en plaçant les objets avec label="Terminée" à la fin
        business_queryset = Business.objects.filter(
            customer=company
        ).order_by(
            Case(
                When(ponderation__label="Terminée", then=Value(0)),
                When(ponderation=0, then=Value(1)),
                default='ponderation__weight',
                output_field=IntegerField()
            ).desc(),
            '-ponderation__weight'
        )
        business_formset = BusinessFormSet(instance=company, queryset=business_queryset)
        context['business_formset'] = business_formset
               
        return context

By default, I display a view “as_customer” - and the customer field is hidden as well.

If I understant correctly your note ( Note - since this is an inlineformset, the relationship from the “child object” back to the “parent object” is fixed. If you have a Company , then the set of Business related to that Company shown in an inlineformset should not be able to alter that relationship. If you want to be able to alter both FK fields in a set of Business , then you would want to create a regular modelformset for those Business objects), that means that when using an inlineformset, you cannot alter the foreign key and a good way to prevent any alteration is to hide the field!!! That’s fine for me, as I have a path to display and update a single business. I just have to hide the label of the foreign key field in the template rendering the formset!

Do you confirm my understanding?

Confirmed, except:

making it hidden doesn’t prevent alteration. It’s still possible for someone to alter the value of that field. However, I believe Django handles that possibility in the formset, so that even if the user were to alter the form, it shouldn’t update the field. (conjecture, don’t know that for certain.)