Suppose I have a view for saving an order to a database based on cart contents:
def cart_checkout(request):
order = Order()
order.first_name = 'x'
order.last_name = 'y'
order.address = 'z'
order.save()
cart = Cart(request)
for product_id, product_quantity in cart:
product = Product.objects.get(pk=product_id)
order_item = OrderItem()
order_item.order = order
order_item.name = product.name
order_item.price = product.price
order_item.amount = product_quantity
order_item.save()
order.update_total_price() # updates the Order total price field with the sum of order items prices
order.save()
return HttpResponse('Checked-out!')
As you can see, I am calling order.save() twice in this view: first to create an Order instance the OrderItems can be attached to in the for loop, and then to update the total price of the order based on order items in it. If I removed the first .save(), I would get an error on the second one telling me the order needs to be saved first.
Calling the .save() method twice does not seem DRY enough to me. Is there a way to do it only once?
Note that I am not subclassing ModelForm, so I cannot use .save(commit=False). Also, I do not want to just hide the save() method in the update_total_price() method.
Models.py:
from django.db import models
from .mixins import DisplayNameMixin
class Product(DisplayNameMixin, models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=6, decimal_places=2)
amount = models.IntegerField()
class Order(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
address = models.CharField(max_length=255)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
def update_total_price(self):
order_items = self.orderitem_set.all()
self.total_price = sum([
x.price * x.amount
for x in order_items
])
class OrderItem(models.Model):
order = models.ForeignKey('Order', on_delete=models.CASCADE)
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=6, decimal_places=2)
amount = models.IntegerField()
I think, you can't help but save the order twice, as you need to have an order_id to create the OrderItems, and then update the order with the items' amount.
I have a few suggestions to make though.
You can make total_price a calculated property, so that you would not have to save the order:
class Order(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
address = models.CharField(max_length=255)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
#property
def total_price(self):
return sum([
x.price * x.amount
for x in self.orderitem_set.all()
])
From DB theory perspective your DB structure is wrong. It needs to be normalized first.
Why it is wrong?
Order.total_price is redundant table column. That information can be found with aggregation. At DB level there are no protections preventing DB users (Django app in your case) from entering compromised data. So your DB can be telling two different total prices (Order.total_price != SUM(OrderItem.price * OrderItem.amount)) at the same time.
So to appease DB normalization gods you need to drop total_price field and use Django aggregations/annotations (https://docs.djangoproject.com/en/3.0/topics/db/aggregation/) when you need to access it.
Saying that, there could be a totally valid reason to put total_price inside Order table. That reason usually is performance. Sometimes SQL query complexity (It is very annoying to filter by an aggregated column).
But there is a price. And that price is de-normalization of your DB. In your case you are paying breaking DRY principle.
Just make sure that you are calling both save()'s in the same transaction.
To expand on petraszd's answer (i.e. remove the total_price field) and engin_ipek's answer (i.e. add total_price as a calculated property), you could try making total_price a cached property, to avoid calculating the same value more than once - as long as the same Order instance is passed around.
You would also probably make the calculation a little less expensive if you used aggregation to calculate the total price, as petraszd mentioned, e.g. adding the products of price and amount.
Related
I’m a python/django begginer. I decided to build a e-commerce website using django for an academic project. I’ve been able to develop enough of the project and build understanding of the subject, but right now I’m having issues finding a way to subtracts the number of items listed in the inventory whenever a order is made.
That’s the code for the models, evey product has it's own stock quantity call inventory:
class Product(models.Model):
name = models.CharField(max_length=200, null=True)
price = models.FloatField()
description = models.TextField(default='', null=True, blank=True)
digital = models.BooleanField(default=False,null=True, blank=True)
image = models.ImageField(null=True, blank=True)
inventory = models.IntegerField(default=0)
def __str__(self):
return self.name
def has_inventory(self):
return self.inventory > 0
This is the code I made to subtract base on quantity of the item ordered, but I can’t make it work, it won’t subtract the number of items from the inventory on the product stock.
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)
def __str__(self):
return str(self.product) + " x " + str(self.quantity)
def inventory(self):
product.inventory = self.inventory
product.inventory -= int(self.quantity)
return inventory
What could I do to make it work?
All logic/action should be written under views.py file. You could create a function where it takes in a request, and when it does, it takes in all the value inputted through a form, and you could use filter to filter out the products you want to subtract its inventory and update query by Django to update the inventory.
It should look something like this inside your views function:
Product.objects.filter(name = name, description = description, digital = digital).update(Inventory = F('Inventory')-inventory)
Here is Django's documentation on queries: Django's Making Queries
I think there are a few problems with the snippet above.
First, the OrderItem.inventory is not referring the right value, it should be like the snippet below.
def inventory(self):
// remember the current stock is stored on Product.inventory
return self.product.inventory - self.quantity
Second, The method name should be remaining_stock not inventory to prevent misunderstanding.
def remaining_stock(self):
return self.product.inventory - self.quantity
Also, don't forget if you want to store inventory of the product please call the save method after successfully inserting the OrderItem.
I have an issue with database and sum in Django.
I have 3 tables: customer, order and orderLine.
For a report, I would like to calculate the sum of all line price for every order of a customer.
class Customer(models.Model):
firstName = models.CharField(max_length=200)
lastName = models.CharField(max_length=200)
mail = models.EmailField(max_length=100)
etc...
def get_count_of_orders(self):
return self.orders.count()
def get_sum_line_prince_of_all_orders(self):
???????????
return (sum_of_all_line_prince_all_orders)
class Order(models.Model):
orderNum = models.CharField(max_length=200)
customer = models.ForeignKey(Customer, related_name="orders")
globalAmount = models.DecimalField(max_digits=20, decimal_places=4)
...
class OrderLine(models.Model):
order = models.ForeignKey(Order, related_name="ordersLines")
linePrice = models.DecimalField(max_digits=20, decimal_places=4)
...
I don't know what to set in get_sum_of_orders to get the right result.
I've trayed different things like annotate or aggregate.
But without success at the moment.
I didn't understand this process at the moment.
Could you help me?
You can access all orders with:
self.orders.all()
and you can iterate them with:
sum = 0
for each_order in self.orders.all():
sum += each_order.globalAmount
return sum
Here I presume that globalAmount is the amount you need to calculate.
If you need to reach OrderLine -> linePrice through each customer and calculate the sum of the linePrice(s) of each customer, try the following:
for each_order in self.orders.all(): #each order
for each_OrderLine in each_order.ordersLines.all()
sum += each_OrderLine.linePrice
Or you could use list comprehension:
sum([myorder.linePrice for myorder in order.ordersLines.all() for order in self.orders.all()])
I am writing a form to let a user enter a purchase from the template. A couple things need to happen:
the purchase goes to populate a row in the replenishment table
some fields of the replenishment table get updated based on what the user has input
here is what my model look like:
class replenishment(models.Model):
Id = models.CharField(max_length=100, primary_key=True, verbose_name= 'references')
Name = models.CharField(max_length=200)
Quantity = models.FloatField(default=0)
NetAmount = models.FloatField(default=0)
SupplierID = models.CharField(max_length=200)
Supplier = models.CharField(max_length=200)
SellPrice = models.FloatField(default=0)
StockOnOrder = models.FloatField(default=0)
StockOnHand = models.FloatField(default=0)
def __str__(self):
return self.reference
and the form:
class ProcurementOperationRecord(forms.Form)
Id = forms.CharField(required=True)
Quantity = forms.FloatField(required=True)
NetAmount = forms.FloatField(required=True)
Supplier = forms.CharField(required=True)
SellPrice = forms.FloatField(required=True)
I have no clue how to let the user input the values in form and automatically add Quantity to StockOnOrder as well as automatically recognize the SupplierID based on Supplier. At this point I don't know where to start really. At least, is it possible to achieve what I try to do?
First, I've changed some things around and added some comments to what and why I did them.
# models/classes in python are singular AND camel cased (99.9%)
class Supplier(models.Model):
...
# models/classes in python are singular AND camel cased (99.9%)
class Replenishment(models.Model):
# attributes are normally lower case and snake cased (99.9%)
# try not to do this, a CharField??, unless you're using a guid? if so use UUIDField()
# https://docs.djangoproject.com/en/3.1/ref/models/fields/#uuidfield
id = models.CharField(db_column='Id', max_length=100, primary_key=True, verbose_name='references')
name = models.CharField(db_column='Name', max_length=200)
quantity = models.FloatField(db_column='Quantity', default=0)
net_amount = models.FloatField(db_column='NetAmount', default=0)
# deleted your field "Supplier" -- with this change you can join to the other table and get what you need without having to duplicate anything
supplier = models.ForeignKey(Supplier, db_column='SupplierID')
sell_price = models.DecimalField(db_column='SellPrice', default=0, max_digits=6, decimal_places=2) # You're asking for trouble if you keep this as FloatField
stock_on_order = models.IntegerField(db_column='StockOnOrder', default=0) # how can you have ordered a .5 for your stock? changed to IntegerField
stock_on_hand = models.IntegerField(db_column='StockOnHand', default=0) # how can you have a .5 of your stock? changed to IntegerField
class Meta:
db_table = 'replenishment' # try not to do this either.. let django come up with the name.. unless you're using an existing database/table?
...
# models/classes in python are singular AND camel cased (99.9%)
# django has a standard that they normally postfix forms with "Form" at the end of the class (no matter if it's a ModelForm or regular Form)
class ProcurementOperationRecordForm(forms.ModelForm)
class Meta:
model = Replenishment
fields = ('id', 'quantity', 'net_amount', 'supplier', 'sell_price')
# I would remove the "id", the client shouldn't care or know about it..
Now to create and update. (This would live inside a view)
# creating?
form = ProcurementOperationRecordForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(..) or render(..)
# updating?
replenishment = Replenishment.objects.get(id='...something')
form = ProcurementOperationRecordForm(data=request.POST, instance=replenishment)
if form.is_valid():
form.save()
return redirect(..) or render(..)
This is just a general idea. You can try something like this.
First get the user input values of quantity and supplier like this from the valid form.
quantity = form.cleaned_data.get('quantity')
supplier = form.cleaned_data.get('supplier')
Then you can update your replenishment model
replenishment.objects.filter(Supplier=supplier).update(StockOnOrder=quantity)
i want to remain the same data as i saved before , after changing , or deleted the model which provide the foreign key ,
i tried to
on_delete=models.SET_NULL
but still removes the instance which i've saved before
class Product(models.Model):
product_name = models.CharField(unique=True, max_length=50 ,
blank=False,null=False)
price = models.PositiveIntegerField()
active = models.BooleanField(default=True)
def __str__(self):
return self.product_name
class Order(models.Model):
id = models.AutoField(primary_key = True)
products = models.ManyToManyField(Product ,through='ProductOrder')
date_time = models.DateTimeField(default=datetime.now())
#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.SET_NULL , null=True)
ordering = models.ForeignKey(Order, on_delete=models.SET_NULL,blank=True,null=True ,default='')
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.product} : {self.quantity}"
and how save the total price in variable , if the the price in Product updated , it remains as the first time ?
and how to prevent losing data from Order model after removing a product from Product model thanks for advice
There are multiple ways to solve this, but it generally has little to do with Django itself, because these are business decisions:
Don't link to the Product in the Order (and ProductOrder) models, but save the product information directly in the ProductOrder model. You could for example use a JSONField (if you're on PostgreSQL) and keep all the product details there, including the price.
Don't change a Product once it's been ordered once. i.e. you can make it impossible to change/delete a Product (on_delete=models.PROTECT). Give a Product a unique SKU and version number, create new versions of your Product when the price changes. That way all orders will use a reference to the Product that was actually ordered. When fetching Products that can be purchased, always fetch the latest version of the Product. You could create a new ProductVersion table for this and link to that instead of the Product.
Don't allow deleting a Product (on_delete=PROTECT), but allow changing the price. Note that if other characteristics of the Product change (such as color), you should always create a new Product. Keep a copy of the paid price in the ProductOrder itself (the line item of the order). This is the most common approach. You just copy the price in the line items, so that you always have actual price paid by the customer.
The solution you choose depends a bit on the business requirements of your company, i.e. how products are managed, what needs to be kept, etc... It also depends on what you want your customers to be able to see when looking up their historical orders.
I'm new on django. I'm making the simple inventory application. Here's my Model:
class Received(models.Model):
item = models.ForeignKey(Item)
weight = models.DecimalField(max_digits=8, decimal_places=2)
....
class Sold(models.Model):
item = models.ForeignKey(Item)
weight = models.DecimalField(max_digits=8, decimal_places=2)
....
class Inventory(models.Model):
item = models.OneToOne(Item)
weight_received = ?
weight_sold = ?
timestamp = models.DateTimeField()
....
class InventoryHistory(models.Model):
# I have no idea
item = models.ForeignKey(Item)
date = models.DateField(auto_now=True)
total_weight = models.DecimalField(max_digits=8, decimal_places=2)
What I wanna do is when:
1.) I do an input data on Received or Sold the Inventory should automatically update (where Inventory.weight_in is SUM of Received.weight and Inventory.weight_out is SUM of Sold.weight.)
2.) I do a delete on them, Inventory should be automatically update
3.) I do en edit on them, Inventory should be automatically update
Is it possible, and how?
And here's another one question about my lack of database knowledge problem. Is it necessary to me to make a InventoryHistory where I can track a history of inventory in daily?
Thank you...
You can use signals. In particular django.db.models.signals.post_save and django.db.models.signals.post_delete. For tracking history I recommend to use django-reversion.