How can I update my Django model using signals? - django

How can I keep adding to the total price, when a new instance is created in the CartItem model and also deduct the price when an item is deleted ? , at the moment my current signals is only showing the price of the current product that I update or create.
signals.py
#receiver([post_save, post_delete], sender=CartItem, dispatch_uid="update_total")
def add_cart_receiver(sender, instance, *args, **kwargs):
total = 0
product_price = instance.item.price
quantity = instance.quantity
updated_total = Decimal(product_price) * int(quantity)
total += updated_total
if instance.items_cart.subtotal != total:
instance.items_cart.subtotal = total
instance.items_cart.save()
models.py
class CartItem(models.Model):
item = models.ForeignKey(Product, on_delete=models.CASCADE, blank=True, null=True)
items_cart = models.ForeignKey('Cart', blank=True, on_delete=models.CASCADE, null=True)
quantity = models.IntegerField(null=True, blank=True)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return 'item:{} cart:{} quantity:{}'.format(self.item, self.items_cart, self.quantity)
class Cart(models.Model):
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
subtotal = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
total = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
return 'id:{} user:{} subtotal:{} total:{} updated:{}'.format(self.id, self.user, self.subtotal, self.total, self.updated)
views.py
def cart_add(request):
product_id = request.POST.get('product_id')
try:
product_obj = Product.objects.get(id=product_id)
quantity = request.POST.get('quantity_field')
cart_obj, new_obj = Cart.objects.new_or_get(request)
last_qty = CartItem.objects.filter(item=product_obj, items_cart=cart_obj).reverse()[0].delete()
new_qty = CartItem(item=product_obj, items_cart=cart_obj, quantity=quantity)
new_qty.save()
return redirect("cart:home")
except ValueError:
print("Unable to increase quantity")
return redirect("cart:home")

I figured out the problem I was having, below is my solution, to keep accumulating the total price of all items in my current cart, I have done this by looping over a Queryset of 'cart_items' and calculating the price * quantity, then adding it to a counter ('total'), then saving it to my 'Cart' model field 'total'. The receiver is collecting 'post_save' and 'post_delete' signals. Hopefully this will help someone else.
signals.py
#receiver([post_save,post_delete], sender=CartItem, dispatch_uid="update_total")
def add_cart_receiver(sender, instance, **kwargs):
cart = instance.items_cart.id
cart_items = CartItem.objects.filter(items_cart=cart)
cart_total = 0
for element in cart_items:
cart_total += (Decimal(element.item.price) * int(element.quantity))
instance.items_cart.subtotal = cart_total
instance.items_cart.save()

Related

Django, total price of not iterable object

Hello I have a question I have no idea how can i get total price of not iterable object in Django, I get this error:
TypeError at /cart
'OrderItem' object is not iterable
Here is my code, I would be pleased by any advise.
views.py
order_items = OrderItem.objects.filter(cart=cart)
order_items = OrderItem.objects.annotate(
sum=Sum(F('item__price') * F('quantity'))
).get(cart=cart)
order_items.total_price = order_items.sum
order_items.save(force_update=True)
models.py
class Item(Visits, models.Model):
title = models.CharField(max_length=150)
price = models.IntegerField(default=1000)
image = models.ImageField(upload_to='pictures', default='static/images/man.png')
description = models.TextField(default="Item")
visits = models.IntegerField(default=0)
class OrderItem(models.Model):
cart = models.ForeignKey('Cart', on_delete=CASCADE, null=True)
item = models.ForeignKey(Item, on_delete=CASCADE, null=True)
quantity = models.IntegerField(default=1)
total_price = models.IntegerField(default=1)
you can create function in you model and used The result on your view
like this:
class OrderItem(models.Model):
cart = models.ForeignKey('Cart', on_delete=CASCADE, null=True)
item = models.ForeignKey(Item, on_delete=CASCADE, null=True)
quantity = models.IntegerField(default=1)
# does not needed
# total_price = models.IntegerField(default=1)
#property
def get_total(self):
total = self.item.price * self.quantity
return total

I have an error: 'NoneType' object has no attribute 'price' error by rendering the template

