'Django. How to show only related choices in a two table relationship?

Why does this work? This is hard coded with contrato_id = 1, but the goal dynamicaly present related choices, not the full table model.

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'].queryset=FaturaBlog.objects.filter(id_contrato=1).all()

But none of them works?

a)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'].queryset = FaturaBlog.objects.filter(id_contrato=self.object.pk).all()

Exception Type: AttributeError Exception Value: 'LancamentoBlogForm' object has no attribute 'object'

b)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.id_fatura_choices = [*forms.ModelChoiceField(FaturaBlog.objects.filter(id_contrato=self.object.pk).all().choices)]

Exception Type: AttributeError Exception Value: 'LancamentoBlogForm' object has no attribute 'object'

c)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'] = FaturaBlog.objects.filter(id_contrato=contratoblog__id).all()

Exception Type: NameError Exception Value: name 'contratoblog__id' is not defined

d)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'] = super(LancamentoFormset, self).get_context_data()

Exception Type: AttributeError Exception Value: 'super' object has no attribute 'get_context_data'

BUT I do have context, as return self.render(context) returns data for base.html

context [{'True': True, 'False': False, 'None': None}, {}, {}, {'object': <ContratoBlog: Terceiro Contrato Blog>, 'contratoblog': <ContratoBlog: Terceiro Contrato Blog>, 'form': <django.forms.formsets.LancamentoBlogFormFormSet object at 0x107cfb0d0>, 'view': <blog.views.ContratoLancamentoEdit object at 0x107cfb0a0>, 'contrato_dados': <ContratoBlog: Terceiro Contrato Blog>, 'fatura_list': <QuerySet [<FaturaBlog: c.3.vct.2022-07-15>, <FaturaBlog: c.3.vct.2022-08-15>, <FaturaBlog: c.3.vct.2022-09-15>]>, 'lancamento_list': <QuerySet [<LancamentoBlog: L7.2022-02-01>, <LancamentoBlog: L8.2022-04-01>, <LancamentoBlog: L9.2022-06-01>]>}] 

e)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'] = FaturaBlog.objects.filter(id_contrato = lancamentoblog__id_contrato).all()

Exception Type: NameError Exception Value: name 'lancamentoblog__id_contrato' is not defined

f)

    class LancamentoFormset(forms.ModelForm):
        def __init__(self, *args, **kwargs):
        self.fields['id_fatura'] = FaturaBlog.objects.prefetch_related('id_contrato').all()
        #neither  select_related or prefetch_related works
        #self.fields['id_fatura'] = FaturaBlog.objects.select_related('id_contrato').all()

NO ERROR. BUT queryset does not return any results, the field is empty in the form.

I've been searching and testing for days. Here is the case: the choice field is related to a model that has two forignkey and the desired output is to show only choices from the related contract, previously assingned. If no parameters are given, the choice field works fine, but is show all records insted of just de records related.

So, de goal is to present to the user just the records related to the same contract. This is the expected behavior

    CONTRATO
    ID [ 1,2,3,4,5,6,...]
    
    FATURA
    ID  ID_CONTRATO
    1   1
    2   1
    3   1
    4   2
    5   2
    6   3
    7   3
    
    
    LANCAMENTO
    ID      ID_CONTRATO     ID_FATURA
    1       3               choices should be [6,7]
    2       3               choices should be [6,7]
    3       3               choices should be [6,7]
    4       2               choices should be [4,5]
    5       2               choices should be [4,5]
    6       1               choices should be [1,2,3]
    7       1               choices should be [1,2,3]

For the full picture, de code below is the full setup in Django:

Model CONTRATO register all contracts

Model FATURA register all invoices and it has a ForeignKey with CONTRATO in a relation one-to-many (so one contract has many invoices)

Model LANCAMENTO register records from contracts that will be later assigned to an invoice in FATURA. So LANCAMETNO has a ForeingKey with CONTRATO in a relation on-to-many (so one contract has many recors in LANCAMENTO). And LANCAMENTO also has a ForeignKey with FATURA, that is null by default, so later it will be assigned a FATURA to that record in LANCAEMTNO.

The goal is to have this logic in a form. So when user goes to LANCAMETNO to assign a FATURA, it can choose only FATURA with the same contract_id as LANCAMETNO.

I got here resarching a lot, but this is as far I can go. I'm stuck.

Here's the code, in case someone could point me in the right direction.

