update cart logic
This commit is contained in:
@@ -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
@@ -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("ایتم ها فقط در حالت سبد خرید قابل ادیت هستند")
|
|
||||||
@@ -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
@@ -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': 'محصولات'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user