How to display a model method in a formset?

models.py

class Business(models.Model):
    name = models.CharField('Affaire',max_length=200,)
    customer = models.ForeignKey(Customer,on_delete=models.CASCADE,verbose_name='Client',
        null = True,blank = True)
    contact = models.ForeignKey(Contact,on_delete=models.SET_NULL, null=True)
    account_exec = models.ForeignKey(Account_exec,on_delete=models.SET_NULL, null=True, verbose_name='Chargé d\'affaires')
    amount = models.DecimalField('Montant',max_digits=9, decimal_places=2)
    ponderation = models.ForeignKey(BusinessStatus, on_delete=models.RESTRICT)

    @property
    def amount_pondered(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        amount = self.amount * taux / 100
        return amount

forms.py

class BusinessForm(ModelForm):
    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','amount_pondered','contract_type']
        #widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput,'contract_type': forms.RadioSelect()}
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}
  

Having “amount_pondered” in the fields generates an error: “Unknown field(s) (amount_pondered) specified for Business”. I can get it, as amount_pondered is not to be entered by the user but is computed).

How can I display this “amount_pondered” field when I’m using a BusinessFormset form an inlineformset_factory?

Create that field as a field in the form, not as a reference to a Model field. Set the value of that field in the __init__ method of the form. You’ll probably also want to mark that field as disabled.

1 Like

Could you give me the code for the init of this field "amount_pondered " as it is defined in the model class?

Actually, I’m going to correct my previous reply. It’s not necessary to define code in the __init__ method for this.

Reviewing the docs for the initial attribute of a field reminds me that you can pass a callable as that attribute. Review the last example in that section of the docs.

1 Like

I still don’t see how to initialize the value of amount_pondered…

class BusinessForm(ModelForm):
    amount_pondered = forms.CharField(disabled=True)
    amount_pondered = ???
    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','contract_type']
        #widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput,'contract_type': forms.RadioSelect()}
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}

In the example in the referenced doc, how are they setting an initial value for day?

1 Like