Here is my code for Models:

    from django.db import models
    from django.urls import reverse, reverse_lazy
    
    class ContratoBlog(models.Model):
        nome_contrato = models.CharField(max_length=100)
        
        def __str__(self):
            return f"{self.nome_contrato}"
        def get_absolute_url(self):
            return reverse('blog:detalhe_contrato', kwargs={'pk' : self.pk})
    
    
    class FaturaBlog(models.Model):
        datavencimento = models.DateField(null=True, blank=True)
        status = models.CharField(max_length=50, null=True, blank=True)
        id_contrato = models.ForeignKey(ContratoBlog, on_delete=models.CASCADE)
    
        def __str__(self):
            return f"F.{self.id}.V.{self.datavencimento}"
    
    
    class LancamentoBlog(models.Model):
        id_contrato = models.ForeignKey(ContratoBlog, on_delete=models.CASCADE)
        datalancamento = models.DateField(null=True, blank=True)
        detalhe = models.CharField(max_length=100, null=True, blank=True)
        id_fatura = models.ForeignKey(FaturaBlog, on_delete=models.CASCADE, blank=True, null=True)
       
        def __str__(self):
            return f"L{self.id}.{self.datalancamento}"

Here is my code for Views:

    from django.forms.models import inlineformset_factory
    from multiprocessing import context
    from urllib import request
    from django.http import HttpResponseRedirect
    from django.shortcuts import get_object_or_404, render
    from django.urls import reverse, reverse_lazy
    from blog.forms import  ContratoBlogLancamentoBlogFormset, ContratoBlogFaturaBlogFormset
    from blog.models import ContratoBlog, FaturaBlog, LancamentoBlog
    from django.views.generic import TemplateView, FormView, CreateView, ListView, DetailView, UpdateView, DeleteView
    from django.views.generic.detail import SingleObjectMixin
    
    class HomeView(TemplateView):
        template_name = 'blog/home.html'
    
    
    #Views for Model Contrato.
    class ContratoBlogDetailView(DetailView):
        model = ContratoBlog
        template_name = "blog/contratoblog_detail.html"
        def get_context_data(self, **kwargs):
            # Call the base implementation first to get a context
            context = super().get_context_data(**kwargs)
            # Add in a QuerySet Complementary Data
            context['contrato_dados'] = ContratoBlog.objects.get(id=self.object.pk)
            context['fatura_list'] = FaturaBlog.objects.filter(id_contrato=self.object.pk).all()
            context['lancamento_list'] = LancamentoBlog.objects.filter(id_contrato=self.object.pk).all()
            return context
    
    class ContratoBlogFormView():
        pass
    
    #View para Faturas do Contrato
    class ContratoBlogFaturaBlogEditView(SingleObjectMixin, FormView):
    
        model = ContratoBlog
        template_name = 'blog/contrato_fatura_edit.html'
        
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context['fatura_list'] = FaturaBlog.objects.filter(id_contrato=self.object.pk).all()
            return context
    
        def get(self, request, *args, **kwargs):
            self.object = self.get_object(queryset=ContratoBlog.objects.all())
            return super().get(request, *args, **kwargs)
    
        def post(self, request, *args, **kwargs):
            self.object = self.get_object(queryset=ContratoBlog.objects.all())
            return super().post(request, *args, **kwargs)
    
        def get_form(self, form_class=None):
            return ContratoBlogFaturaBlogFormset(**self.get_form_kwargs(), instance=self.object)
    
        def form_valid(self, form):
            form.save()
            return HttpResponseRedirect(self.get_success_url())
    
        def get_success_url(self):
            return reverse('blog:detalhe_contrato', kwargs={'pk': self.object.pk})
         
    
    #---------------------------------
    #View para Lancamentos do Contrato
    class ContratoBlogLancamentoBlogEditView(SingleObjectMixin, FormView):
    
        model = ContratoBlog
        template_name = 'blog/contrato_lancamento_edit.html'
    
        def get(self, request, *args, **kwargs):
            self.object = self.get_object(queryset=ContratoBlog.objects.all())
            return super().get(request, *args, **kwargs)
    
        def post(self, request, *args, **kwargs):
            self.object = self.get_object(queryset=ContratoBlog.objects.all())
            return super().post(request, *args, **kwargs)
    
        def get_form(self, form_class=None):
            return ContratoBlogLancamentoBlogFormset(**self.get_form_kwargs(), instance=self.object)
    
        def form_valid(self, form):
            form.save()
            return HttpResponseRedirect(self.get_success_url())
    
        def get_success_url(self):
            return reverse('blog:detalhe_contrato', kwargs={'pk': self.object.pk})