I have these models:
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.CharField(max_length=300, choices=TOTAL)
subcategory = models.CharField(max_length=300, choices=SUBPRODUCT)
price = models.FloatField()
image = models.ImageField(null=True, blank=True)
def __str__(self):
return self.name
#property
def imageURL(self):
try:
url = self.image.url
except:
url = ''
return url
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True)
date_ordered = models.DateTimeField(auto_now_add=True)
complete = models.BooleanField(default=False)
transaction_id = models.CharField(max_length=100, null=True)
def __str__(self):
return str(self.id)
#property
def shipping(self):
shipping = False
orderitems = self.orderitem_set.all()
for i in orderitems:
if i.product.digital == False:
shipping = True
return shipping
#property
def get_cart_total(self):
orderitems = self.orderitem_set.all()
total = sum([item.get_total for item in orderitems])
return total
#property
def get_cart_items(self):
orderitems = self.orderitem_set.all()
total = sum([item.quantity for item in orderitems])
return total
class OrderItem(models.Model):
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
order = models.ForeignKey(Order, on_delete=models.SET_NULL, null=True)
quantity = models.IntegerField(default=0, null=True, blank=True)
date_added = models.DateTimeField(auto_now_add=True)
#property
def get_total(self):
total = self.product.price * self.quantity
return total
My views.py:
def store(request):
data = cartData(request)
cartItems = data['cartItems']
order = data['order']
items = data['items']
context = {'cartItems':cartItems, 'order':order, 'items':items}
return render(request, 'store/store.html', context)
It's my template:
<div class="bottom">
<div class="total">
<span>Total</span>
<span class="total-amount">${{order.get_cart_total|floatformat:2}}</span>
</div>
Checkout
</div>
</div>
As I mentioned above, I have this error: 'NoneType' object has no attribute 'price' error by rendering the template. At the beginning it was working. I added 2 products and It worked. But, after adding products, it's giving this error. I tried many times, but each time I'm getting this error. Please help
As #IainShelvington suggested why you have set OrderItem.product nullable? Though you want it as it is, you can try the code below in the OrderItem model.
#property
def get_total(self):
if self.product and self.quantity: # handling case when either value is Null.
total = self.product.price * self.quantity
return total
else:
return 0

How to calculate the points based on the price of the total purchase in django models?