in the example: day = forms.DateField(initial=datetime.date.today). That’s pretty straight forward.
But in my case, as amount_pondered is defined as follows in the model, I don’t see how to do.

    def amount_pondered(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        amount = self.amount * taux / 100
        return amount

The import statement prior to this imports datetime.

If you’re not familiar with it, datetime.date.today is the name of a function to return the current day.

Specifically:

day = forms.DateField(initial=datetime.date.today)
                                            ^^^^^
# Name of the function to call to get the initial value for this field.

What’s the name of the function that you want to call to initialize your field? What do you think then your field definition might look like?

If you know you’re always going to be provided an instance of Business in the creation of the formset, you can use the name of that function based on that instance.

If you have a situation where you’re not providing an instance to the formset, then you would need to create a different function in your form to call your function if the instance is provided, and to return a zero value otherwise.

I can’t be more specific without seeing the views and formsets involved.

1 Like

Let me try to explain the context

Models.py

class BusinessStatus(models.Model):
    label = models.CharField('Libellé',max_length=50)
    weight = models.IntegerField('Taux', default=10)
    final = models.BooleanField(default=False)
    
    def text(self):
        return self.label + ' (' + str(self.weight) + '%)'

    class Meta:
        ordering = ['weight']
        verbose_name = 'Etat'

    def __str__(self):
        """Cette retourne une chaîne de caractère pour identifier l'instance de la classe d'objet."""
        return self.label

class Business(models.Model):
    name = models.CharField('Affaire',max_length=200,)
    customer = models.ForeignKey(Customer,on_delete=models.CASCADE,verbose_name='Client',
        null = True,blank = True)
    contact = models.ForeignKey(Contact,on_delete=models.SET_NULL, null=True)
    account_exec = models.ForeignKey(Account_exec,on_delete=models.SET_NULL, null=True, verbose_name='Chargé d\'affaires')
    amount = models.DecimalField('Montant',max_digits=9, decimal_places=2)
    ponderation = models.ForeignKey(BusinessStatus, on_delete=models.RESTRICT)

    @property
    def amount_pondered(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        amount = self.amount * taux / 100
        return amount
    
    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'

The view which allows to display and modifiy all the businesses of a given customer

class CustomerUpdateView(UpdateView):
    template_name = 'gestcom/customer_update_form.html'
    model = Customer
    fields = '__all__'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        customer = get_object_or_404(Customer, pk=self.kwargs['pk']) 
        context['customer_name']=customer.name 
        context['customer_form'] = CustomerForm(instance = customer)
        BusinessFormSet = inlineformset_factory(Customer,Business,can_delete=True,extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=customer)
        context['business_formset'] = business_formset
        return context

    def post(self, request, *args, **kwargs):
        # récupération de la clé primaire depuis l'url
        pk = self.kwargs['pk'] 
        # récupération de l'enregistrement Theme à partir de pk
        customer = get_object_or_404(Customer, pk=pk)      
        # création des forms
        customer_form = CustomerForm(instance=customer, data=request.POST)
        BusinessFormSet = inlineformset_factory(Customer, Business, can_delete=True, extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=customer, data= request.POST)      
        if 'update' in request.POST:
            if customer_form.is_valid():
                if business_formset.is_valid():
 messages.success(request,'Les données ont été mises à jour')
                            customer_form.save()
                            business_formset.save()
                else:
                    messages.warning(request, request.POST)
                    messages.warning(request, 'Données affaires invalides: ' + str(business_formset.errors))
            else:
                messages.warning(request, request.POST)
                messages.warning(request, 'Données de client invalides:' + str(customer_form.errors))
        return HttpResponseRedirect(reverse('customer_update_form', args=(customer.id,)))      

Sorry the previous message is uncomplete.
Here again,

Models.py:

class BusinessStatus(models.Model):
    label = models.CharField('Libellé',max_length=50)
    weight = models.IntegerField('Taux', default=10)
    final = models.BooleanField(default=False)
    
    def text(self):
        return self.label + ' (' + str(self.weight) + '%)'

    class Meta:
        ordering = ['weight']
        verbose_name = 'Etat'

    def __str__(self):
        """Cette retourne une chaîne de caractère pour identifier l'instance de la classe d'objet."""
        return self.label

class Business(models.Model):
    name = models.CharField('Affaire',max_length=200,)
    customer = models.ForeignKey(Customer,on_delete=models.CASCADE,verbose_name='Client',
        null = True,blank = True)
    contact = models.ForeignKey(Contact,on_delete=models.SET_NULL, null=True)
    account_exec = models.ForeignKey(Account_exec,on_delete=models.SET_NULL, null=True, verbose_name='Chargé d\'affaires')
    amount = models.DecimalField('Montant',max_digits=9, decimal_places=2)
    ponderation = models.ForeignKey(BusinessStatus, on_delete=models.RESTRICT)

    @property
    def amount_pondered(self):
        status = BusinessStatus.objects.get(pk=self.ponderation_id)
        taux = status.weight
        amount = self.amount * taux / 100
        return amount
    
    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'

The forms which are involved. No way to put amount_pondered in the BusinessForm model related fields. So I have added it with no reference to the model and set it to “disabled”, as you suggested. But I don’t know how to initialize it to the right value, meaning:
For a given business, the field “amount_pondered” which is based on fields on the current instance, amount_poundered = amount * taux / 100, where
taux = BusinessStatus.objects.get(pk=self.ponderation_id).weight

class CustomerForm(ModelForm):
    name = forms.CharField(
        widget=forms.TextInput(attrs={'autofocus': True}))    

    class Meta:
        model= Customer
        fields = '__all__'
        widgets = {'creation_date': DatePickerInput}   

class BusinessForm(ModelForm):
    amount_pondered = forms.CharField(disabled=True)
    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','contract_type']
        #widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput,'contract_type': forms.RadioSelect()}
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}

View which allows to display, update, create and delete businesses for a given customer.

class CustomerUpdateView(UpdateView):
    template_name = 'gestcom/customer_update_form.html'
    model = Customer
    fields = '__all__'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        customer = get_object_or_404(Customer, pk=self.kwargs['pk']) 
        context['customer_name']=customer.name 
        context['customer_form'] = CustomerForm(instance = customer)
        BusinessFormSet = inlineformset_factory(Customer,Business,can_delete=True,extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=customer)
        context['business_formset'] = business_formset  
        return context

    def post(self, request, *args, **kwargs):
        # récupération de la clé primaire depuis l'url
        pk = self.kwargs['pk'] 
        # récupération de l'enregistrement Theme à partir de pk
        customer = get_object_or_404(Customer, pk=pk)      
        # création des forms
        customer_form = CustomerForm(instance=customer, data=request.POST)
        BusinessFormSet = inlineformset_factory(Customer, Business, can_delete=True, extra=1, form = BusinessForm)
        business_formset = BusinessFormSet(instance=customer, data= request.POST)      
        if 'update' in request.POST:
            if customer_form.is_valid():
                if business_formset.is_valid():
                            messages.success(request,'Les données ont été mises à jour')
                            customer_form.save()
                            business_formset.save()
                            contact_formset.save()
                            address_formset.save()                   
                else:
                    messages.warning(request, request.POST)
                    messages.warning(request, 'Données affaires invalides: ' + str(business_formset.errors))
            else:
                messages.warning(request, request.POST)
                messages.warning(request, 'Données de client invalides:' + str(customer_form.errors))
        return HttpResponseRedirect(reverse('customer_update_form', args=(customer.id,)))      

To make it short, I don’t know how to pass the instance to my callable.
In the following trial, 70 is well displayed. But how do I pass the right arguments to init_amount_pondered to make it work as I want? I guess I’m missing something big :frowning:

class BusinessForm(ModelForm):
    def init_amount_pondered():
        return 70
    
    amount_pondered = forms.CharField(disabled=True, initial=init_amount_pondered())
    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','contract_type']
        #widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput,'contract_type': forms.RadioSelect()}
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}

