How to Group and Sum Category with ORM-Query - django

Please note: Similar questions didn't help me as they have the category-foreignkey in the same class.
I have a simple Invoice app with models Invoice, Position, Product and Category. The Product is bound to the Category.
My target is to create a queryset that
filters e. g. a specific date-range
and then group all categories and build their sums
Here is a screenshot of the invoice respectively of its positions:
The expected result of the grouped query should look like this:
Can you help me to create a query that groups and sums the categories within the filtered date-range?
The only solution I was able to create was the filter of a specific date-range:
queryset = Position.objects.filter(invoice__date_of_purchase__range=['2019-01-01', '2019-12-31'])
models.py (which I have simplified):
from django.db import models
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=30, unique=True)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=120)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
def __str__(self):
return self.name
class Invoice(models.Model):
invoice_code = models.CharField(max_length=15)
date_of_purchase = models.DateField()
customer_name = models.CharField(max_length=100)
def __str__(self):
return self.invoice_code
class Position(models.Model):
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.DecimalField(decimal_places=2, max_digits=6)
price = models.DecimalField(decimal_places=2, max_digits=8)
total = models.DecimalField(
decimal_places=2, max_digits=8, blank=True, null=True) # is calculated in view
def __str__(self):
return self.product.name

The following filter will return all categories that have an invoice in the date range and will also filter the annotation to sum only those positions for those invoices
categories = Category.objects.filter(
products__position__invoice__date_of_purchase__range=['2019-11-17', '2019-12-31']
).annotate(
sum=Sum('products__position__total')
)
Each category will now be annotated with an attribute "sum"
for category in categories:
print(category, category.sum)

I'm going to suggest a tweak based on my experience.
Put position into the invoice model as a many to many fields. This should make it cheaper to filter the date range of invoices. It also may help to add a "sent" bol field depending on your use case.
Either in your view or in a utils.py file. Loop thru the query set's "Position's" field with the category as the conditional to separate by category and += the Position.total field to your awaiting variable.

Related

How can I get a total price column for CartItem model?

