feat: add profit and special discount fields to ProductVariant model

- Updated ProductVariant model to include 'profit' and 'special_discount_percent' fields.
- Added corresponding fields in the admin interface for ProductVariant.
- Created migration to add new fields to the database.

feat: implement special discount code functionality in cart

- Added composables for submitting and deleting special discount codes.
- Updated CartSummary and CartItem components to handle special discount codes.
- Enhanced API endpoints to support special discount operations.
- Updated global types to include special discount code details in the cart.
This commit is contained in:
Parsa Nazer
2025-11-15 11:00:33 +03:30
parent 030976044c
commit d29ed8e35b
19 changed files with 718 additions and 208 deletions
+165 -81
View File
@@ -6,14 +6,23 @@ import requests
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from home.models import ShowCaseSlider
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
video = models.FileField(upload_to='category_videos/', blank=True, null=True, verbose_name='ویدیو')
slug = models.SlugField(max_length=50, unique=True,
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',
verbose_name='آیکون', blank=True, null=True)
image = models.ImageField(
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
video = models.FileField(upload_to='category_videos/',
blank=True, null=True, verbose_name='ویدیو')
class Meta:
verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌هااصلی"
@@ -32,13 +41,19 @@ class MainCategoryModel(models.Model):
class SubCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True)
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دسته‌بندی والد')
slug = models.SlugField(max_length=50, unique=True,
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
image = models.ImageField(
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
icon = models.ImageField(upload_to='category_model/',
verbose_name='آیکون', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE,
related_name='subcategorys', verbose_name='دسته‌بندی والد')
class Meta:
verbose_name = "زیر دسته‌بندی"
verbose_name_plural = "زیر دسته‌بندی‌ها"
@@ -49,19 +64,25 @@ class SubCategoryModel(models.Model):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class DollorModel(models.Model):
price = models.FloatField(null=True, blank=True, verbose_name='قیمت دلار')
defualt_price = models.FloatField(null=True, blank=True, default=80000.0, verbose_name='قیمت دستی')
defualt_price = models.FloatField(
null=True, blank=True, default=80000.0, verbose_name='قیمت دستی')
# these fields will avoid dublicate of this model
unique = (('unique', 'unique'),)
unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique')
unique_filed = models.CharField(
max_length=20, choices=unique, unique=True, default='unique')
def __str__(self):
return str(self.price)
def save(self, *args, **kwargs):
if not self.price:
self.update_price()
@@ -82,23 +103,25 @@ class DollorModel(models.Model):
if self.price:
print('\n\nprice from last price \n\n')
return self.price
else:
print('\n\nprice from defualt price \n\n')
return self.defualt_price
return price_in_usd
class Meta:
verbose_name = 'مدل دلار'
verbose_name_plural = 'مدل دلار'
indexes = [
models.Index(fields=['unique_filed'], name='dollor_unique_field_idx'),
models.Index(fields=['unique_filed'],
name='dollor_unique_field_idx'),
]
class InPackItems(models.Model):
item_title = models.CharField(max_length=50)
cover = models.ImageField(upload_to='product_items/', verbose_name='کاور ایتم')
cover = models.ImageField(
upload_to='product_items/', verbose_name='کاور ایتم')
class Meta:
verbose_name = 'ایتم داخل پک'
@@ -117,15 +140,24 @@ class ProductModel(models.Model):
view = models.IntegerField(default=0, verbose_name='بازدید')
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True,
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField('self', blank=True, verbose_name='محصولات مرتبط')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE, related_name='products', verbose_name='فروشگاه', blank=True, null=True)
show_in_bot = models.BooleanField(default=False, verbose_name='نمایش در ربات')
bot_banner = models.TextField(null=True, blank=True, verbose_name='بنر ربات')
meta_description = models.CharField(
max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
meta_keywords = models.CharField(max_length=300, blank=True, null=True,
help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
meta_rating = models.FloatField(
default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
created_at = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت محصول')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL,
related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField(
'self', blank=True, verbose_name='محصولات مرتبط')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE,
related_name='products', verbose_name='فروشگاه', blank=True, null=True)
show_in_bot = models.BooleanField(
default=False, verbose_name='نمایش در ربات')
bot_banner = models.TextField(
null=True, blank=True, verbose_name='بنر ربات')
def __str__(self):
return self.name
@@ -145,14 +177,13 @@ class ProductModel(models.Model):
models.Index(fields=['name'], name='product_name_idx'),
models.Index(fields=['created_at'], name='product_created_at_idx'),
models.Index(fields=['show'], name='product_show_idx'),
models.Index(fields=['category', 'created_at'], name='product_category_created_idx'),
models.Index(fields=['category', 'name'], name='product_category_name_idx'),
models.Index(fields=['category', 'created_at'],
name='product_category_created_idx'),
models.Index(fields=['category', 'name'],
name='product_category_name_idx'),
]
class ProductDetailCategory(models.Model):
title = models.CharField(max_length=40, verbose_name='عنوان')
@@ -164,38 +195,43 @@ class ProductDetailCategory(models.Model):
verbose_name_plural = 'دسته بندی های جزيیات'
indexes = [
models.Index(fields=['title'], name='detail_category_title_idx'),
]
]
class CommentModel(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
product = models.ForeignKey(
ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
title = models.CharField(max_length=50)
content = models.TextField(verbose_name='محتوای نظر')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='کاربر')
timestamp = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت کامنت')
user = models.ForeignKey(
User, on_delete=models.CASCADE, verbose_name='کاربر')
timestamp = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت کامنت')
status_types = (
('reviewed_and_confirmed', 'بررسی و تایید شده'),
('reviewed_and_rejected', 'بررسی شده و رد شده'),
('not_reviwed', 'بررسی نشده'),
)
review_status = models.CharField(default='not_reviwed', verbose_name='نشان دادن کامنت', max_length=30, choices=status_types)
review_status = models.CharField(
default='not_reviwed', verbose_name='نشان دادن کامنت', max_length=30, choices=status_types)
class Meta:
verbose_name = 'نظر'
verbose_name_plural = 'نظرات'
indexes = [
models.Index(fields=['product'], name='comment_product_idx'),
models.Index(fields=['review_status'], name='comment_review_status_idx'),
models.Index(fields=['product', 'review_status'], name='comment_product_status_idx'),
models.Index(fields=['review_status'],
name='comment_review_status_idx'),
models.Index(fields=['product', 'review_status'],
name='comment_product_status_idx'),
models.Index(fields=['user'], name='comment_user_idx'),
models.Index(fields=['timestamp'], name='comment_timestamp_idx'),
]
def __str__(self):
return f"{self.user}-{self.content[:30]}"
class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
@@ -206,9 +242,13 @@ class AttributeType(models.Model):
verbose_name = 'نوع متغییر محصول'
verbose_name_plural = 'نوع های متغییر محصول'
class AttributeValue(models.Model):
attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
attribute_type = models.ForeignKey(
AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(
verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
class Meta:
unique_together = ('attribute_type', 'value')
verbose_name = 'مقدار متغییر محصول'
@@ -217,6 +257,7 @@ class AttributeValue(models.Model):
def __str__(self):
return f"{self.attribute_type}: {self.value}"
class ProductImageModel(models.Model):
name = models.CharField(max_length=30, verbose_name='نام عکس')
image = models.ImageField(upload_to='product_images/')
@@ -230,26 +271,35 @@ class ProductImageModel(models.Model):
class ProductDetailModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
name = models.CharField(max_length=50, verbose_name='نام جزيیات',
help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
detail_category = models.ForeignKey(
ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
class Meta:
verbose_name = 'جزیات محصول'
verbose_name_plural = 'جزیات محصول ها'
indexes = [
models.Index(fields=['detail_category'], name='product_detail_category_idx'),
models.Index(fields=['detail_category'],
name='product_detail_category_idx'),
]
def __str__(self):
return f'جزيیات محصول {self.detail_category.title} - {self.name}'
class DetailModel(models.Model):
title = models.CharField(max_length=50, verbose_name='عنوان')
detail_text1 = models.CharField(max_length=150 , verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴', blank=True, null=True)
detail_model = models.ForeignKey(ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details')
detail_text1 = models.CharField(max_length=150, verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(
max_length=150, verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text3 = models.CharField(
max_length=150, verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(
max_length=150, verbose_name='متن جزیات ۴', blank=True, null=True)
detail_model = models.ForeignKey(
ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details')
def __str__(self):
return f'{self.title}'
@@ -263,46 +313,70 @@ class DetailModel(models.Model):
class ProductVariant(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant')
in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود')
price = models.PositiveIntegerField(verbose_name='قیمت محاسبه شده', blank=True, null=True)
input_price = models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی')
min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
product = models.ForeignKey(
ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
product_attributes = models.ManyToManyField(
AttributeValue, verbose_name='ویژگی‌ها', related_name='variant')
in_stock = models.PositiveIntegerField(
default=0, verbose_name='تعداد موجود')
price = models.PositiveIntegerField(
verbose_name='قیمت محاسبه شده', blank=True, null=True)
input_price = models.PositiveIntegerField(
default=0, verbose_name='قیمت ورودی')
min_price = models.PositiveIntegerField(
verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
profit = models.BigIntegerField(
default=0, verbose_name='سود (تومان)', help_text='مقدار سود به ازای هر واحد به تومان')
special_discount_percent = models.SmallIntegerField(
default=0, verbose_name='درصد تخفیف ویژه', help_text='درصدی که از سود برای محاسبه تخفیف ویژه استفاده می‌شود')
currency_type = (
('dollor', 'دلار'),
('toman', 'تومان'),
('derham', 'درهم')
)
in_pack_items = models.ManyToManyField(InPackItems, blank=True, verbose_name='ایتم های داخل پک')
in_pack_items = models.ManyToManyField(
InPackItems, blank=True, verbose_name='ایتم های داخل پک')
sell = models.IntegerField(default=0, verbose_name='فروش')
currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type)
currency = models.CharField(
verbose_name='نوع ارز', max_length=20, choices=currency_type)
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
color = models.CharField(verbose_name='رنگ', max_length=7, blank=True, null=True)
color = models.CharField(
verbose_name='رنگ', max_length=7, blank=True, null=True)
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو')
details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
slider_category = models.ForeignKey(ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول')
video = models.FileField(upload_to='product_videos/',
blank=True, null=True, verbose_name='ویدیو')
details = models.ManyToManyField(
ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
slider_category = models.ForeignKey(
ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت محصول')
class Meta:
verbose_name = 'تنوع محصول'
verbose_name_plural = 'تنوع‌های محصول'
indexes = [
models.Index(fields=['product', 'price', 'created_at'], name='idx_product_price_created'),
models.Index(fields=['in_stock', 'discount'], name='idx_stock_discount'),
models.Index(fields=['created_at'], name='idx_created'),
models.Index(fields=['product', 'price', 'created_at'],
name='idx_product_price_created'),
models.Index(fields=['in_stock', 'discount'],
name='idx_stock_discount'),
models.Index(fields=['created_at'], name='idx_created'),
models.Index(fields=['price'], name='idx_price'),
models.Index(fields=['discount'], name='product_variant_discount_idx'),
models.Index(fields=['in_stock'], name='product_variant_in_stock_idx'),
models.Index(fields=['product'], name='product_variant_product_idx'),
models.Index(fields=['product', 'in_stock'], name='variant_product_stock_idx'),
models.Index(fields=['product', 'discount'], name='variant_product_discount_idx'),
models.Index(fields=['discount'],
name='product_variant_discount_idx'),
models.Index(fields=['in_stock'],
name='product_variant_in_stock_idx'),
models.Index(fields=['product'],
name='product_variant_product_idx'),
models.Index(fields=['product', 'in_stock'],
name='variant_product_stock_idx'),
models.Index(fields=['product', 'discount'],
name='variant_product_discount_idx'),
]
def __str__(self):
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
@property
def price_before_discount(self):
return self.price
@@ -310,18 +384,28 @@ class ProductVariant(models.Model):
@property
def price_after_discount(self):
return self.price - self.discount_amount
@property
def discount_amount(self):
return self.price * (self.discount / 100)
@property
def special_discount_amount_per_unit(self):
"""Calculate special discount amount per unit as profit * special_discount_percent / 100."""
try:
return int(self.profit * (self.special_discount_percent / 100))
except Exception:
return 0
def set_or_update_price(self, dollor_price=None):
if not dollor_price:
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_object, _ = DollorModel.objects.get_or_create(
unique_filed='unique')
dollor_price = dollor_object.price
if dollor_price is None:
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
raise ValidationError(
{"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
dollar_to_dirham = 0.27