update cart logic

This commit is contained in:
Parsa Nazer
2025-09-23 10:17:19 +03:30
parent cd6c5bf343
commit dab89b93a7
13 changed files with 469 additions and 153 deletions
+6
View File
@@ -43,6 +43,12 @@ class BankRecordInline(StackedInline):
@admin.register(Cart)
class CartAdmin(ModelAdmin):
pass
@admin.register(OrderModel) @admin.register(OrderModel)
class OrderAdmin(ModelAdmin, ImportExportModelAdmin): class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm import_form_class = ImportForm
@@ -0,0 +1,51 @@
# Generated by Django 5.1.2 on 2025-09-21 06:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0029_alter_ordermodel_discount_amount'),
('product', '0055_alter_productmodel_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='ordermodel',
name='status',
field=models.CharField(choices=[('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), ('RECEIVED', 'تحویل شده'), ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده')], default='ADMIN_PENDING', max_length=20, verbose_name='وضعیت سفارش'),
),
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='carts', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'سبد خرید',
'verbose_name_plural': 'سبد های خرید',
},
),
migrations.CreateModel(
name='CartItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='order.cart')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='product.productvariant')),
],
options={
'verbose_name': 'ایتم سبد خرید',
'verbose_name_plural': 'ایتم های سبد خرید',
'unique_together': {('cart', 'product')},
},
),
]
@@ -0,0 +1,31 @@
# Generated by Django 5.1.2 on 2025-09-21 08:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0029_shopmodel'),
('order', '0030_alter_ordermodel_status_cart_cartitem'),
]
operations = [
migrations.AddField(
model_name='cart',
name='address',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='carts', to='account.useraddressmodel', verbose_name='ادرس'),
),
migrations.AddField(
model_name='cartitem',
name='discount',
field=models.PositiveSmallIntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='cartitem',
name='discount_code',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='order.discountcode', verbose_name='کدتخفیف'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2025-09-21 08:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0031_cart_address_cartitem_discount_and_more'),
]
operations = [
migrations.AddField(
model_name='cart',
name='discount_code',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='order.discountcode', verbose_name='کدتخفیف'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-09-21 08:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0032_cart_discount_code'),
]
operations = [
migrations.RemoveField(
model_name='cartitem',
name='discount_code',
),
migrations.AlterField(
model_name='cartitem',
name='discount',
field=models.PositiveSmallIntegerField(default=0),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-09-21 09:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0033_remove_cartitem_discount_code_and_more'),
('product', '0055_alter_productmodel_options'),
]
operations = [
migrations.RenameField(
model_name='cartitem',
old_name='product',
new_name='product_variant',
),
migrations.AlterUniqueTogether(
name='cartitem',
unique_together={('cart', 'product_variant')},
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-09-22 06:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0034_rename_product_cartitem_product_variant_and_more'),
]
operations = [
migrations.RemoveField(
model_name='cartitem',
name='discount',
),
]
+110 -66
View File
@@ -6,9 +6,11 @@ from django_jalali.db import models as jmodels
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.conf import settings from django.conf import settings
class DiscountCode(models.Model): class DiscountCode(models.Model):
code = models.CharField(max_length=50, verbose_name='کد تخفیف') code = models.CharField(max_length=50, verbose_name='کد تخفیف')
percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') percent = models.DecimalField(
max_digits=4, decimal_places=2, verbose_name='درصد')
quantity = models.PositiveIntegerField(verbose_name='تعداد') quantity = models.PositiveIntegerField(verbose_name='تعداد')
expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا') expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا')
@@ -33,11 +35,96 @@ class DiscountCode(models.Model):
print('log later bug') print('log later bug')
class Cart(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='carts'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL,
related_name='carts', null=True, verbose_name='ادرس')
discount_code = models.ForeignKey(
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
class Meta:
verbose_name = 'سبد خرید'
verbose_name_plural = 'سبد های خرید'
def __str__(self):
return f"Cart for {self.user.email}"
@property
def discount_code_amount(self):
if self.discount_code:
return int(int(self.cart_total - self.items_discount_amount) * (self.discount_code.percent / 100))
else:
return 0
@property
def items_discount_amount(self):
return int(sum(item.item_discount_amount for item in self.items.all()))
@property
def total_before_tax(self):
return self.cart_total - (self.discount_code_amount + self.items_discount_amount)
@property
def tax_amount(self):
return int(self.total_before_tax * settings.DEFAULT_TAX_RATE / 100)
@property
def cart_total(self):
return sum(item.price_before_discount for item in self.items.all())
@property
def final_price(self):
return self.total_before_tax + self.tax_amount
class CartItem(models.Model):
cart = models.ForeignKey(
Cart,
on_delete=models.CASCADE,
related_name='items'
)
product_variant = models.ForeignKey(
ProductVariant,
on_delete=models.CASCADE,
related_name='cart_items'
)
quantity = models.PositiveIntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید'
unique_together = ('cart', 'product_variant')
def __str__(self):
return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}"
@property
def price_before_discount(self):
return self.quantity * self.product_variant.price_before_discount
@property
def item_discount_amount(self):
return self.product_variant.discount_amount * self.quantity
@property
def price_after_discount(self):
return self.price_before_discount - self.item_discount_amount
@property
def discount(self):
return self.product_variant.discount
class OrderModel(models.Model): class OrderModel(models.Model):
objects = jmodels.jManager() objects = jmodels.jManager()
STATUS_CHOICES = [ STATUS_CHOICES = [
('CART', 'در سبد خرید'),
('ADMIN_PENDING', 'در انتظار تایید'), ('ADMIN_PENDING', 'در انتظار تایید'),
('PENDING', 'درحال پردازش'), ('PENDING', 'درحال پردازش'),
('POSTED', 'ارسال شده'), ('POSTED', 'ارسال شده'),
@@ -45,16 +132,24 @@ class OrderModel(models.Model):
('CANCELED', 'لغو شده'), ('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'), ('REFUNDED', 'مرجوع شده'),
] ]
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') user = models.ForeignKey(User, on_delete=models.SET_NULL,
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس') null=True, related_name='orders', verbose_name='کاربر')
created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش") address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL,
related_name='orders', null=True, verbose_name='ادرس')
created_at = jmodels.jDateField(
blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت") is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت")
discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") discount_code = models.ForeignKey(
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='CART', verbose_name="وضعیت سفارش") DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
discount_amount = models.BigIntegerField(null=True, blank=True, verbose_name='مقدار کد تخفیف') status = models.CharField(max_length=20, choices=STATUS_CHOICES,
default='ADMIN_PENDING', verbose_name="وضعیت سفارش")
discount_amount = models.BigIntegerField(
null=True, blank=True, verbose_name='مقدار کد تخفیف')
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') final_price = models.BigIntegerField(
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(
null=True, blank=True, verbose_name='کل سبد خرید')
def __str__(self): def __str__(self):
return f'سفارش: {self.pk + 1000}' return f'سفارش: {self.pk + 1000}'
@@ -64,69 +159,18 @@ class OrderModel(models.Model):
verbose_name_plural = 'سفارشات' verbose_name_plural = 'سفارشات'
def _cal_discount_amount(self, cart_total):
discount_percent = self.discount_code.percent if self.discount_code else 0
return int(cart_total * discount_percent / 100)
def _cal_tax(self, cart_total, discount_amount):
tax_rate = getattr(settings, 'DEFAULT_TAX_RATE', 20)
return int((cart_total - discount_amount) * tax_rate / 100)
def _cal_cart_total(self):
from django.db.models import Sum, F, FloatField
return self.items.aggregate(
total=Sum(F('price') * (1 - F('discount_percent')/100) * F('quantity'),
output_field=FloatField()
)).get('total') or 0
def _cal_final_price(self, cart_total, discount_amount, tax):
return cart_total - discount_amount + tax
def update_order(self):
if self.status == 'CART':
cart_total = self._cal_cart_total()
discount_amount = self._cal_discount_amount(cart_total)
self.discount_amount = discount_amount
self.cart_total = cart_total
tax = self._cal_tax(cart_total, discount_amount)
self.tax = tax
self.final_price = self._cal_final_price(cart_total, discount_amount, tax)
self.save()
else:
pass
class OrderItemModel(models.Model): class OrderItemModel(models.Model):
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') order = models.ForeignKey(
OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.BigIntegerField(verbose_name='قیمت') price = models.BigIntegerField(verbose_name='قیمت')
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") product = models.ForeignKey(
ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف') discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
class Meta: class Meta:
verbose_name = 'ایتم سبد خرید' verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید' verbose_name_plural = 'ایتم های سبد خرید'
def set_or_update_fields(self):
self.price = self.product.price
self.discount_percent = self.product.discount
def __str__(self): def __str__(self):
return f'({self.product}) - ({self.order.user})' return f'({self.product}) - ({self.order.user})'
def save(self, *args, **kwargs):
self.clean()
self.set_or_update_fields()
super().save(*args, **kwargs)
self.order.update_order()
def delete(self, *args, **kwargs):
self.clean()
order = self.order
super().delete(*args, **kwargs)
order.update_order()
def clean(self):
if self.pk and self.order.status != "CART":
raise ValidationError("ایتم ها فقط در حالت سبد خرید قابل ادیت هستند")
+22 -20
View File
@@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import OrderItemModel, OrderModel, DiscountCode from .models import OrderItemModel, OrderModel, DiscountCode, Cart, CartItem
from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer
from account.serializers import UserAddressSerializer from account.serializers import UserAddressSerializer
from product.models import ProductVariant from product.models import ProductVariant
@@ -52,38 +52,37 @@ class OrderItemSerailzier(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
discount = serializers.SerializerMethodField() discount = serializers.SerializerMethodField()
class Meta: class Meta:
model = OrderItemModel model = CartItem
exclude = ('order',) exclude = ('cart',)
read_only_fields = ('order', 'product', 'discount_percent') read_only_fields = ('cart', 'product', 'discount_percent')
def get_product(self, obj): def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data return ProductVariantSerialzier(instance=obj.product_variant, context={'request': self.context.get('request')}).data
def get_discount_amount(self, obj): def get_discount_amount(self, obj):
discount_amount = int(obj.price * (obj.product.discount / 100)) return f'{obj.item_discount_amount:,.0f} تومان'
return f'{(discount_amount * obj.quantity):,.0f} تومان'
def get_final_price(self, obj): def get_final_price(self, obj):
final_price = obj.price - int(obj.price * (obj.product.discount / 100)) return f'{obj.price_after_discount:,.0f} تومان'
return f'{(final_price * obj.quantity):,.0f} تومان'
def get_price(self, obj): def get_price(self, obj):
return f'{(obj.price * obj.quantity):,.0f} تومان' return f'{obj.price_before_discount:,.0f} تومان'
def get_discount(self, obj): def get_discount(self, obj):
return obj.product.discount return obj.product_variant.discount
from django.conf import settings
class CartSerializer(serializers.ModelSerializer): class CartSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True) items = OrderItemSerailzier(many=True)
cart_total = serializers.SerializerMethodField() cart_total = serializers.SerializerMethodField()
tax = serializers.SerializerMethodField() tax_amount = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField() discount_code = serializers.SerializerMethodField()
items_discount_amount = serializers.SerializerMethodField()
address = UserAddressSerializer() address = UserAddressSerializer()
class Meta: class Meta:
model = OrderModel model = Cart
fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price', 'address'] fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount', 'final_price', 'address']
def get_discount_code(self, obj): def get_discount_code(self, obj):
@@ -91,20 +90,23 @@ class CartSerializer(serializers.ModelSerializer):
return { return {
'code': f'{obj.discount_code.code}', 'code': f'{obj.discount_code.code}',
'percent': obj.discount_code.percent, 'percent': obj.discount_code.percent,
'amount': f'{10000:,.0f} تومان' 'amount': f'{obj.discount_code_amount:,.0f} تومان'
} }
else: else:
return None return None
def get_tax(self, obj): def get_tax_amount(self, obj):
return f'{1000:,.0f} تومان' return f'{obj.tax_amount:,.0f} تومان'
def get_cart_total(self, obj): def get_cart_total(self, obj):
return f'{10000:,.0f} تومان' return f'{obj.cart_total:,.0f} تومان'
def get_items_discount_amount(self, obj):
return f'{obj.items_discount_amount:,.0f} تومان'
def get_final_price(self, obj): def get_final_price(self, obj):
return f'{8000:,.0f} تومان' return f'{obj.final_price:,.0f} تومان'
class OrderListSerializer(serializers.ModelSerializer): class OrderListSerializer(serializers.ModelSerializer):
+135 -64
View File
@@ -31,9 +31,8 @@ class ApplyDiscountView(APIView):
serializer_class = DiscountCodeSerializer serializer_class = DiscountCodeSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
cart_order, created = OrderModel.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
status='CART'
) )
discount_code = get_object_or_404(DiscountCode, code=request.data.get('code')) discount_code = get_object_or_404(DiscountCode, code=request.data.get('code'))
@@ -44,9 +43,8 @@ class ApplyDiscountView(APIView):
return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK) return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
def delete(self, request): def delete(self, request):
cart_order, created = OrderModel.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
status='CART'
) )
cart_order.discount_code = None cart_order.discount_code = None
cart_order.save() cart_order.save()
@@ -59,9 +57,8 @@ class CartItemClear(APIView):
tags=["order cart"] tags=["order cart"]
) )
def delete(self, request): def delete(self, request):
cart_order, created = OrderModel.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
status='CART'
) )
cart_order.items.all().delete() cart_order.items.all().delete()
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
@@ -82,9 +79,8 @@ class CartItemViews(APIView):
quantity = product_variant.in_stock quantity = product_variant.in_stock
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد' response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
cart_order, created = OrderModel.objects.get_or_create(user=request.user, status='CART') cart_order, created = Cart.objects.get_or_create(user=request.user)
order_item, created = OrderItemModel.objects.get_or_create(order=cart_order, product=product_variant, defaults={'quantity': quantity}) order_item, created = CartItem.objects.get_or_create(cart=cart_order, product_variant=product_variant, defaults={'quantity': quantity})
if not created and order_item.quantity: if not created and order_item.quantity:
order_item.quantity = quantity order_item.quantity = quantity
order_item.save() order_item.save()
@@ -106,7 +102,7 @@ class CartItemViews(APIView):
status=status.HTTP_204_NO_CONTENT, status=status.HTTP_204_NO_CONTENT,
) )
from order.models import Cart
class CartView(APIView): class CartView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = CartSerializer serializer_class = CartSerializer
@@ -115,7 +111,7 @@ class CartView(APIView):
) )
def get(self, request): def get(self, request):
user = request.user user = request.user
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART') cart_instance, created = Cart.objects.get_or_create(user=user)
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request}) cart_ser = self.serializer_class(instance=cart_instance, context={'request': request})
return Response(cart_ser.data, status=status.HTTP_200_OK) return Response(cart_ser.data, status=status.HTTP_200_OK)
@@ -199,7 +195,8 @@ from rest_framework import serializers
class BankTypeSerializer(serializers.Serializer): class BankTypeSerializer(serializers.Serializer):
gateway_type = serializers.ChoiceField(choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1']) gateway_type = serializers.ChoiceField(choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1'])
from django.db import transaction
from django.utils import timezone
class PaymentView(APIView): class PaymentView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = BankTypeSerializer serializer_class = BankTypeSerializer
@@ -209,65 +206,136 @@ class PaymentView(APIView):
) )
def post(self, request): def post(self, request):
print(request.data.get('gateway_type')) print(request.data.get('gateway_type'))
cart_order = get_object_or_404(OrderModel, user=request.user, status='CART')
amount = 10000
user_mobile_number = request.user.phone
factory = bankfactories.BankFactory() # Get user's cart
try: cart = get_object_or_404(Cart, user=request.user)
bank = (
factory.create( # Check if cart has items
bank_models.BankType.ZIBAL if not cart.items.exists():
) return Response(
{'error': 'سبد خرید خالی است'},
status=status.HTTP_400_BAD_REQUEST
) )
bank.set_request(request)
bank.set_amount(amount)
bank.set_client_callback_url('http://localhost:3000/transaction')
bank.set_mobile_number(user_mobile_number)
bank_record = bank.ready() # Check if cart has address
# cart_order.bank_records.add(bank_record) if not cart.address:
# cart_order.save() return Response(
bank_record.order = cart_order {'error': 'آدرس انتخاب نشده است'},
bank_record.save() status=status.HTTP_400_BAD_REQUEST
return Response(bank.get_gateway()) )
# Validate product variant quantities
insufficient_stock_items = []
for cart_item in cart.items.all():
if cart_item.product_variant.in_stock < cart_item.quantity:
insufficient_stock_items.append({
'product': cart_item.product_variant.product.name,
'variant': str(cart_item.product_variant),
'requested': cart_item.quantity,
'available': cart_item.product_variant.in_stock
})
if insufficient_stock_items:
return Response({
'error': 'موجودی برخی محصولات کافی نیست',
'insufficient_items': insufficient_stock_items
}, status=status.HTTP_400_BAD_REQUEST)
# Validate discount code if present
if cart.discount_code and not cart.discount_code.is_valid():
return Response({
'error': cart.discount_code.not_valid_reason()
}, status=status.HTTP_400_BAD_REQUEST)
# Calculate order totals
cart_total = cart.cart_total
discount_amount = 0
if cart.discount_code:
discount_amount = int((cart_total * cart.discount_code.percent) / 100)
# Calculate tax (assuming 9% tax rate, adjust as needed)
tax_rate = 9 # percent
subtotal = cart_total - discount_amount
tax = int((subtotal * tax_rate) / 100)
final_price = subtotal + tax
try:
with transaction.atomic():
# Create order
order = OrderModel.objects.create(
user=request.user,
address=cart.address,
created_at=timezone.now().date(),
discount_code=cart.discount_code,
discount_amount=discount_amount,
tax=tax,
final_price=final_price,
cart_total=cart_total,
status='ADMIN_PENDING'
)
# Create order items and reduce product variant quantities
for cart_item in cart.items.all():
OrderItemModel.objects.create(
order=order,
quantity=cart_item.quantity,
price=cart_item.product_variant.price,
product=cart_item.product_variant,
discount_percent=cart_item.discount
)
# Reduce product variant quantity
cart_item.product_variant.in_stock -= cart_item.quantity
cart_item.product_variant.save()
# Reduce discount code quantity if used
if cart.discount_code:
cart.discount_code.quantity -= 1
cart.discount_code.save()
# Setup payment gateway
user_mobile_number = request.user.phone
factory = bankfactories.BankFactory()
bank = factory.create(bank_models.BankType.ZIBAL)
bank.set_request(request)
bank.set_amount(final_price) # Use final_price instead of hardcoded amount
bank.set_client_callback_url('http://localhost:3000/transaction')
bank.set_mobile_number(user_mobile_number)
bank_record = bank.ready()
# Link bank record to order (assuming you have this relationship)
bank_record.order = order
bank_record.save()
# Clear cart after successful order creation
# cart.items.all().delete()
# cart.discount_code = None
# cart.save()
return Response({
'url': bank.get_gateway()['url'],
})
except AZBankGatewaysException as e: except AZBankGatewaysException as e:
print(e) print(f"Payment gateway error: {e}")
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({
'error': 'خطا در اتصال به درگاه پرداخت'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
print(f"Order creation error: {e}")
return Response({
'error': 'خطا در ثبت سفارش'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from azbankgateways import bankfactories, models as bank_models from azbankgateways import bankfactories, models as bank_models
from django.http import Http404, HttpResponse
@csrf_exempt
@api_view(['GET'])
def callback_view(request):
tracking_code = request.GET.get('tc', None)
if not tracking_code:
logging.debug("این لینک معتبر نیست.")
raise Http404
try:
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code)
except bank_models.Bank.DoesNotExist:
logging.debug("این لینک معتبر نیست.")
raise Http404
if bank_record.is_success:
return HttpResponse("پرداخت با موفقیت انجام شد.")
return HttpResponse(
"پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت."
)
from rest_framework import serializers from rest_framework import serializers
from azbankgateways.models import Bank from azbankgateways.models import Bank
from azbankgateways.models.enum import PaymentStatus from azbankgateways.models.enum import PaymentStatus
@@ -333,13 +401,17 @@ class CallbackView(APIView):
return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK) return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
return Response( return Response(
{"detail": "پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK {
"detail": "پرداخت ناموفق بود. در صورت کسر وجه، مبلغ حداکثر تا ۴۸ ساعت آینده به حساب شما بازگردانده می‌شود.",
"bank_result": bank_record_ser.data,
},
status=status.HTTP_200_OK,
) )
class SetAddressSerilizer(serializers.Serializer): class SetAddressSerilizer(serializers.Serializer):
address_id = serializers.IntegerField() address_id = serializers.IntegerField()
from order.models import Cart
class SetAddressForCartView(APIView): class SetAddressForCartView(APIView):
serializer_class = SetAddressSerilizer serializer_class = SetAddressSerilizer
permission_classes = [IsAuthenticated, SetAddressPermissions] permission_classes = [IsAuthenticated, SetAddressPermissions]
@@ -355,9 +427,8 @@ class SetAddressForCartView(APIView):
if not permission.has_object_permission(request, self, address_object): if not permission.has_object_permission(request, self, address_object):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
cart_order, created = OrderModel.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
status='CART'
) )
cart_order.address = address_object cart_order.address = address_object
cart_order.save() cart_order.save()
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-09-21 06:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0054_productmodel_bot_banner'),
]
operations = [
migrations.AlterModelOptions(
name='productmodel',
options={'ordering': ['category', 'name'], 'verbose_name': 'محصول', 'verbose_name_plural': 'محصولات'},
),
]
+12
View File
@@ -302,6 +302,18 @@ class ProductVariant(models.Model):
def __str__(self): def __str__(self):
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}" return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
@property
def price_before_discount(self):
return self.price
@property
def price_after_discount(self):
return self.price * (self.discount / 100)
@property
def discount_amount(self):
return self.price * (self.discount / 100)
def set_or_update_price(self, dollor_price=None): def set_or_update_price(self, dollor_price=None):
if not dollor_price: if not dollor_price:
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
+2 -1
View File
@@ -15,6 +15,7 @@ from order.models import OrderModel
from django.db.models import Min, Max from django.db.models import Min, Max
from home.models import ShowCaseSlider from home.models import ShowCaseSlider
from home.serializers import ShowCaseSliderSerialzier from home.serializers import ShowCaseSliderSerialzier
from order.models import Cart, CartItem
# class APIView(APIView): # class APIView(APIView):
# def __init__(self, *args, **kwargs): # def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs) # super().__init__(*args, **kwargs)
@@ -58,7 +59,7 @@ class ProductView(APIView):
def get(self, request, slug): def get(self, request, slug):
product = get_object_or_404(ProductModel, slug=slug) product = get_object_or_404(ProductModel, slug=slug)
if request.user.is_authenticated: if request.user.is_authenticated:
cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART') cart_obj, _ = Cart.objects.get_or_create(user=request.user)
cart_items = cart_obj.items.all() cart_items = cart_obj.items.all()
cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request}) cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request})
product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data} product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data}