Multiplying the values of two different models in Django - django

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

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()
))

optimise django sql query

I'm using Django 2.x.
I have two models
class AmountGiven(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
interest_rate = models.FloatField(blank=True, default=None, null=True)
given_date = models.DateField(default=timezone.now)
total_due = models.FloatField(blank=True, default=0.0, editable=False)
class AmountReturned(models.Model):
amount_given = models.ForeignKey(AmountGiven, on_delete=models.CASCADE, blank=True)
amount = models.FloatField()
return_date = models.DateField(default=date.today)
Use case
There can be multiple records of the amount given to a contact
There can be multiple records of the returned amount for an amount given
Now, I want to get total_due amount for a particular contact. This includes
total_payable = total_given + interest
total_due = total_payable - total_returned
To calculate total_due and interest, I have defined few property methods in the AmountGiven model.
#property
def interest_to_pay(self):
if self.interest_rate:
simple_interest_amount = ...
return simple_interest_amount
return 0
#property
def total_payable(self):
return self.amount + self.interest_to_pay
#property
def amount_due(self):
total_due = self.total_payable - self.total_returned
self.total_due = total_due
self.save()
return total_due
#property
def total_returned(self):
returned_amount = self.amountreturned_set.aggregate(total_returned=Sum('amount'))['total_returned']
if not returned_amount:
returned_amount = 0
return returned_amount
In Contact model, there is a property method to get the total due amount for the contact.
#property
def amount_due(self):
total_due = 0
for due in self.amountgiven_set.all():
total_due += due.amount_due
return total_due
Query
ContactSerializer
class ContactMinSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = (
'id', 'first_name', 'amount_due', 'created', 'modified'
)
Since amount_due property is being used in the ContactSerializer, amount_due property is called everytime a contact is call and thus results in nested DB queries.
How can I optimise the above scenario in the application to reduce the DB queries while getting contact or list of contacts? Specially two properties amount_due and total_returned.
amount_due() updates the total_due field in the table, every time it is called.
Edit 2
class ContactViewSet(LoggingMixin, viewsets.ModelViewSet):
serializer_class = ContactMinSerializer
def get_queryset(self):
return Contact.objects.filter(user=self.request.user).annotate(
total_due=Sum(
F('amountgiven_set__total_payable')
- F('amountgiven_set__total_returned')
)
).order_by('first_name')
You're looking for annotations.
Your viewset should define a queryset as follows:
from django.db.models import F, Sum
Contact.objects.annotate(
total_due=Sum(
F('amountgiven_set__total_payable')
- F('amountgiven_set__total_returned')
)
)
Then define a MethodSerializer field on your serializer to account for it.
class ContactMinSerializer(serializers.ModelSerializer):
total_due = serializers.SerializerMethodField()
def get_total_due(self, obj):
return return self.total_due
class Meta:
model = Contact
fields = (
'id', 'first_name', 'created', 'modified',
'total_due',
)

Django REST Framework : serializer fields are missing in the response

I'm using Django 2.0 and Django REST Framework.
I have two models contact and transaction as below
contact model
class Contact(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100, blank=True, null=True)
amount given model
class AmountGiven(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
interest_rate = models.FloatField(blank=True, default=None, null=True, help_text='% of interest to be calculated')
_given_date = models.DateTimeField(
db_column='given_date',
default=timezone.now,
help_text='Date and time when amount was given to the contact'
)
def __str__(self):
return str(self.amount)
#property
def given_date(self):
return self._given_date
#given_date.setter
def given_date(self, value):
self._given_date = value
#property
def interest_to_pay(self):
if self.interest_rate:
datetime_diff = datetime.now(get_localzone()) - self.given_date
days = datetime_diff.days
duration_in_year = days/365
simple_interest_amount = (self.amount * duration_in_year * self.interest_rate)/100
return simple_interest_amount
return 0
#property
def total_payable(self):
return self.amount + self.interest_to_pay
#property
def amount_due(self):
returned_amount = 0
for returned in self.amountreturned_set.all():
returned_amount += returned.amount
return self.total_payable - returned_amount
and ContactSerializer
class ContactSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedRelatedField(
view_name='contacts:detail',
read_only=True
)
user = serializers.CurrentUserDefault()
amount_due = ReadOnlyField(source='amountgiven__amount_due')
class Meta:
model = Contact
fields = ('url', 'id', 'first_name', 'last_name', 'full_name', 'amount_due')
and in views.py
class ContactViewSet(viewsets.ModelViewSet):
serializer_class = ContactSerializer
permission_classes = (IsAuthenticated, AdminAuthenticationPermission,)
def get_queryset(self):
return Contact.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
But there is no field as amount_due and url in the response returned while making the request to /contacts/ endpoint with GET method.
Based on your comment, you want the sum of all the amounts(please edit your question). so you should use annotate in your queryset:
from django.db.models import Sum
def get_queryset(self):
return Contact.objects.filter(user=self.request.user).annotate(amount_due=Sum('amountgiven_set__amount'))
(I recommend using modelManager for the queryset and the filtering instead of doing it here)
and add a field like this to your serializer:
amount_due = serializer.IntegerFiled()
Your modeling doesn't allow you to access amount_due in the way which you'd like.
Your Contact model does not have amountgiven attribute.
It does however have amountgiven_set which you can use to obtain a queryset of amountgiven for given contact.
But there can be multiple ones so you need to decide which amount_due you want to display in your serializer.
You can use SerializerMethodField to serializer the value of amount_due which you would like:
class ContactSerializer(serializers.HyperlinkedModelSerializer):
amount_due = serializers.SerializerMethodField()
def get_amount_due(self, obj):
amountgiven = obj.amountgiven_set.first()
return amountgiven.amount_due
But again, as i already mentioned - amountgiven_set returns a queryset where you can have multiple objects.
In case you are sure you have only one for given contact, you can use first() as in my example to get it.

Annotating sum while still having access to foreign objects

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.

How to bind custom function to model field?

Say this is my simple models.py
class Order(models.Model):
quantity = models.IntegerField()
item_price = models.FloatField()
I'd like to have a function to calculate the total price:
def calc_total(self):
return self.quantity * self.item_price
Now, how do I create model field total_price so it gets populated automatically in database? I know I can use calc_total method in template but I need this in db also.
Override the save method in your model to set the value each time the model is saved:
class Order(models.Model):
quantity = models.IntegerField()
item_price = models.FloatField()
total_price = models.FloatField()
def calc_total(self):
return self.quantity * self.item_price
def save(self, *args, **kwargs):
self.total_price = self.calc_total()
super(Order, self).save(*args, **kwargs)