I am trying to create points earned by users after buying something and placed an order from the frontend. Also, I need to save the points on the database because users later use that points to buy something.
The points system looks like this.
Point System for % of the total purchase
Upto 10,000 = 1 %
10k to 50k =2.75%
50K plus = 5%
I haven't saved the price in DB, I just used it as a property so that it remains safe and cant be changed by anyone. It calculates whenever the get or post API is called.
class Order(models.Model):
ORDER_STATUS = (
('To_Ship', 'To Ship',),
('Shipped', 'Shipped',),
('Delivered', 'Delivered',),
('Cancelled', 'Cancelled',),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
order_status = models.CharField(max_length=50,choices=ORDER_STATUS,default='To_Ship')
ordered_date = models.DateTimeField(auto_now_add=True)
ordered = models.BooleanField(default=False)
#property
def total_price(self):
# abc = sum([_.price for _ in self.order_items.all()])
# print(abc)
return sum([_.price for _ in self.order_items.all()])
def __str__(self):
return self.user.email
class Meta:
verbose_name_plural = "Orders"
ordering = ('-id',)
class OrderItem(models.Model):
orderItem_ID = models.CharField(max_length=12, editable=False, default=id_generator)
order = models.ForeignKey(Order,on_delete=models.CASCADE, blank=True,null=True,related_name='order_items')
item = models.ForeignKey(Product, on_delete=models.CASCADE,blank=True, null=True)
order_variants = models.ForeignKey(Variants, on_delete=models.CASCADE,blank=True,null=True)
quantity = models.IntegerField(default=1)
ORDER_STATUS = (
('To_Ship', 'To Ship',),
('Shipped', 'Shipped',),
('Delivered', 'Delivered',),
('Cancelled', 'Cancelled',),
)
order_item_status = models.CharField(max_length=50,choices=ORDER_STATUS,default='To_Ship')
#property
def price(self):
total_item_price = self.quantity * self.order_variants.price
return total_item_price
Updated Code:
class Points(models.Model):
order = models.OneToOneField(Order,on_delete=models.CASCADE,blank=True,null=True)
points_gained = models.IntegerField(default=0)
def collect_points(sender,instance,created,**kwargs):
if created:
if instance.total_price <= 10000:
abc = 0.01* (instance.total_price)
else:
abc = 0.75 * (instance.total_price)
return abc
post_save.connect(collect_points,sender=Order)
def save(self,*args,**kwargs):
self.points_gained = self.collect_points()
super(Points, self).save(*args, **kwargs)
I tried using Django signals and overwrite save function to create points. But when I check db, there are no rows in points table although order is made.
OrderCreate API
class OrderSerializer(serializers.ModelSerializer):
billing_details = BillingDetailsSerializer()
order_items = OrderItemSerializer(many=True)
user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
#total_price = serializers.SerializerMethodField(source='get_total_price')
class Meta:
model = Order
fields = ['id','user','ordered_date','order_status', 'ordered', 'order_items', 'total_price', 'billing_details']
# depth = 1
def create(self, validated_data):
user = self.context['request'].user
if not user.is_seller:
order_items = validated_data.pop('order_items')
billing_details = validated_data.pop('billing_details')
order = Order.objects.create(user=user,**validated_data)
BillingDetails.objects.create(user=user,order=order,**billing_details)
for order_items in order_items:
OrderItem.objects.create(order=order,**order_items)
order.save()
return order
else:
raise serializers.ValidationError("This is not a customer account.Please login as customer.")
This answer is based on the comments and updated code.
I would have a relationship between the user and the points model, as the points belong to a user and not an order. Also this enables you to update the points whenever the same user orders again.
This results in the following model and post save signal:
def update_points(sender, instance, created, **kwargs):
if created:
if instance.total_price <= 10000:
points_gained = 0.01 * instance.total_price
else:
points_gained = 0.75 * instance.total_price
try:
# Check if user already has points and update if so
points = Points.objects.get(user=instance.user)
points.points_gained = points_gained
points.save(update_fields=['points_gained'])
except Points.DoesNotExist:
# User does not have points yet, create points
Points.objects.create(user=instance.user,
points_gained=points_gained)
post_save.connect(update_points, sender=Order)
class Points(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, blank=False, null=False)
points_gained = models.IntegerField(default=0)

Django Custom model method with parameters, NON DRY

After much research and trouble i came up with a non DRY solution, Hope someone can make it DRY.
All im trying to get is a calculated Price which takes a parameter and displays in the template accordingly.
i have a function get_price on model vehiclecategory which takes a parameter duration which is received from frontend forms.
MODELS.PY
class VehicleCategory(models.Model):
CATEGORY_CHOICES=(
('E-Cycle', 'E-Cycle'),
('E-Scooter', 'E-Scooter')
)
main_category = models.CharField(max_length=15, choices= CATEGORY_CHOICES)
title = models.CharField(unique=True, max_length=200)
image = models.ImageField(
null=True,
blank=True,
width_field="width_field",
height_field= "height_field",
default= 'e-bike.png',
upload_to='category')
width_field = models.IntegerField(default=250)
height_field = models.IntegerField(default=250)
slug =models.SlugField(max_length=200, db_index=True, unique=True)
def __str__(self):
return self.title
#GET PRICE
def get_price(self, duration):
for item in VehiclePrice.objects.all():
if item.vehicle_category.title == self.title and (duration >= item.slab.start and duration <= item.slab.end):
return item.total_price
class Meta():
verbose_name = "Vehicle Category"
verbose_name_plural = "Vehicle Categories"
class PriceSlab(models.Model):
start = models.IntegerField()
end = models.IntegerField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return '%s - %s ' % (self.start, self.end)
class VehiclePrice(CustomerStatus):
help_text= "Ensure no more than 2 digits after decimal"
vehicle_category = models.ForeignKey(VehicleCategory, on_delete= models.SET_NULL, null=True, related_name='vehicle_category_price')
slab = models.ForeignKey(PriceSlab, on_delete=models.CASCADE)
net_price = models.DecimalField(help_text= help_text, max_digits=5, decimal_places=2)
tax_percent = models.DecimalField(help_text=help_text, max_digits=4, decimal_places=2, default=18.00)
discount_percent = models.DecimalField(help_text=help_text,max_digits=4, decimal_places=2, default=0, blank=True)
#property
def total_tax(self):
tax = (self.net_price * self.tax_percent)/100
return tax
#property
def get_price(self):
total = self.net_price + self.total_tax
return total
#property
def total_discount(self):
discount = (self.get_price * self.discount_percent)/100
return discount
#property
def total_price(self):
total = self.get_price - self.total_discount
return round(total)
class Meta():
unique_together=('customer_status','vehicle_category' ,'slab')
def __str__(self):
return '%s - %s - %s' % (self.customer_status, self.vehicle_category, self.slab)
VIEWS.PY
class HomeView(ListView):
template_name = 'app/home.html'
def get(self, request):
if request.method == "GET":
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
if start_date and end_date:
start_date = datetime.strptime(start_date, "%d/%m/%Y").date()
end_date = datetime.strptime(end_date, "%d/%m/%Y").date()
duration = (end_date - start_date).days +1
print(duration)
vehiclecategory= VehicleCategory.objects.all()
context = {
'price1': VehicleCategory.objects.get(main_category= 'E-Cycle', title="Sporty").get_price(duration),
'price2': VehicleCategory.objects.get(main_category= 'E-Cycle', title="Step-Through").get_price(duration),
'price3': VehicleCategory.objects.get(main_category= 'E-Cycle', title="Fatbike").get_price(duration),
'price4': VehicleCategory.objects.get(main_category= 'E-Scooter', title="Scooter").get_price(duration),
'vehiclecategory1': vehiclecategory.filter(main_category= 'E-Cycle', title="Sporty"),
'vehiclecategory1': vehiclecategory.filter(main_category= 'E-Cycle', title="Step-Through"),
'vehiclecategory1': vehiclecategory.filter(main_category= 'E-Cycle', title="Fatbike"),
'vehiclecategory2': vehiclecategory.filter(main_category= 'E-Scooter', title="Scooter"),
'form':CartQuantityForm(),
'dateform': DateForm(),
}
else:
context={'dateform': DateForm(),}
return render(request, self.template_name, context )
after the user inputs the date range, the vehicles are displayed, but when u go to the cart and come back the same page, the page refreshes as a new one. how can keep the date range values intact and render the same page as the user got first time he searched for a vehicle, so that he can add or modify the vehicles selected???
You may put your start & end dates into your URL.
You can create 2 urls record dispatching the same view:
path(r'/prices/', HomeView.as_view())
path(r'/prices/(?P<start>\d{4}-\d{2}-\d{2})_(?P<end>\d{4}-\d{2}-\d{2})', HomeView.as_view())
Then you need to make some changes in your view:
class HomeView(ListView):
template_name = 'app/home.html'
def get(self, request, **kwargs):
start = kwargs.get('start')
end = kwargs.get('end')
if start is None or end is None:
# Ask for dates & Redirect to its new url with dates.
else:
# Check the dates, convert them to date object & do the rest.
Maybe not the best solution but the first thing came to my mind is this one.

Django: method of model from querying a different one

I have a model CartItem that has a ForeignKey to a Product model.
Because from Product model I get the description, image, etc.
However, I want to have a method called sub_total that returns and integer. I use this to calculate total to be paid for this CartItem.
This sub_total method query a different model costo_de_los_productos using some of the properties of CartItem. like: self.product.category.name, self.product.name, self.size, self.quantity.
I need to return an Integer from sub_total method.
However, something is not right with me query, if I comment it and return 0 it works, but total is 0.
def sub_total(self):
product_price = costo_de_los_productos.objects.filter(category=self.product.category.name,
product = self.product.name,
size=self.size,
quantity=self.quantity).values_list("price", flat=True)
What could be wrong?
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
size = models.CharField(max_length=20, choices=TAMANIOS)
quantity = models.CharField(max_length=20, choices=CANTIDADES)
file = models.FileField(upload_to='files', blank=True, null=True)
comment = models.CharField(max_length=100, blank=True, null=True, default='')
uploaded_at = models.DateTimeField(auto_now_add=True)
step_two_complete = models.BooleanField(default=False)
# def __str__(self):
# return str(self.id) + " - " + str(self.size) + " por " + str(self.quantity)
def sub_total(self):
product_price = costo_de_los_productos.objects.filter(category = self.product.category.name,
product = self.product.name,
size=self.size,
quantity=self.quantity).values_list("price", flat=True)
# print(type(product_price))
return product_price
costo_de_los_productos model:
class costo_de_los_productos(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
price = models.IntegerField(default=30)
size = models.CharField(max_length=20, choices=TAMANIOS)
quantity = models.CharField(max_length=20, choices=CANTIDADES)
product model:
class Product(models.Model):
name = models.CharField(max_length=250, unique=False)
slug = models.SlugField(max_length=250, unique=False)
description = models.TextField(blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
image = models.ImageField(upload_to='product', blank=True, null=True)
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('name',)
verbose_name = 'product'
verbose_name_plural = 'products'
def get_url(self):
return reverse('shop:ProdDetail', args=[self.category.slug, self.slug])
def __str__(self):
return '{}'.format(self.name)
category model:
class Category(models.Model):
name = models.CharField(max_length=250, unique=True)
slug = models.SlugField(max_length=250, unique=True)
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='category', blank=True, null=True)
video = EmbedVideoField(null=True, blank=True)
class Meta:
ordering = ('name',)
verbose_name = 'category'
verbose_name_plural = 'categories'
def get_url(self):
return reverse('shop:allCat', args=[self.slug])
def __str__(self):
return '{}'.format(self.name)
Image of "costo_de_los_productos" from Admin Panel:
UPDATE 1
Cannot print anything after the product_price query.
def sub_total(self):
print("Enters Subtotal")
print(self.product.category.name)
print(self.product.name)
print(self.size)
print(self.quantity)
product_price = costo_de_los_productos.objects.filter(category=self.product.category.name,
product=self.product.name,
size=self.size,
quantity=self.quantity).values_list("price", flat=True)[0]
print("Line after product_price query")
print(type(product_price))
return product_price
Hard coding the values doesn't return expected integer:
def sub_total(self):
print("Enters Subtotal")
print(self.product.category.name)
print(self.product.name)
print(self.size)
print(self.quantity)
product_price = costo_de_los_productos.objects.filter(category="Stickers",
product="Stickers transparentes",
size="5cm x 5cm",
quantity=300).values_list("price", flat=True)[0]
print("Line after product_price query")
print(type(product_price))
return product_price
prints results:
Enters Subtotal
Stickers
Stickers transparentes
5cm x 5cm
300