Search code examples
pythondjangomodel

Django how get all invoices containing item


I spent over two days looking in docs and internet and cannont find solution. I have models:

class Invoice(models.Model):
(...)

class Product(models.Model):
(...)

class InvoicedItems(models.Model):
    invoice = models.ForeignKey(Invoice, on_delete=CASCADE)
    article = models.ForeignKey(Product, on_delete=CASCADE)

How to get list of all invoices containing one product? I want to make search engine.

I tried to define in InvoicedItems:

    def get_invoices_by_article(article):
        inv_obj = InvoicedItems.objects.filter(article=article)
        inv = inv_obj.invoice
        return inv

But all the time I get error: 'QuerySet' object has no attribute 'invoice'

I know that I am close but I need your help. Thanks in advance!


Solution

  • Your problem is this: 'QuerySet' object has no attribute 'invoice'

    When you do any .filter() call it always returns a QuerySet Obj.. or a fancy list/array

    Outputting in python manage.py shell demonstrates this:

    inv_obj = InvoicedItems.objects.filter(article=article_obj)
    print(inv_obj)
    # <QuerySet [<Article Object (1)>]>
    #    ^ Not an Article Object
    

    So your method will work with some minor changes (and a variable name change)

    def get_invoices_by_article(article):
        inv_list = InvoicedItems.objects.filter(article=article)
        inv = inv_list.first().invoice
        return inv
    

    but! this raises 2 major issues:

    1. What if there's no matching articles?
    2. What if there's multiple?

    Example Solutions:

    def get_invoices_by_article(article):
        inv_list = InvoicedItems.objects.filter(article=article)
    
        count = inv_list.count() 
    
        if count == 0:
            return None
    
        if count > 1:
            # return list of invoices
            return [i.invoice for i in inv_list]
    
        # implied: is 1
        # return single invoice
        return inv_list.first().invoice
    
    
    # Maybe it's easier to ALWAYS return a List, so it's always a single type!
    #   empty or not!
    def get_invoices_by_article(article):
        return [i.invoice for i in InvoicedItems.objects.filter(article=article)]
    

    After you figure out which path you want to take with the method, I'd recommend making it into a Model Manager function instead.

    Current way:

    invoice_item = InvoicedItems.objects.all().first()
    my_return = invoice_item.get_invoices_by_article(article_obj)
    

    You must fetch an InvoiceItem Obj before you can use it -> Extra work!

    Model Manager way:

    my_return = InvoicedItems.objects.get_invoices_by_article(article_obj)
    

    You cut out that db hit!

    Basic Model Manager:
    from django.db import models
    
    class InvoicedItemsManager(models.Manager):
      def get_invoices_by_article(article):
          return [i.invoice for i in  InvoicedItems.objects.filter(article=article)]
    
    
    class InvoicedItems(models.Model):
        invoice = models.ForeignKey(Invoice, on_delete=CASCADE)
        article = models.ForeignKey(Product, on_delete=CASCADE)
    
        objects = InvoicedItemsManager()
    

    Simple as that! Now you can add any number of methods you want for InvoicedItems.objects.


    But maybe a Model Method or a Manager Method is overkill- idk! It's up to you