We’re close, and heading in the right direction.

First a couple things to clean up:

  • You have your function defined as:

This is the definition of a method inside a class. As a result, it will be passed the instance of the class as the first parameter. By convention, this parameter is named self. That means the definition of this function should be:
def init_amount_pondered(self):

  • The initial parameter should be passed a callable, and not the output of the function.

Notice the example above regarding today, they aren’t passing today(), they’re passing today. Also, your reference should be to the function for the current instance, also referred to by self. As a result, the parameter should be:

initial=self.init_amount_pondered

(If this doesn’t make any sense to you, I suggest you review the docs at https://docs.python.org/3/tutorial/classes.html.)

  • Do yourself a favor here and remove the @property decorator on your amount_pndered function. There are almost no circumstances where it’s required and may actually make things more difficult here.

Now, what happens in a modelformset is that a form is generated for each instance of the model in the formset. A ModelForm has access to the instance of the model as self.instance.

This means that you should be able access the amount_pondered function in the form as self.instance.amount_pondered

e.g. return self.instance.amount_pondered().

If you are only ever going to create this formset for pre-existing instances (never having a blank form), you could reference this function directly in the initial parameter of the field:
initial = self.instance.amount_pondered
But this would throw an error if you ever try to render a blank form (no pre-existing instance).

So I would suggest that you put this in the init_amount_pondered function, guarded by an if statement, e.g. if self.instance:. The else clause for that if can return whatever value you’d like.

in the following, in the “initial=self.init_amount_pondered”, VS Code raises an error on “self”, as a variable not defined… There is still something I’m missing…

class BusinessForm(ModelForm):
    def init_amount_pondered(self):
        if self.instance:
            status = BusinessStatus.objects.get(pk=self.ponderation_id)
            taux = status.weight
            taux = 30
            amount = self.amount * taux / 100
        else:
            amount = 0
        return amount
    
    amount_pondered = forms.CharField(disabled=True, initial=self.init_amount_pondered)
    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','contract_type']
        #widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput,'contract_type': forms.RadioSelect()}
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}

That’s probably just a VSCode issue. Is it otherwise working?

It may not, in which case you would need to make a couple changes, but I’d try this first.

Also , you moved the functionality from the model to your form, which is unnecessary.

Hi Ken,

Unfortunately,it’s not a VS Code issue, Python shows the same problem

forms.py", line 73, in BusinessForm
amount_pondered = forms.CharField(disabled=True, initial=self.init_amount_pondered)
^^^^
NameError: name ‘self’ is not defined

Any idea of the changes to make?

I sincerely want to apologize for chasing you down a wrong path. Apparently this is a lesson I need to be reminded of periodically - I probably make this mistake about every year or so.

I should have stayed with my original thought.

This was my first answer - and the direction I should have stayed with. I shouldn’t have second-guessed myself.

Anyway, what you’re looking for should probably look something like this:

class BusinessForm(ModelForm):
    def init_amount_pondered(self):
        if self.instance.pk:
            amount = self.instance.amount_pondered()
        else:
            amount = 0
        return amount
    
    amount_pondered = forms.CharField(disabled=True)

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

    class Meta:
        model = Business
        fields = ['name', 'customer', 'account_exec', 'amount','ponderation','contract_type']
        widgets = {'creation_date': DatePickerInput,'signature_date': DatePickerInput}

Again, I’m really sorry for this.

Hello Ken

Yes! it works!!!
Thank you very much, Y’ve been very, very helpfull.
And don’t be sorry, our chat has allowed me to better understand what’s going on behind the scene.

Thanks again for being so responsive and patient!

Richard