简体   繁体   中英

How to display only one record from child model for each header item in Django ListView using distinct()

I am using two related models in my Django application. The objects so created in the models are being displayed using the listview class. In the child model I can create multiple rows based on some key date. When I try to display values from both the models, all the child objects for the respective FK fields are displayed (wherever more than one records are there).

Let me make the situation clearer as below:

models.py

class MatPriceDoc(models.Model):
    mat_price_doc_number = models.IntegerField(null=True, blank=True.....)
    mat_num = models.ForeignKey(Material, on_delete=.......)
    mat_group = models.ForeignKey(MatGrp, on_delete=.......)
    doc_type = models.CharField(max_length=2, null=True, default='RF'....)
    create_date = models.DateField(default=timezone.now,...)

class MatPriceItems(models.Model):
    price_doc_header = models.ForeignKey(MatPriceDoc, on_delete=...)
    price_item_num = models.CharField(max_length=3, default=10,...)
    price_valid_from_date = models.DateField(null=True, verbose_name='From date')
    price_valid_to_date = models.DateField(null=True, verbose_name='To date')
    mat_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True,...)

views.py

class MatPriceListView(ListView):
    template_name = "mat_price_list.html"
    context_object_name = 'matprice'
    model = MatPriceDoc

    def get_context_data(self, **kwargs):
        context = super(MatPriceListView, self).get_context_data(**kwargs)
        context.update({
            'mat_price_item_list': MatPriceItems.objects.distinct().order_by('price_doc_header'),  #This is where I have tried **distinct()**
        })
        return context

    def get_queryset(self):
        return MatPriceDoc.objects.order_by('mat_num')

Now the material price changes periodically and there would always be more than one price item for each material for each key date ( price_valid_from_date ). My objective is to display the latest price of all those materials for which price documents exist. My predicament is how to pick up only one of the many price items for the same material / document combination. My code line 'mat_price_item_list': MatPriceItems.objects.distinct().order_by('price_doc_header'), is of course not yielding any result (all price items are being displayed in successive columns).

Is there a way to show only one price item in the listview?

Edit

In the following image the prices maintained for various dates for materials are shown. What I was trying to get was only the price for the latest (valid) date is displayed for a particular material. So in the instant case (as displayed), prices for only 4th March 2020 should be displayed for each item MS and HSD .

在此处输入图像描述

Edit 2

This is how the child model data for an instance of header doc number (no. 32 here) looks like (image grab from the child table using data browser):

在此处输入图像描述

The columns are: Child obj. item no. / Price / Valid From / Valid To / Hdr Doc no. / Child Obj Row no.

My thinking was : Can I not pick up only the first object (a subset) from a collection of doc number? In the instant case (ref the image), doc no. 32 has three child items (bearing number 148 , 149 and 156 ). Is it not possible to only pick up item number 156 and discard the rest?

I tried:

MatPriceItems.objects.order_by('-price_valid_to_date').first()

but raises error "MatPriceItems" object is not iterable .

What would enable me to get the only item for a header item and display it?

I believe you need the following:

class MatPriceListView(ListView):
    template_name = "mat_price_list.html"
    context_object_name = 'matprice'
    model = MatPriceDoc

    def get_context_data(self, **kwargs):
        context = super(MatPriceListView, self).get_context_data(**kwargs)
        context.update({
            'mat_price_item_list': MatPriceItems.objects.all().('-price_doc_header').first(),
        })
        return context

    def get_queryset(self):
        return MatPriceDoc.objects.order_by('mat_num')

The key line change being:

MatPriceItems.objects.all().order_by('-price_doc_header').first()

Such that you order by the price_doc_header descending (hence the minus infront of that value). Then take the .first() in the returned <QuerySet> object.

Likewise, the following would also be valid:

MatPriceItems.objects.all().order_by('price_doc_header').last()

You could also make use of Django's built in aggregation in the ORM:

from django.db.models import Max

MatPriceItems.objects.all().aggregate(Max('price_doc_header'))

Putting this all together, I would say the best solution for readabiltiy of the code would be using Django's built-in Max function:

from django.db.models import Max

class MatPriceListView(ListView):
    template_name = "mat_price_list.html"
    context_object_name = 'matprice'
    model = MatPriceDoc

    def get_context_data(self, **kwargs):
        context = super(MatPriceListView, self).get_context_data(**kwargs)
        context.update({
            'mat_price_item_list': MatPriceItems.objects.all().aggregate(Max('price_doc_header'))
        })
        return context

    def get_queryset(self):
        return MatPriceDoc.objects.order_by('mat_num')

Because it tells any colleague exactly what you're doing: taking the maximum value.

Update : On a second glance of your question - I believe you may also need to do some sort of .filter() ...then make an .annotate() for the Max ...

I'm also adding what I now believe is the correct answer based on a re-reading of your question.

I feel like the query needs to be ammended to:

MatPriceItems.objects.all().select_related(
    'mat_price_docs'
).order_by(
    'price_doc_header__mat_num'
)

ie,

class MatPriceListView(ListView):
    template_name = "mat_price_list.html"
    context_object_name = 'matprice'
    model = MatPriceDoc

    def get_context_data(self, **kwargs):
        context = super(MatPriceListView, self).get_context_data(**kwargs)
        context.update({
            'mat_price_item_list': MatPriceItems.objects.all().select_related('mat_price_docs').order_by('price_doc_header__mat_num'),
        })
        return context

    def get_queryset(self):
        return MatPriceDoc.objects.order_by('mat_num')

To achieve this, you also add the related_name argument to the price_doc_header ForeignKey field.

class MatPriceItems(models.Model):
    price_doc_header = models.ForeignKey(MatPriceDoc, related_name='mat_price_docs', on_delete=...)
    price_item_num = models.CharField(max_length=3, default=10,...)
    price_valid_from_date = models.DateField(null=True, verbose_name='From date')
    price_valid_to_date = models.DateField(null=True, verbose_name='To date')
    mat_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True,...)

As always, any changes to your models - please re-run the makemigarations and migrate management commands.

Edit : You may even be able to define a new queryset using an annotation to link to the ForeignKey:

def get_queryset(self):
        return MatPriceItems.objects.all().order_by(
                    'price_doc_header__mat_num'
               ).annotate(
                    mat_price_doc_number = F('price_doc_header__mat_price_doc_number')
                    ...
               )

NB You can import the F() expression as from django.db.models import F

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM