Files
hossein-por-shop/backend/order/views.py
T
Parsa Nazer e54a063d09 fix invoice
2026-05-14 17:04:00 +03:30

617 lines
24 KiB
Python

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 rest_framework import status
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, extend_schema_view
from utils.pagination import StructurePagination
from order.models import OrderModel
from django.urls import reverse
from account.models import UserAddressModel
# try:
# pass
# except DiscountNotAvailableError:
# 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]
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'))
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
cart_order.save()
return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
def delete(self, request):
cart_order, created = Cart.objects.get_or_create(
user=request.user,
)
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"]
)
def delete(self, request):
cart_order, created = Cart.objects.get_or_create(
user=request.user,
)
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
def post(self, request, pk):
product_variant = get_object_or_404(ProductVariant, pk=pk)
response = 'محصول با موفقیت به سبد خرید اضافه شد'
quantity = request.data.get('quantity', 1)
quantity = max(quantity, 0)
if product_variant.in_stock < quantity:
quantity = product_variant.in_stock
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.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):
cart_item = get_object_or_404(CartItem, pk=pk)
permission = CanDeleteCartItemPermissions()
if not permission.has_object_permission(request, self, cart_item):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
cart_item.delete()
return Response(
{"detail": "محصول با موفقیت از سبد خرید شما حذف شد"},
status=status.HTTP_204_NO_CONTENT,
)
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})
return Response(cart_ser.data, status=status.HTTP_200_OK)
class OrderlistView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderListSerializer
pagination_class = StructurePagination
@extend_schema(
parameters=[
OpenApiParameter(
name="limit",
description="لیمیتش",
required=False,
type=OpenApiTypes.INT,
),
OpenApiParameter(
name="offset",
description="افستش",
required=False,
type=OpenApiTypes.INT,
),
OpenApiParameter(
name="status",
description=(
"['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']"
),
required=False,
type=OpenApiTypes.STR,
),
OpenApiParameter(
name="sort",
description=(
"Sort results by one of the following fields:\n"
"['created_at', '-created_at', 'final_price', '-final_price']"
"\nPrefix with `-` for descending order."
),
required=False,
type=OpenApiTypes.STR,
),
],
tags=["order"]
)
def get(self, request):
user = request.user
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']:
orders.filter(status=status_filter)
if sort:
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
return Response({'detail': 'پارامتر sort اشتباه است'}, status=status.HTTP_400_BAD_REQUEST)
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})
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})
return Response(order_ser.data, status=status.HTTP_200_OK)
class BankTypeSerializer(serializers.Serializer):
gateway_type = serializers.ChoiceField(
choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1'])
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': 'سبد خرید خالی است'},
status=status.HTTP_400_BAD_REQUEST
)
# Check if cart has address
if not cart.address:
return Response(
{'error': 'آدرس انتخاب نشده است'},
status=status.HTTP_400_BAD_REQUEST
)
# Validate product variant quantities
insufficient_stock_items = []
adjusted_items = []
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,
'variant': str(cart_item.product_variant),
'requested': cart_item.quantity,
'available': available_stock
})
# Auto-adjust the cart item quantity
if available_stock > 0:
# Reduce quantity to available stock
cart_item.quantity = available_stock
cart_item.save()
adjusted_items.append({
'product': cart_item.product_variant.product.name,
'variant': str(cart_item.product_variant),
'new_quantity': available_stock
})
else:
# Remove item if no stock available
product_name = cart_item.product_variant.product.name
variant_name = str(cart_item.product_variant)
cart_item.delete()
adjusted_items.append({
'product': product_name,
'variant': variant_name,
'new_quantity': 0,
'removed': True
})
if insufficient_stock_items:
# Create error message with product names
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,
address=cart.address,
created_at=timezone.now().date(),
discount_code=cart.discount_code,
special_discount_code=cart.special_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(
order=order,
quantity=cart_item.quantity,
price=cart_item.product_variant.price,
product=cart_item.product_variant,
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)
# Use final_price instead of hardcoded amount
bank.set_amount(cart.final_price)
bank.set_client_callback_url(
'https://heymlz.com/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:
logger.error(f"Payment gateway error: {e}")
return Response({
'error': 'خطا در اتصال به درگاه پرداخت'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"Order creation error: {e}")
return Response({
'error': 'خطا در ثبت سفارش'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
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']
def get_status_detail(self, obj):
with override('fa'):
return obj.get_status_display()
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,
PaymentStatus.REDIRECT_TO_BANK,
PaymentStatus.RETURN_FROM_BANK,
}:
return "pending"
elif obj.status in {
PaymentStatus.CANCEL_BY_USER,
PaymentStatus.EXPIRE_GATEWAY_TOKEN,
PaymentStatus.EXPIRE_VERIFY_PAYMENT,
PaymentStatus.ERROR,
}:
return "canceled"
elif obj.status == PaymentStatus.COMPLETE:
return "succeeded"
return "unknown"
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)
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})
except bank_models.Bank.DoesNotExist:
return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND)
if bank_record.is_success:
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()
return Response(
{
"detail": "پرداخت ناموفق بود. در صورت کسر وجه، مبلغ حداکثر تا ۴۸ ساعت آینده به حساب شما بازگردانده می‌شود.",
"bank_result": bank_record_ser.data,
},
status=status.HTTP_200_OK,
)
class SetAddressSerilizer(serializers.Serializer):
address_id = serializers.IntegerField()
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:
return Response({'detail': 'address_id را ارسال کنید'}, status=status.HTTP_400_BAD_REQUEST)
address_object = get_object_or_404(UserAddressModel, pk=address_id)
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': 'ادرس با موفقیت انتخاب شد'})
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseNotFound
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
from .invoice_generator import generate_order_invoice, generate_shop_order_invoice
class UserOrderInvoiceView(APIView):
"""
API endpoint for authenticated users to download their own order invoice PDF.
Users can only download invoices for orders that belong to them.
"""
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["order invoice"],
description="Download PDF invoice for the authenticated user's order.",
responses={200: OpenApiTypes.BINARY},
)
def get(self, request, order_id):
from .models import OrderModel
try:
bank_detail = Bank.objects.get(tracking_code=order_id)
order = bank_detail.order
order_id = order.id
except OrderModel.DoesNotExist:
return Response(
{'detail': 'سفارش مورد نظر یافت نشد'},
status=status.HTTP_404_NOT_FOUND,
)
if order.user != request.user:
return Response(
{'detail': 'شما اجازه دسترسی به این فاکتور را ندارید'},
status=status.HTTP_403_FORBIDDEN,
)
if not order.is_paid:
return Response(
{'detail': 'فاکتور فقط برای سفارش‌های پرداخت شده قابل دانلود است'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
pdf_file = generate_order_invoice(order_id)
response = HttpResponse(pdf_file.read(), content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice_{order_id}.pdf"'
return response
except Exception as e:
return Response(
{'detail': f'خطا در ایجاد فاکتور: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@login_required
def download_order_invoice(request, order_id):
"""
Download invoice PDF for a specific OrderModel.
Requires superuser permissions.
"""
if not request.user.is_superuser:
return HttpResponseForbidden("شما اجازه دسترسی به این فاکتور را ندارید")
try:
pdf_file = generate_order_invoice(order_id)
response = HttpResponse(pdf_file.read(), content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="order_invoice_{order_id}.pdf"'
return response
except ValueError as e:
return HttpResponseNotFound(f"خطا: {str(e)}")
except Exception as e:
return HttpResponse(f"خطا در ایجاد فاکتور: {str(e)}", status=500)
@login_required
def download_shop_order_invoice(request, shop_order_id):
"""
Download invoice PDF for a specific ShopOrderModel.
Shop owners can only download their own invoices, admins can download all.
"""
from .models import ShopOrderModel
try:
shop_order = ShopOrderModel.objects.get(pk=shop_order_id)
# Check permissions
if not request.user.is_staff and not request.user.is_superuser:
if not hasattr(request.user, 'shop') or request.user.shop != shop_order.shop:
return HttpResponseForbidden("شما اجازه دسترسی به این فاکتور را ندارید")
pdf_file = generate_shop_order_invoice(shop_order_id)
response = HttpResponse(pdf_file.read(), content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="shop_order_invoice_{shop_order_id}.pdf"'
return response
except ShopOrderModel.DoesNotExist:
return HttpResponseNotFound("فاکتور مورد نظر یافت نشد")
except Exception as e:
return HttpResponse(f"خطا در ایجاد فاکتور: {str(e)}", status=500)