Let's say that I have a simple shopping cart system with these models:
from django.db import models
from django.utils import timezone
class Product(models.Model):
name = models.CharField(max_length=255)
# More fields...
#property
def featured_image(self):
try:
return self.productimage_set.all()[0]
except IndexError:
return None
def __str__(self):
return self.name
class ProductImage(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.FileField(upload_to='products/')
# More fields...
def __str__(self):
return self.product.name
class Order(models.Model):
created = models.DateTimeField(default=timezone.now)
# More fields...
def __str__(self):
return str(self.pk)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
# More fields...
def __str__(self):
return 'Order %s: %s of %s' % (
self.order_id,
self.quantity,
self.product.name,
)
When a user places an order, an OrderItem object is created for each product they ordered, which lets me know (1) which product they purchased and (2) how many of that product they purchased.
Now, suppose I wanted to show the user which products they have ordered the most of. I'll use this data as an example:
Order 1:
Product A: 10
Order 2:
Product A: 10
Product B: 5
Order 3:
Product A: 10
Product B: 5
Product C: 5
This particular user should be shown the products in this order:
Product A: 30 total
Product B: 10 total
Product C: 5 total
This is the code that achieves this:
order_items_by_quantity = OrderItem.objects.values(
'product__name',
).annotate(
total_purchased=Sum('quantity'),
).order_by(
'-total_purchased',
)
for order_item in order_items_by_quantity:
print(order_item)
But with this approach, I can't use order_item.product.featured_image. Is there a way I can have both things? Namely, the quantities all summed up and ordered from most to least and the ability to use order_item.product.featured_image.
order_items_by_quantity = OrderItem.objects.values(
'product__name', 'product_id'
).annotate(
total_purchased=Sum('quantity'),
).order_by(
'-total_purchased',
)
for order_item in order_items_by_quantity:
pid = order_item['product_id']
image = ProductImage.objects.filter(product_id=pid).first()
print(order_item, image)
I don't think there's a way to get object property after you use values() as the queryset returns dicts with SELECT key-values and not model objects. You can do additional query to get products with featured images and map it to your order_items:
order_items_by_quantity = OrderItem.objects.values(
'product__name', 'product_pk'
).annotate(total_purchased=Sum('quantity')).order_by('-total_purchased')
product_image_map = {}
for product in Product.objects.prefetch_related('productimage_set'):
product_image_map[product.pk] = product.featured_image
for order_item in order_items_by_quantity:
order_item['featured_image'] = product_image_map[order_item['product_id']]
That way you ask database once for your products, not every time you iterate over order_items_by_quantity. Additionally you should do prefetch_related() on your images.
Related
My model is like this:
class Cart(models.Model):
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(null=False,blank=False)
total = models.FloatField(null=False,blank=False)
#property
def total(self):
return self.quantity * self.product.price
The total appears in my admin dashboard but when I inspect the db from the shell I don't get the total value, I get the other values:
<QuerySet [{'id': 42, 'product_id': 11, 'quantity': 2}]>
Expected output:
<QuerySet [{'id': 42, 'product_id': 11, 'quantity': 2, 'total': 29.00}]>
The #property decorator is a Python method, your database knows nothing about it. The property will still work in Python code and Django templates, but you won't be able to use DB operations against it like other fields (e.g. using .filter or .values with total).
If you need to query against the total, what you probably want is to use annotations instead.
Just omit the total from your model altogether and you can annotate your queries to have the DB calculate the total on-the-fly:
from django.db.models import ExpressionWrapper, F
queryset = Cart.objects.annotate(
total=ExpressionWrapper(
F("quantity") * F("product__price"), output_field=models.FloatField()
)
)
for cart in queryset:
print(cart.total)
ExpressionWrapper is used to define the output_field (so Django knows what type the result should be) which is needed when multiplying integerfields by floatfields.
If you want queries to always return the annotated total, you can override the model's default manager.
class CartManager(models.Manager):
def with_total(self):
qs = super().get_queryset()
return qs.annotate(
total=ExpressionWrapper(
F("quantity") * F("product__price"), output_field=models.FloatField()
)
)
def get_queryset(self): # override the default queryset
return self.with_total()
class Cart(models.Model):
objects = CartManager() # override the default manager
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(null=False, blank=False)
Django models save data once the save() method execuate.
Try this, it works very well. It's a best practice
class Cart(models.Model):
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(null=False,blank=False)
total = models.FloatField(null=False,blank=False)
def save(self, *args, **kwargs):
self.total = self.quantity * self.product.price
super(Cart, self).save(*args, **kwargs)
#property decorator is not for saving data in DB.
What the #property decorator does is declare that it can be accessed as a regular property.
This means you can call total as if it were a member your model instead of a function, so like this:
total = cart.total #in views
{{ cart.total }} #in templates
instead of
total = cart.total() #in views
{{ cart.total() }} #in templates
for more details on property, you can refer here
If you still want to store the value in total field, then save it to database instead of returning it in the method.
class Cart(models.Model):
id = models.AutoField(primary_key=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(null=False,blank=False)
total = models.FloatField(null=False,blank=False)
#property
def save_total(self):
total = self.quantity * self.product.price
self.total = total
return total
or
you can save the data to total in your views, while your cart is saved in view.py
Well i'm trying to create two models whereas the product model is:
class Product(models.Model):
id = models.AutoField(primary_key=True)
price = models.FloatField(null=False)
inStock = models.BooleanField(null=False)
and another model for cart where:
class Cart(models.Model):
productID = models.ForeignKey(Product,on_delete=models.CASCADE)
quantity = models.IntegerField()
total
I want total here to equal Product.price multiplied by the Cart.quantity
Is there a way how can I achieve it on Django?
You can use #property decorator
class Cart(models.Model):
productID = models.ForeignKey(Product,on_delete=models.CASCADE)
quantity = models.IntegerField()
#property
def total(self):
return self.quantity * self.productID.price
To use in serializer you can try this:
total = serializers.SerializerMethodField()
def get_total(self, obj):
return obj.total
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.
I wanna summarize the current stock quantity of each item in django admin.
In item page, each item have a column, and I wanna show the number in each column of item.
This is my model:
from django.contrib.auth.models import User
from django.db import models
class Item(models.Model):
item_name = models.CharField(max_length=128)
class In(models.Model):
in_date = models.DateTimeField()
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='ins')
quantities = models.IntegerField()
class Out(models.Model):
out_date = models.DateTimeField()
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='outs')
quantities = models.IntegerField()
Model In means stock-in, Out means stock-out
I write functions in my admin.py like below:
class ItemAdmin(admin.ModelAdmin):
list_display = ['item_name', 'pattern', 'vendor', 'item_ins', 'item_outs']
def item_ins(self, obj):
return obj.ins.aggregate(Sum('quantities')).get('quantities__sum')
item_ins.short_description = 'stock-in'
def item_outs(self, obj):
return obj.outs.aggregate(Sum('quantities')).get('quantities__sum')
item_outs.short_description = 'stock-out'
I already knew how to aggregate total stock-in/stock-out number of each item, but I don't know how to get current stock quantity(stock-in subtract sotck-out) of each item.
Please help me! Thank you!
The simplest but not so efficient implementation would be
def item_quantity(self, obj):
return self.item_ins(obj)-self.item_outs(obj)
def item_quantity(self, obj):
if not self.item_ins(obj):
return '-'
if self.item_outs(obj):
return self.item_ins(obj) - self.item_outs(obj)
return '-'
Thanks to Mr Dharanidhar Reddy , this is my final code .
Sometimes stock-in or stock-out may be empty , so I added some if statement.
i have tried a lot to get an answer for my problem , someone tell me if its possible please.
i'm working on a restaurant order management system , and apart of the system is a point of sale at the same time(for cashier) , when a customer visit to the restaurant , the cashier be able to fill a receipt,for example (3 pizza with 2 sandwich)
models.py
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.PositiveIntegerField(default=1)
def __str__(self):
return self.name
class Order(models.Model):
id = models.AutoField(primary_key = True)
products = models.ManyToManyField(Product ,through='ProductOrder')
#property
def total(self):
return self.productorder_set.aggregate(
price_sum=Sum(F('quantity') * F('product__price'),
output_field=IntegerField()) )['price_sum']
class ProductOrder(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE ,
null=True,blank=True)
ordering = models.ForeignKey(Order, on_delete=models.CASCADE ,
blank=True)
quantity = models.IntegerField(default=1)
#to make a new instance from order,but doesnt make! before saving
#ProductOrder
def create_order(sender, instance, **kwargs):
instance.ordering.save()
pre_save.connect(create_order,sender=ProductOrder)
forms.py
class ProductOrdering(forms.ModelForm):
class Meta:
model = ProductOrder
fields = ['product','ordering','quantity']
views.py
class ProductOrderCreate(CreateView):
form_class = ProductOrdering
model = ProductOrder
template_name = 'create_product_order.html'
success_url = '/orders/'
def form_valid(self,form):
form.instance.ordering = Order.objects.order_by('-pk')[0]
return super(ProductOrderCreate,self).form_valid(form)
if the signal(pre_save) worked fine and after select a product,creating another field automatically(not formset) it will solve the problem
i asked alot everywhere but still not solved , please someone help me out i much appreciate
i think this is not specific to django problem, this is logical problem, so for me i will create an API endpoint to create "order", so from frontend send payload with identifier where will be store on db, what i mean identifier here is every "order" has 1 unique stuff so even customer order 3 items that item will be identify by their identifier.