Here is my code for Fomrs.py

    from importlib.metadata import MetadataPathFinder
    from django.forms import BaseInlineFormSet
    from django import forms
    from django.forms.models import inlineformset_factory
    from blog.models import ContratoBlog, FaturaBlog, LancamentoBlog
    
    class FooModelChoiceField(forms.Form):
        foo_select = forms.ModelChoiceField(queryset=None)
    
        def __init__(self, *args, **kwargs):
            super(FooModelChoiceField, self).__init__(*args, **kwargs)
            qs = FaturaBlog.objects.filter(fatura__id_contrato=self.id_contrato)
            self.fields['foo_select'].queryset = qs
    
    ContratoBlogFaturaBlogFormset = inlineformset_factory(
        ContratoBlog, FaturaBlog, 
        fields=('datavencimento','status', 'id_contrato')
        )
    
    ContratoBlogLancamentoBlogFormset = inlineformset_factory(
        ContratoBlog, LancamentoBlog, 
        fields=('datalancamento', 'detalhe', 'id_fatura')
        )

Here is my code for urls.py

    from django.urls import path, re_path
    from blog.views import HomeView, ContratoBlogDetailView, ContratoBlogFaturaBlogEditView, ContratoBlogLancamentoBlogEditView
    
    app_name = 'blog'
    
    urlpatterns = [
        path('', HomeView.as_view(), name='home'),
        #re_path(r'^contrato/(?P<pk>\d+)$', ContratoBlogDetailView.as_view(), name='detalhe_contrato'),
        path('contrato/<int:pk>', ContratoBlogDetailView.as_view(), name='detalhe_contrato'),
        path('contrato/<int:pk>/fatura/edit/', ContratoBlogFaturaBlogEditView.as_view(), name='contrato_fatura_edit'),
        path('contrato/<int:pk>/lancamento/edit/', ContratoBlogLancamentoBlogEditView.as_view(), name='contrato_lancamento_edit'),

Template 1 - Shows All FATURA and All LANCAMETNO related to CONTRATO:

{% extends 'base.html' %}

{% block content %}
{% include 'blog/contrato_nav.html' %}

<p>
    Contrato ({{contratoblog}}). {{contratoblog.nome_contrato}}.  <BR>
</p>

<p><hr>
    Faturas: <a href="{{ contratoblog.get_absolute_url }}/fatura/edit/">[Editar]</a> <BR>
    <table>
    {% for fatura in fatura_list %}
    <tr>
        <td>[{{ fatura.id }}]</td> <td>Vct. {{ fatura.datavencimento|date:"d M y" }} </td><td>{{ fatura }}</td>  
    </tr>
    {% endfor %}
    </table>
</p>
<p><hr>
    Lan&ccedil;amentos: <a href="{{ contratoblog.get_absolute_url }}/lancamento/edit/">[Editar]</a> <BR>
    <table>
    {% for lancamento in lancamento_list %}
    <tr>
        <td> {{ lancamento.datalancamento|date:"d-M-y" }} </td> <td>{{ lancamento.detalhe }}. </td> <td>{{ lancamento.id_fatura }}</td>
    </tr>
    {% endfor %}
    </table>
</p>
{% endblock %}

Template 2 - Shows All LANCAMETNO related to CONTRATO and but SHOULD show only FATURA related, insted of show all records in fatura.

{% extends 'base.html' %}

{% block content %}

<h2>Lan&ccedil;amentos:</h2>

<hr>
  <form action="" method="post" >
    {% csrf_token %}
    {{ form.non_form_errors }}

    {% for hidden_field in form.hidden_fields %}
      {{ hidden_field.errors }}
      {{ hidden_field }}
      {{ hidden_field.field }}
      {{ hidden_field.help_text }}
    {% endfor %}
     {{ form.management_form }}

    <table>
      <h3>
        {% for hidden_field in form.forms %}
          {{ hidden_field.errors }}
        {% endfor %}
      </h3>
      {% for lancamento_form in form.forms %}
      <h5>
        {% if lancamento_form.instance.id %}
        {% else %}
          {% if form.forms|length > 1 %}
            <!-- Adicionar outro Lan&ccedil;amento -->
          {% else %}
            <!-- Adicionar Lan&ccedil;amento-->
          {% endif %}
        {% endif %}
      </h5>
      <tr><th>Data Lancaamento</th><th>Contrato</th><th>Detalhe</th><th>Fatura</th><th>Deletar</th> </tr>    
        <tr> 
          <td>{{lancamento_form.id}}{{ lancamento_form.datalancamento }}</td> 
          <td>{{ lancamento_form.id_contrato }}</td> 
           <td>{{ lancamento_form.detalhe }}</td> 
           <td>{{ lancamento_form.id_fatura }}</td> 
           <td>{{lancamento_form.DELETE}} Deletar.</td>
        </tr>
      {% endfor %}

    </table>
    <hr>
    <p>
      <button type="submit" value="Update Fatura" >Atualizar Lan&ccedil;amento</button>
      <a href="{{ contrato.get_absolute_url  }}" role="button" >Cancelar</a>
    </p>
  </form>
{% endblock content %}

Well, that's all to it so far. But still couldn/t get it to work as desiered. Does any one has a direction for solution?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source