diff --git a/backend/account/apps.py b/backend/account/apps.py index bd9c0fd..f0b161e 100644 --- a/backend/account/apps.py +++ b/backend/account/apps.py @@ -4,4 +4,6 @@ from django.apps import AppConfig class AccountConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'account' - verbose_name = 'اکانت' \ No newline at end of file + verbose_name = 'اکانت' + def ready(self): + import account.signals \ No newline at end of file diff --git a/backend/account/models.py b/backend/account/models.py index da7d7cf..e0e389a 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -129,7 +129,7 @@ class UserAddressModel(models.Model): city = models.CharField(max_length=30, verbose_name='شهر') province = models.CharField(max_length=30, verbose_name='استان') for_me = models.BooleanField(default=False, verbose_name='برای خود کاربر') - is_main = models.BooleanField(default=False) + is_main = models.BooleanField(default=False, verbose_name='ادرس اصلی کاربر') def __str__(self): return f"{self.user.phone}, {self.name}" @@ -152,6 +152,10 @@ class PushSubscription(models.Model): def __str__(self): return f'{self.user} push' + class Meta: + verbose_name = 'اشتراک نوتیفیکیشن' + verbose_name_plural = 'اشتراک های نوتیفیکیشن' + def send_notif(self, title, body, icon): payload = { "title": 'فروشگاه هی ملز', diff --git a/backend/account/signals.py b/backend/account/signals.py new file mode 100644 index 0000000..d7d47d9 --- /dev/null +++ b/backend/account/signals.py @@ -0,0 +1,10 @@ +from django.db import transaction +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import UserAddressModel + +@receiver(post_save, sender=UserAddressModel) +def ensure_single_main_address(sender, instance, **kwargs): + if instance.is_main: + with transaction.atomic(): + UserAddressModel.objects.filter(user=instance.user).exclude(pk=instance.pk).update(is_main=False) \ No newline at end of file diff --git a/backend/account/urls.py b/backend/account/urls.py index 958cb0c..2d82f10 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -1,12 +1,11 @@ from django.urls import path from . import views -from djoser.urls.jwt import views as djoser_jwt_views urlpatterns = [ path('profile', views.ProfileView.as_view()), - path('verify', djoser_jwt_views.TokenVerifyView.as_view(), name='jwt-verify'), + path('verify', views.TokenVerifyView.as_view(), name='jwt-verify'), path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'), path('yee_token_bedeeee', views.KonGhoshadToken.as_view()), path('address/create', views.CreateAddressView.as_view(), name='create-address'), diff --git a/backend/account/views.py b/backend/account/views.py index 30687e8..a6c46b7 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -5,8 +5,8 @@ from rest_framework.response import Response from .serializers import * from .models import UserAddressModel, User, SecurityBreachAttemptModel from rest_framework.permissions import IsAuthenticated, AllowAny -from drf_spectacular.utils import extend_schema, OpenApiParameter -from rest_framework_simplejwt.views import TokenObtainPairView +from drf_spectacular.utils import extend_schema, OpenApiParameter, extend_schema_view +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from django.shortcuts import get_object_or_404, redirect from rest_framework_simplejwt.tokens import RefreshToken import ghasedak_sms @@ -19,7 +19,7 @@ from rest_framework_simplejwt.tokens import RefreshToken class SendOTPView(APIView): permission_classes = [AllowAny] @extend_schema( - tags=["Authentication"], + tags=["authentication"], request={ "application/json": { "type": "object", @@ -65,12 +65,18 @@ Code: {otp}""" except Exception as e: return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@extend_schema_view( + post=extend_schema(tags=['authentication']) +) +class TokenRefreshView(TokenRefreshView): + pass + class CustomTokenObtainPairView(TokenObtainPairView): serializer_class = CustomTokenObtainPairSerializer @extend_schema( - tags=["Authentication"] + tags=["authentication"] ) def post(self, request, *args, **kwargs): phone = request.data.get("phone") @@ -93,10 +99,10 @@ class CustomTokenObtainPairView(TokenObtainPairView): -class KonGhoshadToken(TokenObtainPairView): +class KonGhoshadToken(APIView): serializer_class = CustomTokenObtainPairSerializer @extend_schema( - tags=["Authentication"] + tags=["authentication"] ) def get(self, request, *args, **kwargs): random_user = User.objects.all().first() @@ -116,12 +122,16 @@ class KonGhoshadToken(TokenObtainPairView): class ProfileView(APIView): serializer_class = ProfileSerializer permission_classes = [IsAuthenticated] - + @extend_schema( + tags=["accounts profile"] + ) def get(self, request): user_ser = self.serializer_class(instance=request.user, context={'request': request}) return Response(user_ser.data, status=status.HTTP_200_OK) - + @extend_schema( + tags=["accounts profile"] + ) def patch(self, request): user = request.user user_ser = self.serializer_class(user, data=request.data, partial=True, context={'request': request}) @@ -130,6 +140,10 @@ class ProfileView(APIView): return Response(user_ser.data) return Response(user_ser.errors, status=status.HTTP_400_BAD_REQUEST) + +@extend_schema_view( + post=extend_schema(tags=["accounts address"]) +) class CreateAddressView(generics.CreateAPIView): queryset = UserAddressModel.objects.all() serializer_class = UserAddressSerializer @@ -139,7 +153,10 @@ class CreateAddressView(generics.CreateAPIView): user = self.request.user is_first_address = not UserAddressModel.objects.filter(user=user).exists() serializer.save(user=user, is_main=is_first_address) - +@extend_schema_view( + put=extend_schema(tags=["accounts address"]), + patch=extend_schema(tags=["accounts address"]) +) class EditAddressView(generics.UpdateAPIView): queryset = UserAddressModel.objects.all() serializer_class = UserAddressSerializer @@ -147,21 +164,27 @@ class EditAddressView(generics.UpdateAPIView): def get_queryset(self): return UserAddressModel.objects.filter(user=self.request.user) - +@extend_schema_view( + delete=extend_schema(tags=["accounts address"]) +) class DeleteAddressView(generics.DestroyAPIView): queryset = UserAddressModel.objects.all() permission_classes = [permissions.IsAuthenticated] def get_queryset(self): return UserAddressModel.objects.filter(user=self.request.user) - +@extend_schema_view( + get=extend_schema(tags=["accounts address"]) +) class GetUserAddressesView(generics.ListAPIView): serializer_class = UserAddressSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): return UserAddressModel.objects.filter(user=self.request.user) - +@extend_schema_view( + get=extend_schema(tags=["accounts address"]) +) class GetIDUserAddressView(generics.RetrieveAPIView): serializer_class = UserAddressSerializer permission_classes = [permissions.IsAuthenticated] @@ -174,6 +197,9 @@ class GetIDUserAddressView(generics.RetrieveAPIView): class SubscribeView(APIView): serializer_class = PushSubscriptionSerializer permission_classes = [IsAuthenticated] + @extend_schema( + tags=["accounts subscribe"] + ) def post(self, request): push_ser = self.serializer_class(data=request.data) if push_ser.is_valid(): @@ -191,6 +217,9 @@ class UnsubscribeSerializer(serializers.Serializer): class UnsubscribeView(APIView): permission_classes = [IsAuthenticated] serializer_class = UnsubscribeSerializer + @extend_schema( + tags=["accounts subscribe"] + ) def post(self, request): endpoint = request.data.get("end_point") if not endpoint: @@ -221,6 +250,7 @@ class LogoutView(APIView): @extend_schema( request=LogoutSerializer, + tags=["authentication"], responses={205: None, 400: "Bad request (invalid token or missing data)"}, ) def post(self, request): @@ -230,4 +260,12 @@ class LogoutView(APIView): token.blacklist() return Response(status=status.HTTP_205_RESET_CONTENT) except Exception as e: - return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(status=status.HTTP_400_BAD_REQUEST) + + +from djoser.urls.jwt import views as djoser_jwt_views +@extend_schema_view( + post=extend_schema(tags=["authentication"]) +) +class TokenVerifyView(djoser_jwt_views.TokenVerifyView): + pass \ No newline at end of file diff --git a/backend/azbankgateways/admin.py b/backend/azbankgateways/admin.py index 577115b..fe26c29 100644 --- a/backend/azbankgateways/admin.py +++ b/backend/azbankgateways/admin.py @@ -66,6 +66,7 @@ class BankAdmin(ModelAdmin): "extra_information", "created_at", "update_at", + 'order', ] diff --git a/backend/azbankgateways/models/banks.py b/backend/azbankgateways/models/banks.py index 4f54d13..ce9c5d6 100644 --- a/backend/azbankgateways/models/banks.py +++ b/backend/azbankgateways/models/banks.py @@ -73,7 +73,7 @@ class Bank(models.Model): max_length=255, blank=True, null=True, verbose_name=_("Bank choose identifier") ) - order = models.ForeignKey(OrderModel, on_delete=models.SET_NULL, null=True, blank=True ,related_name='bank_records') + order = models.ForeignKey(OrderModel, on_delete=models.SET_NULL, null=True, blank=True ,related_name='bank_records', verbose_name='سفارش') created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_("Created at")) update_at = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("Updated at")) diff --git a/backend/core/urls.py b/backend/core/urls.py index 3e46822..eeb96c0 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -3,9 +3,9 @@ from django.contrib import admin from django.urls import path, include from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView from django.conf import settings -from rest_framework_simplejwt.views import TokenObtainPairView,TokenRefreshView +from rest_framework_simplejwt.views import TokenObtainPairView from product import views -from account.views import CustomTokenObtainPairView +from account.views import CustomTokenObtainPairView, TokenRefreshView from home.views import HomeView from .views import FakeAdminLoginView from azbankgateways.urls import az_bank_gateways_urls diff --git a/backend/order/admin.py b/backend/order/admin.py index dedddf7..f7527bf 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -35,6 +35,7 @@ class BankRecordInline(StackedInline): model = Bank extra = 0 max_num = 0 + tab = True def has_delete_permission(self, request, obj=None): return False def get_readonly_fields(self, request, obj=None): @@ -46,11 +47,11 @@ class BankRecordInline(StackedInline): class OrderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - + search_fields = ['order_id', 'user__phone', 'user__first_name', 'user__last_name', 'user__email'] list_filter = ['is_paid', 'status'] actions_list = ['redirect_to_learn', 'udpate_bank_status'] - list_display = ['user', 'is_paid', 'status', 'discount_code', 'address',] - readonly_fields = ('created_at', ) + list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',] + readonly_fields = ('created_at', 'order_id', 'tax', 'final_price', 'cart_total', 'discount', 'discount_code', 'user', 'address', 'is_paid') compressed_fields = True warn_unsaved_form = True # exclude = ('bank_records',) diff --git a/backend/order/migrations/0024_orderitemmodel_discount_ordermodel_order_id.py b/backend/order/migrations/0024_orderitemmodel_discount_ordermodel_order_id.py new file mode 100644 index 0000000..06629d2 --- /dev/null +++ b/backend/order/migrations/0024_orderitemmodel_discount_ordermodel_order_id.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-03-27 10:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0023_remove_ordermodel_bank_records'), + ] + + operations = [ + migrations.AddField( + model_name='orderitemmodel', + name='discount', + field=models.SmallIntegerField(default=0, verbose_name='تخفیف'), + ), + migrations.AddField( + model_name='ordermodel', + name='order_id', + field=models.PositiveIntegerField(blank=True, null=True, unique=True), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index 25049ed..8d7ba6a 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -44,6 +44,7 @@ class OrderModel(models.Model): ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده'), ] + order_id = models.PositiveIntegerField(unique=True, null=True, blank=True, verbose_name='شماره سفارش') user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', 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="تاریخ ثبت سفارش") @@ -54,7 +55,6 @@ class OrderModel(models.Model): tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') - # bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True) def __str__(self): return f'سفارش: {self.id + 1000}' @@ -66,39 +66,32 @@ class OrderModel(models.Model): def save(self, *args, **kwargs): - try: - push_object = PushSubscription.objects.get(user=self.user) - except: - print('object not found') - try: - push_object.send_notif(f'سفارش شما به {self.get_status_display()} تغییر کرد', f'سفارش شما به {self.get_status_display()} تغییر کرد', ProductImageModel.objects.all().first().image.url) - except: - print('didnt send') + if not self.pk: + last_instance = self.__class__.objects.order_by("pk").last() + self.order_id = (last_instance.pk + 1001) if last_instance else 1001 super().save(*args, **kwargs) - # def discount(self): - # # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) - # # discount_percent = self.discount_code.percent - # # return total_with_item_discount * ((100 - discount_percent) / 100) - # pass + def cal_discount(self): + # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) + # discount_percent = self.discount_code.percent + # return total_with_item_discount * ((100 - discount_percent) / 100) + pass - # def tax(self): - # return self.total_without_tax() * 0.2 + def cal_tax(self): + return self.total_without_tax() * 0.2 - # def total(self): - # pass - # return self.total_with_discount() + self.tax() + def cal_total(self): + pass + return self.total_with_discount() + self.tax() - # def final_price(self): - # pass + def cal_final_price(self): + pass - # def submit_cart(self): - # pass @@ -108,15 +101,20 @@ class OrderItemModel(models.Model): quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") price = models.PositiveIntegerField(verbose_name='قیمت', default=0) product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") + discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') class Meta: verbose_name = 'ایتم سبد خرید' verbose_name_plural = 'ایتم های سبد خرید' - def total(self): - return self.quantity * self.product.price + # def total(self): + # return self.quantity * self.product.price + + # def total_with_discount(self): + # return self.quantity * self.product.get_toman_price_after_discount() + + def update_fields(self): + pass - def total_with_discount(self): - return self.quantity * self.product.get_toman_price_after_discount() def __str__(self): return f'({self.product}) - ({self.order.user})' diff --git a/backend/order/serializers.py b/backend/order/serializers.py index f25a84f..a50d23d 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -108,7 +108,6 @@ class CartSerializer(serializers.ModelSerializer): class OrderListSerializer(serializers.ModelSerializer): count = serializers.SerializerMethodField() images = serializers.SerializerMethodField() - order_id = serializers.SerializerMethodField() verbose_status = serializers.SerializerMethodField() class Meta: model = OrderModel @@ -127,8 +126,6 @@ class OrderListSerializer(serializers.ModelSerializer): for item in obj.items.all()[:3] ] return filter(lambda x: x is not None, image_list) - def get_order_id(self, obj): - return obj.id + 1000 class OrderGetSerializer(serializers.ModelSerializer): diff --git a/backend/order/urls.py b/backend/order/urls.py index 6ec9fd4..31d4db3 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -1,8 +1,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView, SetAddressForCartView -from .views import PaymentView, callback_view +from .views import * urlpatterns = [ path('all', OrderlistView.as_view(), name='order-list'), diff --git a/backend/order/views.py b/backend/order/views.py index 4dcab70..19a01ee 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -10,7 +10,7 @@ from .models import OrderItemModel, OrderModel, DiscountCode from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions from azbankgateways import bankfactories, models as bank_models from azbankgateways.exceptions import AZBankGatewaysException -from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, extend_schema_view from utils.pagination import StructurePagination from order.models import OrderModel from django.urls import reverse @@ -23,7 +23,10 @@ from account.models import UserAddressModel # pass - +@extend_schema_view( + post=extend_schema(tags=["cart discount code"]), + delete=extend_schema(tags=["cart discount code"]), +) class ApplyDiscountView(APIView): serializer_class = DiscountCodeSerializer permission_classes = [IsAuthenticated] @@ -52,6 +55,9 @@ class ApplyDiscountView(APIView): class CartItemClear(APIView): permission_classes = [IsAuthenticated] serializer_class = OrderItemSerailzier + @extend_schema( + tags=["order cart"] + ) def delete(self, request): cart_order, created = OrderModel.objects.get_or_create( user=request.user, @@ -60,7 +66,10 @@ class CartItemClear(APIView): cart_order.items.all().delete() return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) - +@extend_schema_view( + post=extend_schema(tags=["order cart"]), + delete=extend_schema(tags=["order cart"]), +) class CartItemViews(APIView): permission_classes = [IsAuthenticated] serializer_class = OrderItemSerailzier @@ -101,6 +110,9 @@ class CartItemViews(APIView): class CartView(APIView): permission_classes = [IsAuthenticated] serializer_class = CartSerializer + @extend_schema( + tags=["order cart"] + ) def get(self, request): user = request.user cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART') @@ -144,7 +156,8 @@ class OrderlistView(APIView): required=False, type=OpenApiTypes.STR, ), - ] + ], + tags=["order"] ) def get(self, request): user = request.user @@ -191,7 +204,8 @@ class PaymentView(APIView): permission_classes = [IsAuthenticated] serializer_class = BankTypeSerializer @extend_schema( - description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']" + description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']", + tags=['order payment'] ) def post(self, request): print(request.data.get('gateway_type')) @@ -261,6 +275,9 @@ class SetAddressSerilizer(serializers.Serializer): class SetAddressForCartView(APIView): serializer_class = SetAddressSerilizer permission_classes = [IsAuthenticated, SetAddressPermissions] + @extend_schema( + tags=["order cart"] + ) def post(self, request): address_id = request.data.get('address_id', None) if not address_id: diff --git a/backend/product/models.py b/backend/product/models.py index ea20a76..1a1ca6b 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -178,16 +178,22 @@ class CommentModel(models.Model): class AttributeType(models.Model): - name = models.CharField(verbose_name='نام نوع اتربیوت', max_length=100) + name = models.CharField(verbose_name='نام نوع متغییر', max_length=100) def __str__(self): return self.name + class Meta: + 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) class Meta: unique_together = ('attribute_type', 'value') + verbose_name = 'مقدار متغییر محصول' + verbose_name_plural = 'مقدار های متغییر محصول' def __str__(self): return f"{self.attribute_type}: {self.value}" diff --git a/backend/ticket/models.py b/backend/ticket/models.py index 76933b7..0941fe3 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -20,6 +20,10 @@ class Attachment(models.Model): self.name = self.file.name super(Attachment, self).save(*args, **kwargs) + class Meta: + verbose_name = 'پیوست تیکت' + verbose_name_plural = 'پیوست های تیکت' + class Ticket(models.Model): # objects = jmodels.jManager() STATUS_CHOICES = [ diff --git a/docker-compose.yml b/docker-compose.yml index 2d0c81c..756bfc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,10 @@ services: - django networks: - default + environment: + - API_BASE_URL="https://api.heymlz.com" + - DEBUG="false" + - NUXT_IMAGE_DOMAINS="https://c262408.parspack.net" django: container_name: shop_backend diff --git a/frontend/app.vue b/frontend/app.vue index d570ea3..8793e66 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -35,6 +35,6 @@ const closeModal = () => { /> - + diff --git a/frontend/assets/css/button.comp.css b/frontend/assets/css/button.comp.css index 1ab15d2..d1bff66 100644 --- a/frontend/assets/css/button.comp.css +++ b/frontend/assets/css/button.comp.css @@ -44,6 +44,31 @@ } } +@utility btn-primary { + @apply text-white bg-blue-500 border-[1.5px] border-transparent; + @apply btn-lg; + + svg[class~="iconify"] path { + @apply stroke-white; + } + + &:hover { + @apply bg-transparent border-blue-500 text-blue-500; + + svg[class~="iconify"] path { + @apply stroke-blue-500; + } + } + + &:disabled { + @apply bg-slate-100 text-slate-400; + + svg[class~="iconify"] path { + @apply stroke-slate-400; + } + } +} + @utility btn-secondary { @apply text-black bg-slate-100; @apply btn-lg; diff --git a/frontend/components/global/Brands.vue b/frontend/components/global/Brands.vue index ca209e9..d8cfe08 100644 --- a/frontend/components/global/Brands.vue +++ b/frontend/components/global/Brands.vue @@ -12,42 +12,55 @@ const {} = toRefs(props);