Django: Group by date (day, month, year) - django

I've got a simple Model like this:
class Order(models.Model):
created = model.DateTimeField(auto_now_add=True)
total = models.IntegerField() # monetary value
And I want to output a month-by-month breakdown of:
How many sales there were in a month (COUNT)
The combined value (SUM)
I'm not sure what the best way to attack this is. I've seen some fairly scary-looking extra-select queries but my simple mind is telling me I might be better off just iterating numbers, starting from an arbitrary start year/month and counting up until I reach the current month, throwing out simple queries filtering for that month. More database work - less developer stress!
What makes most sense to you? Is there a nice way I can pull back a quick table of data? Or is my dirty method probably the best idea?
I'm using Django 1.3. Not sure if they've added a nicer way to GROUP_BY recently.

Django 1.10 and above
Django documentation lists extra as deprecated soon. (Thanks for pointing that out #seddonym, #Lucas03). I opened a ticket and this is the solution that jarshwah provided.
from django.db.models.functions import TruncMonth
from django.db.models import Count
Sales.objects
.annotate(month=TruncMonth('created')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count
Older versions
from django.db import connection
from django.db.models import Sum, Count
truncate_date = connection.ops.date_trunc_sql('month', 'created')
qs = Order.objects.extra({'month':truncate_date})
report = qs.values('month').annotate(Sum('total'), Count('pk')).order_by('month')
Edits
Added count
Added information for django >= 1.10

Just a small addition to #tback answer:
It didn't work for me with Django 1.10.6 and postgres. I added order_by() at the end to fix it.
from django.db.models.functions import TruncMonth
Sales.objects
.annotate(month=TruncMonth('timestamp')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.order_by()

Another approach is to use ExtractMonth. I ran into trouble using TruncMonth due to only one datetime year value being returned. For example, only the months in 2009 were being returned. ExtractMonth fixed this problem perfectly and can be used like below:
from django.db.models.functions import ExtractMonth
Sales.objects
.annotate(month=ExtractMonth('timestamp'))
.values('month')
.annotate(count=Count('id'))
.values('month', 'count')

metrics = {
'sales_sum': Sum('total'),
}
queryset = Order.objects.values('created__month')
.annotate(**metrics)
.order_by('created__month')
The queryset is a list of Order, one line per month, combining the sum of sales: sales_sum
#Django 2.1.7

Here's my dirty method. It is dirty.
import datetime, decimal
from django.db.models import Count, Sum
from account.models import Order
d = []
# arbitrary starting dates
year = 2011
month = 12
cyear = datetime.date.today().year
cmonth = datetime.date.today().month
while year <= cyear:
while (year < cyear and month <= 12) or (year == cyear and month <= cmonth):
sales = Order.objects.filter(created__year=year, created__month=month).aggregate(Count('total'), Sum('total'))
d.append({
'year': year,
'month': month,
'sales': sales['total__count'] or 0,
'value': decimal.Decimal(sales['total__sum'] or 0),
})
month += 1
month = 1
year += 1
There may well be a better way of looping years/months but that's not really what I care about :)

Here is how you can group data by arbitrary periods of time:
from django.db.models import F, Sum
from django.db.models.functions import Extract, Cast
period_length = 60*15 # 15 minutes
# Annotate each order with a "period"
qs = Order.objects.annotate(
timestamp=Cast(Extract('date', 'epoch'), models.IntegerField()),
period=(F('timestamp') / period_length) * period_length,
)
# Group orders by period & calculate sum of totals for each period
qs.values('period').annotate(total=Sum(field))

By month:
Order.objects.filter().extra({'month':"Extract(month from created)"}).values_list('month').annotate(Count('id'))
By Year:
Order.objects.filter().extra({'year':"Extract(year from created)"}).values_list('year').annotate(Count('id'))
By day:
Order.objects.filter().extra({'day':"Extract(day from created)"}).values_list('day').annotate(Count('id'))
Don't forget to import Count
from django.db.models import Count
For django < 1.10

i have orders table in my database . i am going to count orders per month in the last 3 months
from itertools import groupby
from dateutil.relativedelta import relativedelta
date_range = datetime.now()-relativedelta(months=3)
aggs =Orders.objects.filter(created_at=date_range)\
.extra({'date_created':"date(created_at)"}).values('date_created')
for key , group in groupby(aggs):
print(key,len(list(group)))
created_at is datetime field. by extra function what done is taking date from datetime values. when using datetime we may not get the count correct because objects are created at different time in a day.
The for loop will print date and number of count

Related

Django: How to get the list of month within a daterange in django query

Suppose I have query:
ExampleModel.objects.filter(some_datetime_field__gte=start, some_datetime_field__lte=end)
How do I get the list of all months present within "start" and "end" in the above mentioned query.
For example:
IF
start= 1/10/2018 and end=10/1/2019
Then the output will be:
OCTOBER
NOVEMBER
DECEMBER
JANUARY
Anyone any idea how to perform this?
Thank you in advance
You can extract months and then get their names
from django.db.models.functions import ExtractMonth
months = (
ExampleModel.objects
.filter(some_datetime_field__gte=start, some_datetime_field__lte=end)
.annotate(month=ExtractMonth('some_datetime_field'))
.values_list('month', flat=True)
.distinct()
)
At the end of this code you'll have a list of months(numbers). for example
[1, 3, 6, 8]
And you can get their names using calendar
import calendar
[calendar.month_name[month] for month in months]
You can use annotation and Query Expressions.
import calendar
from django.db.models import Case, When, Value, CharField
conditions = []
for i in range(1, 13):
month_name = calendar.month_name[i]
conditions.append(When(some_datetime_field__month=i, then=Value(month_name)))
# conditions will be like below
# [
# When(some_datetime_field__month=1, then=Value('January')),
# When(some_datetime_field__month=2, then=Value('February')),
# ...
# ]
ExampleModel.objects.annotate(
month_name=Case(*conditions, default=Value(""), output_field=CharField())
).order_by("month_name").values_list("month_name", flat=True).distinct()
# Result will be like
# <ExampleModelQuerySet ['January', 'September']>

Django ORM query - Sum inside annotate using when condition

I have a table, lets call it as DummyTable.
It has fields - price_effective, store_invoice_updated_date, bag_status, gstin_code.
Now I want to get the output which does a group by of - month, year from the field store_invoice_updated_date and gstin_code.
Along with that group by I wanna do thse calculations -
Sum of price_effective as 'forward_price_effective' if the bag_status is other than 'return_accepted' or 'rto_bag_accepted'. Dont know how to do an exclude here i.e. using a filter in annotate
Sum of price effective as 'return_price_effective' if the bag_status is 'return_accepted' or 'rto_bag_accepted'.
A field 'total_price' that subtracts the 'return_price_effective' from 'forward_price_effective'.
I have formulated this query, which doesn't work
from django.db.models.functions import TruncMonth
from django.db.models import Count, Sum, When, Case, IntegerField
DummyTable.objects.annotate(month=TruncMonth('store_invoice_updated_date'), year=TruncYear('store_invoice_updated_date')).annotate(forward_price_effective=Sum(Case(When(bag_status__in=['delivery_done']), then=Sum(forward_price_effective)), output_field=IntegerField()), return_price_effective=Sum(Case(When(bag_status__in=['return_accepted', 'rto_bag_accepted']), then=Sum('return_price_effective')), output_field=IntegerField())).values('month','year','forward_price_effective', 'return_price_effective', 'gstin_code')
Solved it by multiple querysets.
Just couldnt find out a way to appropriately use 'Case' with 'When' with 'filter' and 'exclude'.
basic_query = BagDetails.objects.filter(store_invoice_updated_date__year__in=[2018]).annotate(month=TruncMonth('store_invoice_updated_date'), year=TruncYear('store_invoice_updated_date') ).values('year', 'month', 'gstin_code', 'price_effective', 'company_id', 'bag_status')
forward_bags = basic_query.exclude(bag_status__in=['return_accepted', 'rto_bag_accepted']).annotate(
Sum('price_effective')).values('year', 'month', 'gstin_code', 'price_effective', 'company_id')
return_bags = basic_query.filter(bag_status__in=['return_accepted', 'rto_bag_accepted']).annotate(
Sum('price_effective')).values('month', 'gstin_code', 'price_effective', 'company_id')

Filter two dates in one query django/python

Is there a way I can filter two datefield columns from a table on the same query?
Example:
I have date_ots and date_lta I need to filter and get the result from today + 4 days on both columns.
Thanks for your attention,
Alex
If you would like exact timing (hours, min, sec), you can do:
from datetime import timedelta
four_days_from_now = timezone.now() + timedelta(days=4)
query = Model.objects.filter(date_ots=four_days_from_now, date_lta=four_days_from_now)
If you only want the date 4 days from now (at any time), you can do:
from datetime import timedelta
four_days_from_now = timezone.now().date() + timedelta(days=4)
query = Model.objects.filter(date_ots=four_days_from_now, date_lta=four_days_from_now)

Get the total values in each month using Django QuerySet

I have this model for employee overtime hours
class Overtime(IncomeBase):
day = models.DateField(verbose_name="Date")
value = models.FloatField(default=1)
I need to extract the total value for each month. Now I am using a daily QuerySet in the manager.
class OvertimeManager(models.Manager):
def daily_report(self):
return self.values('day').annotate(hours=models.Sum('value')).order_by('-day')
However now I need a monthly report that will get the Sum of value for each month.
I tried to extract the month first but then I lose the values.
Note: the month should not have the total sum for all years, so specifically I need to group by month,year
If you are using Postgresql you can do this. Ofc there is similar fuctions.
Overtime.objects.extra({'month': "to_char(day, 'Mon')", "year": "extract(year from day)"}).values('month', 'year').annotate(Sum('value'))
More info:
http://www.postgresql.org/docs/7.4/static/functions-formatting.html
http://www.postgresql.org/docs/9.1/static/functions-datetime.html
Or django way:
from django.db import connection
truncate_month = connection.ops.date_trunc_sql('month','day')
Overtime.objects.extra({'month': truncate_month}).values('month').annotate(Sum('value'))
I think this will help you.

Django filter query with date and integer

I try to write query which must show planes, which working life is over.
Models.py
class Aircraft(models.Model):
registration_num = models.IntegerField(primary_key=True)
airplane_age = models.IntegerField()
commision_day = models.DateField()
Views.py
def query1(request):
air_list = Aircraft.objects.filter(commision_day__year__lte=(datetime.date.today().year - 10))
#air_list = Aircraft.objects.extra(where=['commision_day + airplane_age < CURDATE()'])
#air_list = Aircraft.objects.extra(where=["commision_day+airplane_age<datetime.date.today()"]) F('airplane_age') year=10, month=1, day=1
Data example
commision_day = 1995.10.05
airplane_age = 15
I try many methods to do, but no one works((((
So i need to show plnes which commision day + airplane_age < today day
Thanks
You could try setting up a datetime.date object to use as a threshold, and then filter Aircraft objects against that by their date directly:
today = datetime.date.today()
threshold = datetime.date(today.year-10, today.month, today.day)
Aircraft.objects.filter(commision_day__lte=threshold)
Try this queryset:
from datetime import datetime, timedelta
Aircraft.objects.filter(commision_day__lte=(datetime.now() - timedelta(years=10)))
The goal here is to combine the values of two fields, and compare the result with a value calculated in the current process, essentially:
Aircraft.objects.extra(where=['somedate + years < today'])
The expression within where=['...'] must be valid SQL and the right syntax will depend on your database backend. That is, you need to know the right way to add years to a date to form the somedate + years correctly, for example in SQLite it would be like this:
date(commision_day, '+' || airplane_age || ' year') < date('now')
This is more efficient than your solution, but it is not portable, as it depends on the database backend due to non-standard syntax of date operations.
Try this:
from dateutil.relativedelta import relativedelta
from django.db.models import F
Aircraft.objects.filter(commision_day__lte=datetime.now()-relativedelta(years=F('airplane_age')))