4

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

Niladry Kar
  • 1,163
  • 4
  • 20
  • 50
  • 2
    You need a [window function](https://docs.djangoproject.com/en/2.1/ref/models/expressions/#window-functions) that will calculate a running total grouped by month. I'll try and work out the details for you if I get some time later. – Daniel Roseman Jan 11 '19 at 08:17
  • Actually I am very much new in Django...It will very helpful if you can work out the details for me as I do not know the implementation of this window function in app...I am just going through the basics... – Niladry Kar Jan 11 '19 at 09:10
  • Can't you loop through the months (while fixing the initial value) aggregating the values? Something along the lines of `Purchase.objects.filter(date__range=(START_DATE,FIRST_MONTH_END_DATE).aggregate(total=Sum('total_purchase'))`, then move your `FIRST_MONTH_END_DATE` 1 month ahead and so on until you reach the end of your specified time frame? Not sure how efficient it is though. – henriquesalvaro Jan 11 '19 at 11:35
  • This will not be convenient way of doing the above solution as because the date range will be selected by the user which will have a start date and a end date...I cannot extract the last day of the month – Niladry Kar Jan 11 '19 at 11:55

2 Answers2

7

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.

Raydel Miranda
  • 13,825
  • 3
  • 38
  • 60
  • I am using django version 2.0.6 and here its giving me this error "AttributeError: 'str' object has no attribute 'month'" – Niladry Kar Jan 15 '19 at 06:33
  • @NiladryKar the function I wrote expect `datetime.date` objects as parameters. Are you providing strings? – Raydel Miranda Jan 15 '19 at 06:35
  • Oh yeah that was my mistake sorry for that...But after rectifying my errors Now I am facing this error "TypeError: 'QuerySet' object does not support item assignment" in this line of code "result[month_number] = month_partial_total" – Niladry Kar Jan 15 '19 at 06:53
  • @NiladryKar That's my bad, I put the same name to the dictionary holding results and the first query, I've fixed it. – Raydel Miranda Jan 15 '19 at 06:55
  • Tried a lot but the values in the dict is showing "OrderedDict()"...But there are entries between the date range I have given – Niladry Kar Jan 15 '19 at 07:25
  • @NiladryKar Is your range inside the same month? – Raydel Miranda Jan 15 '19 at 07:29
  • Nope, I have declared like this 'start_date = datetime.date(2018, 4, 1)' and 'end_date = datetime.date(2019, 3, 31)' – Niladry Kar Jan 15 '19 at 07:30
  • @NiladryKar Hahahaha, ok I see the problem, the for loop will not work since start_date.month < end_date.month – Raydel Miranda Jan 15 '19 at 07:32
  • 1
    @NiladryKar I fixed that too. I hope this help. On the other hand, I also hope you understand this is an effort in order to address you in the right direction, the answer does not need to resolve your exact task in order to be correct. – Raydel Miranda Jan 15 '19 at 07:45
  • Yeah I totally understand that...That do does the trick... But I problem with the calculation arises The months with no purchases at all the aggregate value is coming None and it also not adding the monthly value...Just see my example I think you can understand what I mean to say... – Niladry Kar Jan 15 '19 at 07:58
  • The solution you provided is aggregating the values of that month only...Its not adding the aggregated value with the next month...I have done the same using TruncMonth function in Django... – Niladry Kar Jan 15 '19 at 09:44
  • Okay, the last update, just accumulate in partial_total_month – Raydel Miranda Jan 15 '19 at 13:38
0

Solution

According to the answer given by Mr.Raydel Miranda, Finally I got the solution to my problem...

I have done the following in my views and it worked very nicely:

import datetime
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('Total_Purchase')))
date_cursor = start_date
z = 0
while date_cursor < end_date:
    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
    if month_partial_total == None:
        month_partial_total = int(0)
        e = month_partial_total
    else:
        e = month_partial_total

    z = z + e

    results[date_cursor.month] = z

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

 return results

Thank you everyone.

Niladry Kar
  • 1,163
  • 4
  • 20
  • 50