Django: After saving the Order and deleting the cart, Order looses information of ordered items

I want the cart items to be saved in each order. But after the cart is deleted, "service_items" becomes empty.

Steps:

  1. Add items to the cart
  2. Save order
  3. Delete cart

I think service_items = models.ManyToManyField(Cart) is causing the issue, but I don't know how to fix it.

Current Order object after saving:

{
    "id": 1,
    "customer": {
        "id": 1,
        "first_name": "Some",
        "last_name": "User",
        "email": "someuser@gmail.com",
        "phone_number": "+817056781234",
        "member_date": "2023-01-28T14:38:43.196580Z"
    },
    "status": false,
    "total": "15.00",
    "order_date": "2023-01-28T16:05:11.704636Z",
    "delivery_date": "2023-01-31T12:52:16.193078Z",
    "advance_payment": "9.00",
    "payment_method": "cash",
    "due_payment": 6.0,
    "service_items": []
}

Expected outcome:

{
    "id": 1,
    "customer": {
        "id": 1,
        "first_name": "Some",
        "last_name": "User",
        "email": "someuser@gmail.com",
        "phone_number": "+817056781234",
        "member_date": "2023-01-28T14:38:43.196580Z"
    },
    "status": false,
    "total": "15.00",
    "order_date": "2023-01-28T16:05:11.704636Z",
    "delivery_date": "2023-01-31T12:52:16.193078Z",
    "advance_payment": "9.00",
    "payment_method": "cash",
    "due_payment": 6.0,
    "service_items": [
        {
            "id": 1,
            "service_item": {
                "id": 1,
                "item_name": "Black Shirt",
                "description": "Half and Full Sleeve Black Shirts.",
                "wash_price": "3.00",
                "stain_treatment_price": "1.00",
                "iron_price": "2.00",
                "dry_wash_price": "4.00",
                "category": {
                    "id": 2,
                    "title": "Shirt"
                }
            },
            "is_wash": false,
            "is_stain_treatment": true,
            "is_iron": false,
            "is_dry_wash": true,
            "quantity": 3,
            "unit_price": "5.00",
            "price": "15.00",
            "customer": 1
        }
    ]
}

models.py

class ServiceItem(models.Model):
    item_name = models.CharField(max_length=200)
    description = models.TextField(null=True)
    
    wash_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    stain_treatment_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    iron_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    dry_wash_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)

    category = models.ForeignKey(Category, on_delete=models.PROTECT)                                                    

    def __str__(self) -> str:
        return self.item_name