class Product(models.Model):
name = models.CharField(max_length=80)
product_image = models.ImageField(upload_to='product/product/images/%Y/%m/%d/', blank=True)
price = models.IntegerField()
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class CartItem(models.Model):
item = models.ForeignKey(Product, null=True, on_delete=models.CASCADE)
qty = models.IntegerField(default=1)
cart = models.ForeignKey(Cart, null=True, on_delete=models.CASCADE)
I'm trying to get an automatic total price that will be shown on check out page. I want to add a 'total_price' column on CartItem model and set the default 'item.price * qty', but when I tried to add this line to the class:
total_price = models.IntegerField(default=item.price)
since default value for qty is 1 but I got AttributeError: 'ForeignKey' object has no attribute 'price' error.
I also tried add this to the class:
#property
def total_price(self):
item = self.object.get(product=self.item)
return self.item.price
but I'm not sure which model will have the property? And when I added this method, I lost total_price column which I set its default as 0. I apologize for the lacking quality of solutions!
You are in right direction. You can try annotation or aggregation to get the total price. Here is one approach:
For all Cart Items using annotation with Sum:
Cart.objects.all().annotate(total_spent=Sum(
F('cartitem__item__price') *
F('cartitem__qty'),
output_field=models.FloatField()
))
For one Cart, you can try like this with aggregation:
class Cart(...):
....
#property
def total_price(self):
return self.cartitem_set.aggregate(price=Sum(
F('item__price') *
F('qty'),
output_field=models.FloatField()
)['price']
Change the total_price property to:
class CartItem(models.Model):
cart = models.ForeignKey(Cart, null=True, on_delete=models.CASCADE,
related_name="orders")
#property
def total_price(self):
return self.qty * self.item.price
And you can easily get the total price of the Order Item.
If you want to get Total amount of all CartItems prices can do like below:
class Cart(models.Model):
#property
def total_amount(self):
self.orders.annotate(total_spent=Sum(
F('item__price') *
F('qty'),
output_field=models.FloatField()
))

django related question querying list of won items by getting list of items then max bid from bid table for each item then check if user won it

I have a question here, I have two tables in my django models one for listings and one for bids
class Listing(models.Model):
class Meta:
verbose_name_plural = 'Listing'
title = models.CharField(max_length=64)
description = models.TextField()
price = models.DecimalField(max_digits=5, decimal_places=2)
image = models.URLField(max_length=500, default='')
category = models.CharField(max_length=32)
created = models.CharField(max_length=32)
addedon = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.title
class Bid(models.Model):
class Meta:
verbose_name_plural = 'Bid'
user = models.ForeignKey(User, on_delete=models.CASCADE)
item = models.ForeignKey(Listing, on_delete=models.CASCADE)
bid = models.DecimalField(max_digits=5, decimal_places=2)
created = models.DateTimeField(auto_now_add=True)
what i want is to show a page with all the user won items
def won(request):
listing = Listing.objects.filter(active=False)
the question here how i can make list of all then check max bid of each listing, then check is the current user is the winner and display it in the template won.html
like :
getMaxBid = Bid.objects.filter(item_id=listing.id).aggregate(Max('bid'))
maxBid = getMaxBid['bid__max']
then if the user is the winner display it
return render(request, "auctions/won.html", {
'listing': listing, 'active': False
})
thanks in advance
You can filter the Listing by first creating a Subquery expression [Django-doc] that will obtain the winner, and then check if the logged in user (or another user) is the winner, so:
from django.db.models import OuterRef, Subquery
listing = Listing.objects.alias(
bid_winner=Subquery(
Bid.objects.filter(item=OuterRef('pk')).order_by('-bid').values('user')
)[:1]
).filter(
active=False,
bid_winner=request.user
)
or Listings where the user has an active bid:
Listing.objects.filter(
bid__user=request.user,
active=True
).distinct()

Django Order QuerySet based on ManyToManyField value

I have a business model as follows:
class Business(models.Model):
class Meta:
verbose_name_plural = "Businesses"
name = models.CharField(max_length=60, null=False, verbose_name="Title")
about = models.TextField(max_length=5000, null=True, verbose_name="Description", blank=True)
upvote = models.ManyToManyField(Account, verbose_name="Upvote Count", blank=True)
The Account model is as follows:
class Account(models.Model):
CHOICES = [('Male', 'Male'),Female', 'Female'),]
class Meta:
verbose_name_plural = "Accounts"
name = models.CharField(max_length=60, null=False, verbose_name="Title")
gender= models.CharField(max_length=6, null=True, verbose_name="Gender", choices=CHOICES)
I am trying to get a QuerySet that will be sorted by gender of the Account.
How do I achieve this?
So far I have achieved sorting by the upvote count.
Business.objects.all().order_by("upvote")
You can use the Sum function [Django docs] with a Conditional Expression [Django docs] to annotate a value according to which you would order:
from django.db.models import Case, Sum, Value, When
Business.objects.annotate(
order_value=Sum(
Case(
When(upvote__gender='Male', then=Value(1)),
When(upvote__gender='Female', then=Value(-1)),
default=Value(0)
)
)
).order_by('order_value')
The above query would give you Business objects with more upvotes by females first and males later, you can reverse the order by writing .order_by('-order_value') instead.
You can access fiels of related models by double underscore. See documentation here.
Try:
Business.objects.all().order_by("upvote__gender")

Django Sum in Annotate

Good afternoon,
I am really struggling with getting a sum using Annotate in DJango.
I am using User object and the following models:
class Depts(models.Model):
dept_name = models.CharField(max_length=55)
dept_description = models.CharField(max_length=255)
isBranch = models.BooleanField(default=False)
def __str__(self):
return "{}".format(self.dept_name)
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile')
title = models.CharField(max_length=75)
dept = models.ForeignKey(Depts, on_delete=models.CASCADE, related_name="dept", null=True)
class ActivityLog(models.Model):
activity_datetime = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='activity_user')
activity_category = models.ForeignKey(ActivityCategory, on_delete=models.CASCADE, null=True, related_name='activity_cat')
activity_description = models.CharField(max_length=100, default="Misc Activity")
class ActivityCategory(models.Model):
activity_name = models.CharField(max_length=40)
activity_description = models.CharField(max_length=150)
pts = models.IntegerField()
def __str__(self):
return '%s' % (self.activity_name)
What I need to do is get a group of departments with aggregating the sum of the pts earned by all the users activitylogs.
So a user is part of department, they do activities, each activity is of a type activity_category and has associated points. How can I query using the ORM to get a sum of points for everyone in each department?
Thank you, I cannot seem to wrap my mind around it.
You annotate the departments with the sum:
from django.db.models import Sum
Depts.objects.annotate(
total_pts=Sum('dept__user__activity_user__activity_category__pts')
)
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the Depts model to the UserProfile
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the dept relation to userprofiles.
After setting the related_name='userprofiles', the query is:
from django.db.models import Sum
Depts.objects.annotate(
total_pts=Sum('userprofiles__user__activity_user__activity_category__pts')
)

How do I create this queryset?

I have the following three models:
class Category(models.Model):
name = models.CharField(max_length=120)
class Experiment(models.Model):
name = models.CharField(max_length=50, unique=True)
categories = models.ManyToManyField(Category)
class Ad(models.Model):
experiment = models.ForeignKey(Experiment, related_name='ads', on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.PROTECT, blank=True, null=True)
I want to create a queryset which returns all ads where ad.category is in ad.experiment.categories.
Some example data to talk through:
Category: ['Cat1', 'Cat2', 'Cat3', Cat4', 'Cat5']
Experiment: [['Exp1',['Cat2','Cat3]],['Exp2',['Cat5','Cat1']]]
Ad: [['Exp1','Cat4'],['Exp1','Cat2']]
The queryset I'm hoping to create would only return the second ad because the ad's category is in the ad's experiment's category.
Any help would be appreciated!
You need to traverse the reverse relationship for experiment and then use an F object to access the ad's category field in the query. The distinct is used because the experiment will have many categories so one ad may have multiple matches on the comparison.
from django.db.models import F
Ad.objects.filter(experiment__category=F('category')).distinct()