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
+4
View File
@@ -15,7 +15,11 @@ from django.template.loader import render_to_string
from folium import Map, Marker
from unfold.decorators import action, display
from django.utils.html import format_html
from account.models import SpecialDiscountCode
@admin.register(SpecialDiscountCode)
class SpecialDiscountCodeAdmin(ModelAdmin):
pass
class UserAddressInLine(TabularInline):
model = UserAddressModel
+17
View File
@@ -61,6 +61,15 @@ class User(AbstractBaseUser, PermissionsMixin):
# def groups(self):
# return None
def generate_special_code(self):
"""Generate and save a unique special code for the user if missing."""
# simple deterministic code based on phone and timestamp hash
base = f"{self.phone}-{timezone.now().timestamp()}"
code = hashlib.sha256(base.encode()).hexdigest()[:12].upper()
return code
@property
def full_name(self):
if self.first_name and self.last_name:
@@ -120,6 +129,14 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.phone
class SpecialDiscountCode(models.Model):
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name='spital_code')
code = models.CharField(max_length=12, unique=True)
def __str__(self):
return f'{self.user} - {self.code}'
class ShopModel(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='shop', verbose_name='کاربر')
+1 -1
View File
@@ -80,7 +80,7 @@ class SendOTPView(APIView):
except User.DoesNotExist:
return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_200_OK)
# return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@extend_schema_view(
post=extend_schema(tags=['authentication'])
@@ -0,0 +1,35 @@
# Generated by Django 5.1.2 on 2025-11-14 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0032_specialdiscountcode'),
('order', '0037_ordermodel_is_stock_rolled_back'),
]
operations = [
migrations.AddField(
model_name='cart',
name='special_discount_code',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='account.specialdiscountcode', verbose_name='کدتخفیف خاص'),
),
migrations.AddField(
model_name='cartitem',
name='special_discount_amount',
field=models.BigIntegerField(default=0, help_text='تخفیف محاسبه شده از سود تنوع', verbose_name='مقدار تخفیف ویژه'),
),
migrations.AddField(
model_name='orderitemmodel',
name='special_discount_amount',
field=models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه'),
),
migrations.AddField(
model_name='ordermodel',
name='special_discount_total',
field=models.BigIntegerField(blank=True, null=True, verbose_name='مجموع تخفیف ویژه'),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-11-14 15:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0038_cart_special_discount_code_and_more'),
]
operations = [
migrations.RemoveField(
model_name='cartitem',
name='special_discount_amount',
),
]
+70 -37
View File
@@ -1,3 +1,4 @@
from account.models import SpecialDiscountCode
from django.db import models, transaction
from account.models import User, UserAddressModel, PushSubscription
from product.models import ProductModel, ProductVariant, ProductImageModel
@@ -47,6 +48,8 @@ class Cart(models.Model):
related_name='carts', null=True, verbose_name='ادرس')
discount_code = models.ForeignKey(
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
special_discount_code = models.ForeignKey(
SpecialDiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف خاص")
class Meta:
verbose_name = 'سبد خرید'
@@ -58,6 +61,7 @@ class Cart(models.Model):
def clear_cart(self):
self.items.all().delete()
self.discount_code = None
self.special_discount_code = None
self.save()
@property
@@ -71,10 +75,17 @@ class Cart(models.Model):
def items_discount_amount(self):
return int(sum(item.item_discount_amount for item in self.items.all()))
@property
def special_discount_total(self):
"""Sum of all special discounts from cart items when special_discount_code is applied."""
if self.special_discount_code:
return int(sum(item.item_special_discount_amount for item in self.items.all()))
return 0
@property
def total_before_tax(self):
return self.cart_total - (self.discount_code_amount + self.items_discount_amount)
return self.cart_total - (self.discount_code_amount + self.items_discount_amount + self.special_discount_total)
@property
def tax_amount(self):
return int(self.total_before_tax * settings.DEFAULT_TAX_RATE / 100)
@@ -102,6 +113,7 @@ class CartItem(models.Model):
quantity = models.PositiveIntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# special_discount_amount = models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه', help_text='تخفیف محاسبه شده از سود تنوع')
class Meta:
verbose_name = 'ایتم سبد خرید'
@@ -111,22 +123,37 @@ class CartItem(models.Model):
def __str__(self):
return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}"
@property
def special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@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 item_special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@property
def price_after_discount(self):
return self.price_before_discount - self.item_discount_amount
return self.price_before_discount - self.item_discount_amount - self.item_special_discount_amount
@property
def discount(self):
return self.product_variant.discount
class OrderModel(models.Model):
objects = jmodels.jManager()
STATUS_CHOICES = [
@@ -155,9 +182,12 @@ class OrderModel(models.Model):
null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(
null=True, blank=True, verbose_name='کل سبد خرید')
special_discount_total = models.BigIntegerField(
null=True, blank=True, verbose_name='مجموع تخفیف ویژه')
cart = models.ForeignKey(
Cart, on_delete=models.CASCADE, null=True, blank=True)
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, null=True, blank=True)
is_stock_rolled_back = models.BooleanField(
default=False, verbose_name="موجودی برگردانده شده")
@@ -169,36 +199,37 @@ class OrderModel(models.Model):
verbose_name_plural = 'سفارشات'
def rollback_stock(self):
"""
Rollback stock quantities for all items in this order
Returns True if successful, False otherwise
"""
if self.is_stock_rolled_back:
return False
# if not self.cart:
# return False
try:
# Get all cart items and rollback their stock
for order_item in self.items.all():
product = order_item.product
# Add back the quantity to stock
product.in_stock += order_item.quantity
product.save()
# Mark as rolled back
self.is_stock_rolled_back = True
self.save(update_fields=['is_stock_rolled_back'])
self.status = 'CANCELED'
self.save()
return True
except Exception as e:
print(e)
# Log the error if you have logging setup
# logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
return False
"""
Rollback stock quantities for all items in this order
Returns True if successful, False otherwise
"""
if self.is_stock_rolled_back:
return False
# if not self.cart:
# return False
try:
# Get all cart items and rollback their stock
for order_item in self.items.all():
product = order_item.product
# Add back the quantity to stock
product.in_stock += order_item.quantity
product.save()
# Mark as rolled back
self.is_stock_rolled_back = True
self.save(update_fields=['is_stock_rolled_back'])
self.status = 'CANCELED'
self.save()
return True
except Exception as e:
print(e)
# Log the error if you have logging setup
# logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
return False
class OrderItemModel(models.Model):
order = models.ForeignKey(
@@ -208,6 +239,8 @@ class OrderItemModel(models.Model):
product = models.ForeignKey(
ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
special_discount_amount = models.BigIntegerField(
default=0, verbose_name='مقدار تخفیف ویژه')
class Meta:
verbose_name = 'ایتم سبد خرید'
+57 -13
View File
@@ -1,9 +1,11 @@
from django.conf import settings
from rest_framework import serializers
from .models import OrderItemModel, OrderModel, DiscountCode, Cart, CartItem
from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer
from account.serializers import UserAddressSerializer
from product.models import ProductVariant
class ProductVariantSerialzier(serializers.ModelSerializer):
product_attributes = AttributeValueSerialzier(many=True)
image = serializers.SerializerMethodField()
@@ -13,12 +15,14 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField()
category = serializers.SerializerMethodField()
slug = serializers.CharField(source='product.slug')
class Meta:
model = ProductVariant
fields = ['id', 'slug', 'title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
fields = ['id', 'slug', 'title', 'product_attributes', 'in_stock', 'price',
'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
def get_discount_amount(self, obj):
discount_amount = int(obj.price * (obj.discount / 100))
discount_amount = int(obj.price * (obj.discount / 100))
return f'{discount_amount:,.0f} تومان'
def get_final_price(self, obj):
@@ -51,10 +55,13 @@ class OrderItemSerailzier(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
discount = serializers.SerializerMethodField()
special_discount_amount = serializers.SerializerMethodField()
class Meta:
model = CartItem
exclude = ('cart',)
read_only_fields = ('cart', 'product', 'discount_percent')
def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product_variant, context={'request': self.context.get('request')}).data
@@ -70,7 +77,20 @@ class OrderItemSerailzier(serializers.ModelSerializer):
def get_discount(self, obj):
return obj.product_variant.discount
from django.conf import settings
def get_special_discount_amount(self, obj):
# For cart items
print('in here asdfasfd')
amount = getattr(obj, 'special_discount_amount', None)
print(amount)
if amount is None:
print('in here')
# If it's an order item, check item_special_discount_amount property
amount = getattr(obj, 'item_special_discount_amount', 0)
if amount is None:
amount = 0
print('in here ')
return f'{int(amount):,.0f} تومان'
class CartSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True)
@@ -79,11 +99,23 @@ class CartSerializer(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField()
items_discount_amount = serializers.SerializerMethodField()
special_discount_total = serializers.SerializerMethodField()
special_discount_code = serializers.SerializerMethodField()
address = UserAddressSerializer()
class Meta:
model = Cart
fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount', 'final_price', 'address']
fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount',
'final_price', 'address', 'special_discount_total', 'special_discount_code']
def get_special_discount_code(self, obj):
if obj.special_discount_code:
return {
'code': f'{obj.special_discount_code.code}',
'user': f'{obj.special_discount_code.user.phone}'
}
else:
return None
def get_discount_code(self, obj):
if obj.discount_code:
@@ -95,7 +127,6 @@ class CartSerializer(serializers.ModelSerializer):
else:
return None
def get_tax_amount(self, obj):
return f'{obj.tax_amount:,.0f} تومان'
@@ -107,17 +138,26 @@ class CartSerializer(serializers.ModelSerializer):
def get_final_price(self, obj):
return f'{obj.final_price:,.0f} تومان'
def get_special_discount_total(self, obj):
# sum of special discounts on cart items
total = obj.special_discount_total if hasattr(
obj, 'special_discount_total') else 0
return f'{int(total):,.0f} تومان'
class OrderListSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField()
class Meta:
model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status']
fields = ['created_at', 'status', "images", "count",
"id", 'final_price', 'order_id', 'verbose_status']
read_only_fields = ['count', 'images', 'order_id', 'verbose_status']
def get_verbose_status(self, obj):
return obj.get_status_display()
@@ -126,6 +166,7 @@ class OrderListSerializer(serializers.ModelSerializer):
def get_order_id(self, obj):
return obj.pk + 1000
def get_images(self, obj):
image_list = [
self.context.get('request').build_absolute_uri(image.image.url)
@@ -136,16 +177,18 @@ class OrderListSerializer(serializers.ModelSerializer):
class OrderGetSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField()
items = OrderItemSerailzier(many=True)
address = UserAddressSerializer()
discount_code = DiscountCodeSerializer()
class Meta:
model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount_amount']
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status',
'address', 'items', 'tax', 'cart_total', 'discount_code', 'discount_amount', 'special_discount_total']
def get_verbose_status(self, obj):
return obj.get_status_display()
@@ -160,5 +203,6 @@ class OrderGetSerializer(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
return obj.id + 1000
+3 -1
View File
@@ -7,7 +7,7 @@ from azbankgateways import (
from .models import OrderModel
from account.models import PushSubscription
import ghasedak_sms
from product.models import ProductImageModel
from celery import shared_task
@shared_task
@@ -24,6 +24,8 @@ def udpate_bank_status():
bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
if bank_record.is_success:
bank_record.order.cart.clear_cart()
bank_record.order.is_paid = True
bank_record.order.save()
logging.debug("This record is verify now.", extra={"pk": bank_record.pk})
else:
order = bank_record.order
+3 -1
View File
@@ -8,9 +8,11 @@ urlpatterns = [
path('cart', CartView.as_view()),
path('cart/set-address', SetAddressForCartView.as_view()),
path('cart/discount', ApplyDiscountView.as_view()),
path('cart/special-discount', ApplySpecialDiscountView.as_view()),
path('cart/all', CartItemClear.as_view()),
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
path('cart/payment', PaymentView.as_view(), name='payment'),
path('transaction/<int:tracking_code>', CallbackView.as_view(), name='callback-gateway'),
path('transaction/<int:tracking_code>',
CallbackView.as_view(), name='callback-gateway'),
path('<int:pk>', OrderGetView.as_view(), name='order-get'),
]
+126 -68
View File
@@ -1,10 +1,19 @@
from django.db import transaction
from order.models import Cart
from django.utils.translation import override
from .permissons import PaymentCallBackPermissions
from azbankgateways.models.enum import PaymentStatus
from azbankgateways.models import Bank
from rest_framework import serializers
from rest_framework.response import Response
from django.utils import timezone
from django.shortcuts import render
from rest_framework.views import APIView, Response
from django.shortcuts import get_object_or_404
from product.models import ProductVariant
from rest_framework.permissions import IsAuthenticated
from .serializers import *
# from cart.models import
# from cart.models import
from rest_framework import status
from .models import OrderItemModel, OrderModel, DiscountCode
from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions
@@ -30,12 +39,14 @@ from account.models import UserAddressModel
class ApplyDiscountView(APIView):
serializer_class = DiscountCodeSerializer
permission_classes = [IsAuthenticated]
def post(self, request):
cart_order, created = Cart.objects.get_or_create(
user=request.user,
)
discount_code = get_object_or_404(DiscountCode, code=request.data.get('code'))
discount_code = get_object_or_404(
DiscountCode, code=request.data.get('code'))
if not discount_code.is_valid():
return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST)
cart_order.discount_code = discount_code
@@ -46,13 +57,49 @@ class ApplyDiscountView(APIView):
cart_order, created = Cart.objects.get_or_create(
user=request.user,
)
cart_order.discount_code = None
cart_order.discount_code = None
cart_order.save()
return Response({'detail': 'کد تخفیف با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
@extend_schema_view(
post=extend_schema(tags=["cart special discount code"]),
delete=extend_schema(tags=["cart special discount code"]),
)
class ApplySpecialDiscountView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
from account.models import SpecialDiscountCode
cart, created = Cart.objects.get_or_create(user=request.user)
code = request.data.get('code')
if not code:
return Response({'detail': 'کد تخفیف ویژه را وارد کنید'}, status=status.HTTP_400_BAD_REQUEST)
try:
special_discount_code = SpecialDiscountCode.objects.get(code=code)
except SpecialDiscountCode.DoesNotExist:
return Response({'detail': 'کد تخفیف ویژه معتبر نیست'}, status=status.HTTP_404_NOT_FOUND)
# Apply the special discount code to cart
cart.special_discount_code = special_discount_code
cart.save()
return Response({'detail': 'کد تخفیف ویژه با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
def delete(self, request):
cart, created = Cart.objects.get_or_create(user=request.user)
cart.special_discount_code = None
cart.save()
return Response({'detail': 'کد تخفیف ویژه با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
class CartItemClear(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier
@extend_schema(
tags=["order cart"]
)
@@ -63,6 +110,7 @@ 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"]),
@@ -70,6 +118,7 @@ class CartItemClear(APIView):
class CartItemViews(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier
def post(self, request, pk):
product_variant = get_object_or_404(ProductVariant, pk=pk)
response = 'محصول با موفقیت به سبد خرید اضافه شد'
@@ -80,15 +129,15 @@ class CartItemViews(APIView):
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
cart_order, created = Cart.objects.get_or_create(user=request.user)
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:
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:
order_item.quantity = quantity
order_item.save()
if not order_item.quantity:
order_item.delete()
return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED)
def delete(self, request, pk):
order_item = get_object_or_404(OrderItemModel, pk=pk)
permission = CanDeleteCartItemPermissions()
@@ -102,17 +151,19 @@ class CartItemViews(APIView):
status=status.HTTP_204_NO_CONTENT,
)
from order.models import Cart
class CartView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = CartSerializer
@extend_schema(
tags=["order cart"]
)
def get(self, request):
user = request.user
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)
@@ -120,6 +171,7 @@ class OrderlistView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderListSerializer
pagination_class = StructurePagination
@extend_schema(
parameters=[
OpenApiParameter(
@@ -160,7 +212,7 @@ class OrderlistView(APIView):
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
status_filter = request.query_params.get("status", None)
sort = request.query_params.get('sort', None)
if status_filter in [ 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
if status_filter in ['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
orders.filter(status=status_filter)
if sort:
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
@@ -168,62 +220,59 @@ class OrderlistView(APIView):
orders = orders.order_by(sort)
paginator = self.pagination_class()
paginated_orders = paginator.paginate_queryset(orders, request)
orders_ser = self.serializer_class(instance=paginated_orders, many=True, context={'request': request})
orders_ser = self.serializer_class(
instance=paginated_orders, many=True, context={'request': request})
return paginator.get_paginated_response(orders_ser.data)
class OrderGetView(APIView):
permission_classes = [IsAuthenticated, GetOrderPermission]
serializer_class = OrderGetSerializer
def get(self, request, pk):
order_object = get_object_or_404(OrderModel, pk=pk)
permission = GetOrderPermission()
if not permission.has_object_permission(request, self, order_object):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
order_ser = self.serializer_class(order_object, context={'request': request})
order_ser = self.serializer_class(
order_object, context={'request': request})
return Response(order_ser.data, status=status.HTTP_200_OK)
from rest_framework import serializers
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):
permission_classes = [IsAuthenticated]
serializer_class = BankTypeSerializer
@extend_schema(
description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']",
tags=['order payment']
)
def post(self, request):
# Get user's cart
cart = get_object_or_404(Cart, user=request.user)
# Check if cart has items
if not cart.items.exists():
return Response(
{'error': 'سبد خرید خالی است'},
{'error': 'سبد خرید خالی است'},
status=status.HTTP_400_BAD_REQUEST
)
# Check if cart has address
if not cart.address:
return Response(
{'error': 'آدرس انتخاب نشده است'},
{'error': 'آدرس انتخاب نشده است'},
status=status.HTTP_400_BAD_REQUEST
)
# Validate product variant quantities
insufficient_stock_items = []
adjusted_items = []
@@ -231,7 +280,7 @@ class PaymentView(APIView):
for cart_item in cart.items.all():
if cart_item.product_variant.in_stock < cart_item.quantity:
available_stock = cart_item.product_variant.in_stock
# Store info about insufficient stock
insufficient_stock_items.append({
'product': cart_item.product_variant.product.name,
@@ -239,7 +288,7 @@ class PaymentView(APIView):
'requested': cart_item.quantity,
'available': available_stock
})
# Auto-adjust the cart item quantity
if available_stock > 0:
# Reduce quantity to available stock
@@ -264,20 +313,25 @@ class PaymentView(APIView):
if insufficient_stock_items:
# Create error message with product names
product_names = [item['product'] for item in insufficient_stock_items]
product_names = [item['product']
for item in insufficient_stock_items]
product_list = '، '.join(product_names)
return Response({
'detail': f'موجودی محصولات زیر کافی نیست: {product_list}',
'message': 'تعداد محصولات به صورت خودکار تنظیم شد',
'insufficient_items': insufficient_stock_items,
'adjusted_items': adjusted_items
}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# Compute special discounts for cart items if special_discount_code is applied
special_total = 0
for cart_item in cart.items.select_related('product_variant').all():
if cart.special_discount_code:
special_total += cart_item.special_discount_amount
# Create order
order = OrderModel.objects.create(
user=request.user,
@@ -285,13 +339,14 @@ class PaymentView(APIView):
created_at=timezone.now().date(),
discount_code=cart.discount_code,
discount_amount=cart.discount_code_amount,
special_discount_total=special_total,
tax=cart.tax_amount,
final_price=cart.final_price,
cart_total=cart.cart_total,
status='ADMIN_PENDING',
cart=cart
)
# Create order items and reduce product variant quantities
for cart_item in cart.items.all():
OrderItemModel.objects.create(
@@ -299,71 +354,64 @@ class PaymentView(APIView):
quantity=cart_item.quantity,
price=cart_item.product_variant.price,
product=cart_item.product_variant,
discount_percent=cart_item.discount
discount_percent=cart_item.discount,
special_discount_amount=cart_item.special_discount_amount
)
# 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(cart.final_price) # Use final_price instead of hardcoded amount
bank.set_client_callback_url('http://localhost:3000/transaction')
# Use final_price instead of hardcoded amount
bank.set_amount(cart.final_price)
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()
return Response({
'url': bank.get_gateway()['url'],
})
except AZBankGatewaysException as e:
print(f"Payment gateway error: {e}")
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 rest_framework.response import Response
from azbankgateways import bankfactories, models as bank_models
from rest_framework import serializers
from azbankgateways.models import Bank
from azbankgateways.models.enum import PaymentStatus
from .permissons import PaymentCallBackPermissions
from django.utils.translation import override
class BankCallbackSerializer(serializers.ModelSerializer):
status_detail = serializers.SerializerMethodField()
bank_type = serializers.SerializerMethodField()
amount = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
class Meta:
model = Bank
fields = ['status', 'bank_type', 'tracking_code', 'amount', 'created_at', 'response_result', 'reference_number', 'status_detail']
fields = ['status', 'bank_type', 'tracking_code', 'amount',
'created_at', 'response_result', 'reference_number', 'status_detail']
def get_status_detail(self, obj):
with override('fa'):
return obj.get_status_display()
@@ -371,8 +419,10 @@ class BankCallbackSerializer(serializers.ModelSerializer):
def get_bank_type(self, obj):
with override('fa'):
return obj.get_bank_type_display()
def get_amount(self, obj):
return f'{int(obj.amount):,.0f} تومان'
def get_status(self, obj):
if obj.status in {
PaymentStatus.WAITING,
@@ -395,25 +445,31 @@ class BankCallbackSerializer(serializers.ModelSerializer):
class CallbackView(APIView):
serializer_class = BankCallbackSerializer
permission_classes = [IsAuthenticated]
def get(self, request, tracking_code):
if not tracking_code:
return Response({'detail': 'تریسکد خالی است.'}, status=status.HTTP_400_BAD_REQUEST)
try:
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code)
bank_record = bank_models.Bank.objects.get(
tracking_code=tracking_code)
permission = PaymentCallBackPermissions()
if not permission.has_object_permission(request, self, bank_record):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
bank_record_ser = self.serializer_class(instance=bank_record, context={'request': request})
bank_record_ser = self.serializer_class(
instance=bank_record, context={'request': request})
except bank_models.Bank.DoesNotExist:
return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND)
if bank_record.is_success:
bank_record.order.cart.clear_cart()
return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
order = bank_record.order
order.cart.clear_cart()
order.is_paid = True
order.save()
return Response({"detail": "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
else:
order = bank_record.order
order.rollback_stock()
@@ -428,10 +484,12 @@ class CallbackView(APIView):
class SetAddressSerilizer(serializers.Serializer):
address_id = serializers.IntegerField()
from order.models import Cart
class SetAddressForCartView(APIView):
serializer_class = SetAddressSerilizer
permission_classes = [IsAuthenticated, SetAddressPermissions]
@extend_schema(
tags=["order cart"]
)
@@ -443,10 +501,10 @@ class SetAddressForCartView(APIView):
permission = SetAddressPermissions()
if not permission.has_object_permission(request, self, address_object):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
cart_order, created = Cart.objects.get_or_create(
user=request.user,
)
cart_order.address = address_object
cart_order.save()
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
+1 -1
View File
@@ -185,7 +185,7 @@ class ProductVariantInLine(StackedInline):
readonly_fields = ['price']
# inlines = [DetailModelInLine]
autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details']
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category']
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category', 'profit', 'special_discount_percent']
# search_fields = ['']
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-11-14 14:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0056_productmodel_image'),
]
operations = [
migrations.AddField(
model_name='productvariant',
name='profit',
field=models.BigIntegerField(default=0, help_text='مقدار سود به ازای هر واحد به تومان', verbose_name='سود (تومان)'),
),
migrations.AddField(
model_name='productvariant',
name='special_discount_percent',
field=models.SmallIntegerField(default=0, help_text='درصدی که از سود برای محاسبه تخفیف ویژه استفاده می\u200cشود', verbose_name='درصد تخفیف ویژه'),
),
]
+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