class Cart(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    service_item = models.ForeignKey(ServiceItem, on_delete=models.CASCADE)

    is_wash = models.BooleanField(default=False)
    is_stain_treatment = models.BooleanField(default=False)
    is_iron = models.BooleanField(default=False)
    is_dry_wash = models.BooleanField(default=False)

    quantity = models.SmallIntegerField()
    unit_price = models.DecimalField(max_digits=6, decimal_places=2)
    price = models.DecimalField(max_digits=6, decimal_places=2)

    class Meta:
        unique_together = ('service_item', 'customer')                                                                   

    def __str__(self):
        return f'{self.customer} - {self.service_item}'

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    service_items = models.ManyToManyField(Cart)
    status = models.BooleanField(db_index=True, default=False)
    total = models.DecimalField(max_digits=6, decimal_places=2)
    order_date = models.DateTimeField(db_index=True, auto_now_add=True)
    delivery_date = models.DateTimeField(db_index=True, default=datetime.now() + timedelta(days = 1))
    advance_payment = models.DecimalField(max_digits=6, decimal_places=2, default=0)
    payment_method = models.CharField(max_length=70, db_index=True, default="cash")
    
    def __str__(self):
        return f'{self.customer} - {self.order_date}'

serializers.py

class CartSerializer(serializers.ModelSerializer):
    service_item = ServiceItemSerializer(read_only=True)
    class Meta:
        model = Cart
        fields = '__all__'


class OrderSerializer(serializers.ModelSerializer):
    due_payment = serializers.SerializerMethodField(method_name='calculate_balance')
    service_items = CartSerializer(many=True)
    class Meta:
        model = Order
        fields = ['id', 'customer', 'status', 'total', 'order_date', 'delivery_date',
                    'advance_payment', 'payment_method', 'due_payment', 'service_items']                         
        depth = 1

    def calculate_balance(self, product:Order):
        if product.advance_payment > 0:
            return Decimal(product.total - product.advance_payment)
        else:
            return Decimal(product.total)

views.py

@api_view(['GET', 'POST'])
def cart(request):
    if request.method == 'GET':
        items = Cart.objects.all()
        serialized_items = CartSerializer(items, many=True)
        return Response(serialized_items.data, status=status.HTTP_200_OK)

    elif request.method == 'POST':
        customer_phone = request.data.get("customer_phone")
        customer = get_object_or_404(Customer, phone_number=customer_phone)                                         # Customer Info
        
        item_name = request.data['item']
        item = get_object_or_404(ServiceItem, item_name=item_name)
        item_quantity = int(request.data['quantity'])

        wash = request.data['is_wash']
        stain_treatment = request.data['is_stain_treatment']
        iron = request.data['is_iron']
        dry_wash = request.data['is_dry_wash']

        item_price = Decimal(0)
        if wash:
            item_price += item.wash_price
        if stain_treatment:
            item_price += item.stain_treatment_price
        if iron:
            item_price += item.iron_price
        if dry_wash:
            item_price += item.dry_wash_price

        cart_item = Cart(customer_id=customer.id, service_item=item, is_wash=wash, 
                        is_stain_treatment=stain_treatment, is_iron=iron, is_dry_wash=dry_wash,
                        quantity=item_quantity, unit_price=item_price, price=Decimal(item_price*item_quantity))
        try:
            cart_item.save()
        except:
            return Response({'message': 'The item may have already been added to the cart'}, 
                            status=status.HTTP_406_NOT_ACCEPTABLE)
        return Response({'message': '{} {} has been added succesfully'.format(item_quantity,item_name)},
                        status=status.HTTP_201_CREATED)

@api_view(['GET', 'POST'])
# @permission_classes([IsAuthenticated])
@throttle_classes([UserRateThrottle])
def orders(request):
    if request.method == 'GET':
        orders = Order.objects.all()
        serialized_orders = OrderSerializer(orders, many=True)
        return Response(serialized_orders.data, status=status.HTTP_200_OK)

    elif request.method == 'POST':
        customer = get_object_or_404(Customer, phone_number=customer_phone)
        cart = Cart.objects.filter(customer=customer)
        if cart:
            total = cart.aggregate(Sum('price'))['price__sum']
            # discount = ???
            # staff = ??
            delivery_date = request.data.get("delivery_date")
            advance_payment = Decimal(request.data.get("advance_payment"))
            payment_method = request.data.get("payment_method")
            
            order = Order.objects.create(customer=customer, total=total, delivery_date=delivery_date, 
                                            advance_payment=advance_payment, payment_method=payment_method)              
                
            order.service_items.set(cart)
            order.save()
            
            
            cart.delete()
            return Response({'message': 'Order successfully placed'}, status=status.HTTP_201_CREATED)
        else:
            return Response({'message': 'Cart is Empty'}, status=status.HTTP_204_NO_CONTENT)

Is there any other way to implement what I am expecting?

models.py

class ServiceItem(models.Model):
    item_name = models.CharField(max_length=200)
    description = models.TextField(null=True)
    
    wash_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    stain_treatment_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    iron_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)
    dry_wash_price = models.DecimalField(max_digits=6, decimal_places=2, null=False, default=0)

    category = models.ForeignKey(Category, on_delete=models.PROTECT)                                                    

    def __str__(self) -> str:
        return self.item_name

