Anuj TBE
Anuj TBE

Reputation: 9826

Django annotate on property field

I'm using Django 2.0 and Django REST Framework

I have a model like below.

class Contact(models.Model):
    first_name = models.CharField(max_length=100)

class AmountGiven(models.Model):
    contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
    amount = models.FloatField(help_text='Amount given to the contact')

    @property
    def total_payable(self):
        return self.amount

    @property
    def amount_due(self):
        returned_amount = 0
        for returned in self.amountreturned_set.all():
            returned_amount += returned.amount

        return self.total_payable - returned_amount

class AmountReturned(models.Model):
    amount_given = models.ForeignKey(AmountGiven, on_delete=models.CASCADE)
    amount = models.FloadField()

I have to get the top 10 contacts of the amount given and due respectively.

In my view, I'm filtering data like

@api_view(http_method_names=['GET'])
def top_ten(request):
    filter_type = request.query_params.get('type', None)

    if filter_type == 'due':
        # query for due type

    elif filter_type == 'given':
        qs = Contact.objects.filter(
            user=request.user
        ).values('id').annotate(
            amount_given=Sum('amountgiven__amount')
        ).order_by(
            '-amount_given'
        )[:10]

        graph_data = []
        for q in qs:
            d = {}
            contact = Contact.objects.get(pk=q['id'])

            d['contact'] = contact.full_name if contact else 'Unknown'
            d['value'] = q['amount_given']
            graph_data.append(d)

        return Response(graph_data)

    else:
        raise NotFound('No data found for given filter type')

the type query can be due or given.

The code for given type is working fine as all fields are in the database. But how can I filter based on the virtual field for due type?

What I have to do is to annotate Sum of amount_due property group by contact.

Upvotes: 3

Views: 14548

Answers (1)

wiaterb
wiaterb

Reputation: 543

You cannot filter based on @property.

As far as I understand your problem correctly you can aggregate sum of related AmountGiven and sum of AmountReturned, then calculate due field which keep result of subtracting letter and former.

The query:

from django.db.models import Sum, Value
from django.db.models.functions import Coalesce

Contact.objects.filter(  
    amountgiven__amount__gt=0
).annotate(
    due=Sum('amountgiven__amount') - Coalesce(Sum('amountgiven__amountreturned__amount'), Value(0))
).order_by('-due').values_list('due', 'id')

will return:

<QuerySet [{'id': 3, 'due': 2500.0}, {'id': 1, 'due': 2450.0}, {'id': 2, 'due': 1500.0}]>

However with this solution you cannot distinct between many AmountGiven across one Contact. You get big picture like results.

If you want split due value per AmountGiven instance the just annotate like so:

AmountGiven.objects.annotate(
    due=Sum('amount') - Coalesce(Sum('amountreturned__amount'), Value(0))
).order_by('-due').values_list('due', 'contact__id', 'id')

which returns

<QuerySet [
   {'contact__id': 3, 'id': 3, 'due': 2500.0},
   {'contact__id': 1, 'id': 1, 'due': 1750.0},
   {'contact__id': 2, 'id': 2, 'due': 1500.0},
   {'contact__id': 1, 'id': 4, 'due': 350.0},
   {'contact__id': 1, 'id': 5, 'due': 350.0}
]> 

References

  1. Coalesce

Upvotes: 5

Related Questions