diff --git a/backend/product/admin.py b/backend/product/admin.py index 0ca14b2..e795ad0 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -59,7 +59,7 @@ class AttributeTypeAdmin(ModelAdmin, ImportExportModelAdmin): class AttributeValueAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - search_fields = ['value'] + search_fields = ['value', 'attribute_type__name'] compressed_fields = True warn_unsaved_form = True @@ -82,7 +82,7 @@ class ProductVariantInLine(StackedInline): extra = 0 show_change_link = True tab = True - + min_num = 1 autocomplete_fields = ['attributes'] # search_fields = [''] @@ -112,11 +112,11 @@ class DetailModelAdmin(ModelAdmin, ImportExportModelAdmin): -class DetailModelInLine(StackedInline): +class DetailModelInLine(TabularInline): model = ProductDetailModel extra = 0 + fields = ['detail', 'detail_category'] show_change_link = True - tab = True autocomplete_fields = ['detail', 'detail_category'] @@ -128,15 +128,15 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): inlines = [ProductVariantInLine, DetailModelInLine] readonly_fields = ('slug', ) search_fields = ['name', 'description', ] - list_filter = ['currency', 'show', 'category'] + list_filter = ['show', 'category'] autocomplete_fields = ['related_products', 'in_pack_items',] # compressed_fields = True warn_unsaved_form = True - list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', 'discount', 'sell'] + list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', ] fieldsets = ( - ('فیلد های اصلی', {'fields': ('name', 'description', 'price', 'min_price', 'currency', 'discount', 'category', 'related_products', 'show',), "classes": ["tab"],}), + ('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'related_products', 'show',), "classes": ["tab"],}), ('فیلد های سيو', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}), - ('فیلد های مربوط به کاربر', {'fields': ('rating', 'view', 'sell', ), "classes": ["tab"],}), + ('فیلد های مربوط به کاربر', {'fields': ('rating', 'view',), "classes": ["tab"],}), ('فیلد های ایتم های پک', {'fields': ('in_pack_items', ), "classes": ["tab"],}) ) @@ -150,6 +150,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): } def display_price(self, obj): + return 1000 return obj.get_toman_price() display_price.short_description = 'قیمت تومانی' @@ -221,9 +222,9 @@ class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): class CommentAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - list_display = ['user', 'product', 'display_content','show'] + list_display = ['user', 'product', 'display_content','review_status'] search_fields = ['content',] - list_filter = ['show',] + list_filter = ['review_status',] compressed_fields = True warn_unsaved_form = True @@ -232,6 +233,7 @@ class CommentAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } + radio_fields = {'review_status': admin.VERTICAL} def display_content(self, obj): return obj.content[0:35] + '...' display_content.short_description = 'محتوای کامنت' diff --git a/backend/product/migrations/0016_remove_productmodel_currency_and_more.py b/backend/product/migrations/0016_remove_productmodel_currency_and_more.py new file mode 100644 index 0000000..5d08f99 --- /dev/null +++ b/backend/product/migrations/0016_remove_productmodel_currency_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.1.2 on 2025-02-06 19:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0015_attributetype_productdetailcategory_attributevalue_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='productmodel', + name='currency', + ), + migrations.RemoveField( + model_name='productmodel', + name='discount', + ), + migrations.RemoveField( + model_name='productmodel', + name='min_price', + ), + migrations.RemoveField( + model_name='productmodel', + name='price', + ), + migrations.RemoveField( + model_name='productmodel', + name='sell', + ), + migrations.AddField( + model_name='productvariant', + name='currency', + field=models.CharField(choices=[('dollor', 'دلار'), ('toman', 'تومان'), ('derham', 'درهم')], default='dollor', max_length=20, verbose_name='نوع ارز'), + preserve_default=False, + ), + migrations.AddField( + model_name='productvariant', + name='discount', + field=models.SmallIntegerField(default=0, verbose_name='تخفیف'), + ), + migrations.AddField( + model_name='productvariant', + name='min_price', + field=models.PositiveIntegerField(default=1, help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود', verbose_name='قیمت کف'), + preserve_default=False, + ), + migrations.AddField( + model_name='productvariant', + name='sell', + field=models.IntegerField(default=0, verbose_name='فروش'), + ), + migrations.AlterField( + model_name='attributevalue', + name='attribute_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='product.attributetype'), + ), + migrations.AlterField( + model_name='attributevalue', + name='value', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='مقدار نوع اتربیوت'), + ), + migrations.AlterField( + model_name='productvariant', + name='price', + field=models.PositiveIntegerField(default=0, verbose_name='قیمت'), + ), + ] diff --git a/backend/product/migrations/0017_remove_commentmodel_show_commentmodel_review_status_and_more.py b/backend/product/migrations/0017_remove_commentmodel_show_commentmodel_review_status_and_more.py new file mode 100644 index 0000000..a70e1df --- /dev/null +++ b/backend/product/migrations/0017_remove_commentmodel_show_commentmodel_review_status_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.2 on 2025-02-07 16:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0016_remove_productmodel_currency_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='commentmodel', + name='show', + ), + migrations.AddField( + model_name='commentmodel', + name='review_status', + field=models.CharField(default='not_reviwed', max_length=30, verbose_name='نشان دادن کامنت'), + ), + migrations.AddField( + model_name='commentmodel', + name='title', + field=models.CharField(default='', max_length=50), + preserve_default=False, + ), + ] diff --git a/backend/product/migrations/0018_alter_commentmodel_review_status.py b/backend/product/migrations/0018_alter_commentmodel_review_status.py new file mode 100644 index 0000000..3dfa93c --- /dev/null +++ b/backend/product/migrations/0018_alter_commentmodel_review_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-08 13:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0017_remove_commentmodel_show_commentmodel_review_status_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='commentmodel', + name='review_status', + field=models.CharField(choices=[('reviewed_and_confirmed', 'بررسی و تایید شده'), ('reviewed_and_rejected', 'بررسی شده و رد شده'), ('not_reviwed', 'بررسی نشده')], default='not_reviwed', max_length=30, verbose_name='نشان دادن کامنت'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 8c583ab..12b3db9 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -100,14 +100,6 @@ class InPackItems(models.Model): class ProductModel(models.Model): name = models.CharField(max_length=255, verbose_name='نام') description = models.TextField(verbose_name='توضیحات') - price = models.PositiveIntegerField(default=0, verbose_name='قیمت') - min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود') - currency_type = ( - ('dollor', 'دلار'), - ('toman', 'تومان'), - ('derham', 'درهم') - ) - currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) image1 = models.ImageField(upload_to='product_images/', verbose_name='عکس اول') image2 = models.ImageField(upload_to='product_images/', blank=True, null=True, verbose_name='عکس دوم') image3 = models.ImageField(upload_to='product_images/', blank=True, null=True, verbose_name='عکس سوم') @@ -115,8 +107,6 @@ class ProductModel(models.Model): rating = models.PositiveIntegerField(default=0, verbose_name='امتیاز') show = models.BooleanField(default=False, verbose_name='نمایش در خانه') view = models.IntegerField(default=0, verbose_name='بازدید') - sell = models.IntegerField(default=0, verbose_name='فروش') - discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') slug = models.SlugField(max_length=50, 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='متا دیسکریپشن') @@ -127,35 +117,11 @@ class ProductModel(models.Model): related_products = models.ManyToManyField('self', blank=True, verbose_name='محصولات مرتبط') in_pack_items = models.ManyToManyField(InPackItems, blank=True, verbose_name='ایتم های داخل پک') - def format_discount_price(self): - discount_price = int(self.price * (100 - self.discount) / 100) - formatted_num = "{:,.0f}".format(discount_price) - return formatted_num def __str__(self): return self.name - def get_toman_price(self, dollor_price=None): - if not dollor_price: - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - dollor_price = dollor_object.price - - dollar_to_dirham = 0.27 - if dollor_price is None: - raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) - if self.currency == 'toman': - toman_price = self.price - elif self.currency == 'dollor': - toman_price = self.price * dollor_price - elif self.currency == 'derham': - toman_price = self.price * dollor_price * dollar_to_dirham - toman_price = toman_price if toman_price > self.min_price else self.min_price - return toman_price - - def get_toman_price_after_discount(self): - return self.get_toman_price() * ((100 - self.discount) / 100) - def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) @@ -209,10 +175,16 @@ class ProductDetailModel(models.Model): class CommentModel(models.Model): 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='زمان ثبت کامنت') - show = models.BooleanField(default=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) class Meta: verbose_name = 'نظر' verbose_name_plural = 'نظرات' @@ -228,8 +200,8 @@ class AttributeType(models.Model): return self.name class AttributeValue(models.Model): - attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE) - value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100) + 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) color = models.CharField(verbose_name='رنک', max_length=7, blank=True, null=True) class Meta: @@ -242,11 +214,39 @@ class ProductVariant(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها') in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود') - price = models.PositiveIntegerField(null=True, blank=True, verbose_name='قیمت (اختیاری)') - + price = models.PositiveIntegerField(default=0, verbose_name='قیمت') + min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود') + currency_type = ( + ('dollor', 'دلار'), + ('toman', 'تومان'), + ('derham', 'درهم') + ) + sell = models.IntegerField(default=0, verbose_name='فروش') + currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) + discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') class Meta: verbose_name = 'تنوع محصول' verbose_name_plural = 'تنوع‌های محصول' def __str__(self): - return f"{self.product.name} - {', '.join(str(attr) for attr in self.attributes.all())}" \ No newline at end of file + return f"{self.product.name} - {', '.join(str(attr) for attr in self.attributes.all())}" + + def get_toman_price(self, dollor_price=None): + if not dollor_price: + dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') + dollor_price = dollor_object.price + + dollar_to_dirham = 0.27 + if dollor_price is None: + raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) + if self.currency == 'toman': + toman_price = self.price + elif self.currency == 'dollor': + toman_price = self.price * dollor_price + elif self.currency == 'derham': + toman_price = self.price * dollor_price * dollar_to_dirham + toman_price = toman_price if toman_price > self.min_price else self.min_price + return toman_price + + def get_toman_price_after_discount(self): + return self.get_toman_price() * ((100 - self.discount) / 100) \ No newline at end of file diff --git a/backend/product/serializers.py b/backend/product/serializers.py index e275e04..f94fcdb 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -19,10 +19,32 @@ class ProductDetailSerializer(serializers.ModelSerializer): exclude = ('product',) +class AttributeTypeSerialzier(serializers.ModelSerializer): + class Meta: + model = AttributeType + fields = "__all__" + +class AttributeValueSerialzier(serializers.ModelSerializer): + attribute_type = AttributeTypeSerialzier() + class Meta: + model = AttributeValue + fields = "__all__" + +class ProductVariantSerialzier(serializers.ModelSerializer): + attributes = AttributeValueSerialzier(many=True) + price = serializers.SerializerMethodField() + class Meta: + model = ProductVariant + fields = "__all__" + + def get_price(self, obj): + dollor_price = self.context.get('dollor_price') + toman_price = obj.get_toman_price(dollor_price=dollor_price) + return "{:,.0f} تومان".format(toman_price) + class DynamicProductSerializer(serializers.ModelSerializer): - - price = serializers.SerializerMethodField() + variants = ProductVariantSerialzier(many=True) is_new = serializers.SerializerMethodField() related_products = serializers.SerializerMethodField() details = ProductDetailSerializer(many=True, read_only=True) @@ -42,15 +64,11 @@ class DynamicProductSerializer(serializers.ModelSerializer): model = ProductModel fields = "__all__" view_type = { - 'list': ['name', 'price', 'image1', 'video', 'rating', 'discount', 'slug', 'category', ], - 'instance': ['name', 'description', 'price', 'image1', 'image2', 'image3', 'video', 'rating', 'discount', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'details', 'in_pack_items'], - 'chat': ['name', 'description', 'price', 'in_stock', 'discount', ] + 'list': ['name', 'image1', 'video', 'rating', 'slug', 'category', 'variants'], + 'instance': ['name', 'description', 'image1', 'image2', 'image3', 'video', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'details', 'in_pack_items', 'variants'], + 'chat': ['name', 'description', ] } - def get_price(self, obj): - dollor_price = self.context.get('dollor_price') - toman_price = obj.get_toman_price(dollor_price=dollor_price) - return "{:,.0f} تومان".format(toman_price) def get_is_new(self, obj): return timezone.now() < obj.created_at + timedelta(days=7) @@ -74,8 +92,8 @@ class DynamicProductSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer): class Meta: model = CommentModel - fields = "__all__" - read_only_fields = ('show', 'product', 'user') + exclude = ('review_status', ) + read_only_fields = ('review_status', 'product', 'user') class SubCategorySerializer(serializers.ModelSerializer): product_count = serializers.SerializerMethodField() diff --git a/backend/product/views.py b/backend/product/views.py index e06c716..b49da78 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -224,7 +224,7 @@ class CommentView(APIView): ) def get(self, request, pk): product = get_object_or_404(ProductModel, id=pk) - comments = product.comments.filter(show=True) + comments = product.comments.filter(review_status__in=['not_reviwed', 'reviewed_and_confirmed']) paginator = self.pagination_class() paginated_comments = paginator.paginate_queryset(comments, request) comments_ser = self.serializer_class(instance=paginated_comments, many=True) diff --git a/backend/utils/admin.py b/backend/utils/admin.py index dda3dfd..61cabce 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -12,7 +12,7 @@ def dollor_price(request): return str(dollor_object.price)[:2] def comment_count(request): - return CommentModel.objects.all().count() + return CommentModel.objects.filter(review_status='not_reviwed').count() def new_ticket_count(request): return Ticket.objects.filter(status__in=['open', 'in_progress']).count() \ No newline at end of file