class Cart(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    service_item = models.ForeignKey(ServiceItem, on_delete=models.CASCADE)

    is_wash = models.BooleanField(default=False)
    is_stain_treatment = models.BooleanField(default=False)
    is_iron = models.BooleanField(default=False)
    is_dry_wash = models.BooleanField(default=False)
    is_placed = models.BooleanField(default=False)
    quantity = models.SmallIntegerField()
    unit_price = models.DecimalField(max_digits=6, decimal_places=2)
    price = models.DecimalField(max_digits=6, decimal_places=2)

    class Meta:
        unique_together = ('service_item', 'customer')                                                                   

    def __str__(self):
        return f'{self.customer} - {self.service_item}'

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    service_items = models.ManyToManyField(Cart)
    status = models.BooleanField(db_index=True, default=False)
    total = models.DecimalField(max_digits=6, decimal_places=2)
    order_date = models.DateTimeField(db_index=True, auto_now_add=True)
    delivery_date = models.DateTimeField(db_index=True, default=datetime.now() + timedelta(days = 1))
    advance_payment = models.DecimalField(max_digits=6, decimal_places=2, default=0)
    payment_method = models.CharField(max_length=70, db_index=True, default="cash")
    
    def __str__(self):
        return f'{self.customer} - {self.order_date}'

#views.py

api_view(['GET', 'POST'])
def cart(request):
    if request.method == 'GET':
        items = Cart.objects.filter(is_placed=0)
        serialized_items = CartSerializer(items, many=True)
        return Response(serialized_items.data, status=status.HTTP_200_OK)

    elif request.method == 'POST':
        customer_phone = request.data.get("customer_phone")
        customer = get_object_or_404(Customer, phone_number=customer_phone)                                         # Customer Info
        
        item_name = request.data['item']
        item = get_object_or_404(ServiceItem, item_name=item_name)
        item_quantity = int(request.data['quantity'])

        wash = request.data['is_wash']
        stain_treatment = request.data['is_stain_treatment']
        iron = request.data['is_iron']
        dry_wash = request.data['is_dry_wash']

        item_price = Decimal(0)
        if wash:
            item_price += item.wash_price
        if stain_treatment:
            item_price += item.stain_treatment_price
        if iron:
            item_price += item.iron_price
        if dry_wash:
            item_price += item.dry_wash_price

        cart_item = Cart(customer_id=customer.id, service_item=item, is_wash=wash, 
                        is_stain_treatment=stain_treatment, is_iron=iron, is_dry_wash=dry_wash,
                        quantity=item_quantity, unit_price=item_price, price=Decimal(item_price*item_quantity))
        try:
            cart_item.save()
        except:
            return Response({'message': 'The item may have already been added to the cart'}, 
                            status=status.HTTP_406_NOT_ACCEPTABLE)
        return Response({'message': '{} {} has been added succesfully'.format(item_quantity,item_name)},
                        status=status.HTTP_201_CREATED)

@api_view(['GET', 'POST'])
# @permission_classes([IsAuthenticated])
@throttle_classes([UserRateThrottle])
def orders(request):
    if request.method == 'GET':
        orders = Order.objects.all()
        serialized_orders = OrderSerializer(orders, many=True)
        return Response(serialized_orders.data, status=status.HTTP_200_OK)

    elif request.method == 'POST':
        customer = get_object_or_404(Customer, phone_number=customer_phone)
        cart = Cart.objects.filter(customer=customer,is_placed=0) # be shure here you shoud have only one cart for the user other will be is_placed=1
        if cart:
            total = cart.aggregate(Sum('price'))['price__sum']
            # discount = ???
            # staff = ??
            delivery_date = request.data.get("delivery_date")
            advance_payment = Decimal(request.data.get("advance_payment"))
            payment_method = request.data.get("payment_method")
            
            order = Order.objects.create(customer=customer, total=total, delivery_date=delivery_date, 
                                            advance_payment=advance_payment, payment_method=payment_method)              
                
            order.service_items.set(cart)
            order.save()
            
            cart.update(
                    is_placed=1
             )
            # so when you place the order cart is not deleted and also deleting the data from the database is not a good practice e.g if you work in a company you are not allowed to use delete rather than you use check is_deleted.
            return Response({'message': 'Order successfully placed'}, status=status.HTTP_201_CREATED)
        else:
            return Response({'message': 'Cart is Empty'}, status=status.HTTP_204_NO_CONTENT)
Back to Top