Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Problem in doing complex annotation and aggregation

This is model:

class Purchase(models.Model):
    date           = models.DateField(default=datetime.date.today,blank=False, null=True)
    total_purchase = models.DecimalField(max_digits=10,decimal_places=2,blank=True, null=True)

I want to perform a month wise calculation of "total_purchase" within a specific daterange in such a way that if there is no purchase in a month the total purchase should be the previous month purchase value And if there is purchase in two months then total purchase will the addition of those two...

Example:

Suppose the date range given by user is from month of April to November.

If there is a Purchase of $2800 in month of April and $5000 in month of August and $6000 in month of October.

Then the output will be like this:

April      2800
May        2800
June       2800
July       2800
August     7800  #(2800 + 5000)
September  7800
October    13800 #(7800 + 6000)
November   13800

Any idea how to perform this in django queries?

Thank you

According to the answer given by Mr.Raydel Miranda. I have done the following

import calendar
import collections
import dateutil

start_date = datetime.date(2018, 4, 1)
end_date = datetime.date(2019, 3, 31)

results = collections.OrderedDict()

result = Purchase.objects.filter(date__gte=start_date, date__lt=end_date).annotate(real_total = Case(When(Total_Purchase__isnull=True, then=0),default=F('tal_Purchase')))

date_cursor = start_date

while date_cursor < end_date:
    month_partial_total = result.filter(date__month=date_cursor.month).agggate(partial_total=Sum('real_total'))['partial_total']

    results[date_cursor.month] = month_partial_total

    if month_partial_total == None:
            month_partial_total = int(0)
    else:
            month_partial_total = month_partial_total

    date_cursor += dateutil.relativedelta.relativedelta(months=1)

    return results

But now the output is coming like this(from the example above):

April      2800
May        0
June       0
July       0
August     5000
September  0
October    6000
November   0

Do anyone have any idea how to add between the months... I want to do something like

e = month_partial_total + month_partial_total.next

I want to add the next iteration value of every month_partial_total. I think this will solve my problem..

Any idea anyone how to perform this in django?

Thank you

like image 499
Niladry Kar Avatar asked Mar 04 '23 18:03

Niladry Kar


1 Answers

I've noted two things in your question:

  1. Results are ordered by month.
  2. Total purchase can be blank or null.

Based on those things I'll propose this approach:

You could get the total for a given month, you just need to handle the case where the total_pushase is null (as a side note, it does not make any sense to have an instance of Purchase where total_purchase is null, at least it must be 0).

Read about Django Conditional expressions to learn more about When and Case.

# Annotate the filtered objects with the correct value (null) is equivalent
# to 0 for this requirement.

result = Purchase.objects.filter(date__gte=start_date, date__lt=end_date).annotate(
    real_total = Case(
        When(total_purchase__isnull=True, then=0),
        default=F('total_purchase')
    )
)

# Then if you want to know the total for a specific month, use Sum.
month_partial_total = result.filter(
    date__month=selected_month
).aggregate(
    partial_total=Sum('real_total')
)['partial_total']

You could use this in a function to achieve you wanted result:

import calendar
import collections
import dateutil

def totals(start_date, end_date):
    """
    start_date and end_date are datetime.date objects.
    """

    results = collections.OrderedDict()  # Remember order things are added.

    result = Purchase.objects.filter(date__gte=start_date, date__lt=end_date).annotate(
        real_total = Case(
            When(total_purchase__isnull=True, then=0),
            default=F('total_purchase')
        )
    )

    date_cursor = start_date
    month_partial_total = 0
    while date_cursor < end_date:
        # The while statement implicitly orders results (it goes from start to end).
        month_partial_total += result.filter(date__month=date_cursor.month).aggregate(
            partial_total=Sum('real_total')
        )['partial_total']


        results[date_cursor.month] = month_partial_total

        # Uncomment following line if you want result contains the month names
        # instead the month's integer values.
        # result[calendar.month_name[month_number]] = month_partial_total

        date_cursor += dateutil.relativedelta.relativedelta(months=1)

    return results

Since Django 1.11 might be able to solve this problem SubQueries, but I've never used it for subquery on the same model.

like image 52
Raydel Miranda Avatar answered Apr 27 '23 13:04

Raydel Miranda