from django.db import transaction from rest_framework import viewsets, mixins, status from rest_framework.permissions import AllowAny, IsAdminUser, SAFE_METHODS from rest_framework.decorators import action from rest_framework.response import Response from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiExample from .models import ( Order, OrderItem, Carrier, Payment, Product, DiscountCode, Category, ProductImage, Refund, ) from .serializers import ( OrderReadSerializer, OrderMiniSerializer, OrderCreateSerializer, OrderItemReadSerializer, CarrierReadSerializer, PaymentReadSerializer, ProductSerializer, CategorySerializer, ProductImageSerializer, DiscountCodeSerializer, RefundSerializer, ) @extend_schema_view( list=extend_schema(tags=["Orders"], summary="List Orders (public)"), retrieve=extend_schema(tags=["Orders"], summary="Retrieve Order (public)"), ) class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = Order.objects.select_related("carrier", "payment").prefetch_related( "items__product", "discount" ).order_by("-created_at") permission_classes = [AllowAny] def get_serializer_class(self): if self.action == "mini": return OrderMiniSerializer if self.action in ["list", "retrieve"]: return OrderReadSerializer if self.action == "create": return OrderCreateSerializer return OrderReadSerializer @extend_schema( tags=["Orders"], summary="Create Order (public)", request=OrderCreateSerializer, responses={201: OrderReadSerializer}, examples=[ OpenApiExample( "Create order", value={ "first_name": "Jan", "last_name": "Novak", "email": "jan@example.com", "phone": "+420123456789", "address": "Ulice 1", "city": "Praha", "postal_code": "11000", "country": "Czech Republic", "note": "Prosím doručit odpoledne", "items": [ {"product_id": 1, "quantity": 2}, {"product_id": 7, "quantity": 1}, ], "carrier": {"shipping_method": "store"}, "payment": {"payment_method": "stripe"}, "discount_codes": ["WELCOME10"], }, ) ], ) def create(self, request, *args, **kwargs): serializer = OrderCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data user = request.user #VELMI DŮLEŽITELÉ: vše vytvořit v transakci, aby se nepřidávaly neúplné objednávky with transaction.atomic(): # Create base order (customer details only for now) order = Order.objects.create( user=user if getattr(user, "is_authenticated", False) else None, first_name=data["first_name"], last_name=data["last_name"], email=data["email"], phone=data.get("phone", ""), address=data["address"], city=data["city"], postal_code=data["postal_code"], country=data.get("country", "Czech Republic"), note=data.get("note", ""), ) # Carrier carrier_payload = data["carrier"] carrier = Carrier.objects.create( shipping_method=carrier_payload["shipping_method"], weight=carrier_payload.get("weight"), ) order.carrier = carrier order.save(update_fields=["carrier", "updated_at"]) # recalc later after items # Items items_payload = data["items"] order_items = [] for item in items_payload: product = Product.objects.get(pk=item["product_id"]) # raises 404 if missing qty = int(item["quantity"]) order_items.append(OrderItem(order=order, product=product, quantity=qty)) OrderItem.objects.bulk_create(order_items) # Discount codes (optional) codes = data.get("discount_codes") or [] if codes: discounts = list(DiscountCode.objects.filter(code__in=codes)) order.discount.add(*discounts) # Recalculate now that items/discounts/carrier are linked order.save() # Payment and validation pay_payload = data["payment"] payment_method = pay_payload["payment_method"] # Validate combos (mirror of Payment.save but here we have order) if payment_method == Payment.PAYMENT.SHOP and order.carrier.shipping_method != Carrier.SHIPPING.STORE: return Response( {"payment": "Platba v obchodě je možná pouze pro osobní odběr."}, status=status.HTTP_400_BAD_REQUEST, ) if payment_method == Payment.PAYMENT.CASH_ON_DELIVERY and order.carrier.shipping_method == Carrier.SHIPPING.STORE: return Response( {"payment": "Dobírka není možná pro osobní odběr."}, status=status.HTTP_400_BAD_REQUEST, ) # Create payment WITHOUT triggering Payment.save (which expects reverse link first) payment = Payment(payment_method=payment_method) # Bypass custom save by bulk_create Payment.objects.bulk_create([payment]) order.payment = payment order.save(update_fields=["payment", "updated_at"]) # If Stripe, create StripePayment now and attach if payment_method == Payment.PAYMENT.STRIPE: from thirdparty.stripe.models import StripePayment stripe_obj = StripePayment.objects.create(amount=order.total_price) payment.stripe = stripe_obj payment.save(update_fields=["stripe"]) out = self.get_serializer(order) return Response(out.data, status=status.HTTP_201_CREATED) # -- List mini orders -- (public) -- @action(detail=False, methods=["get"], url_path="detail") @extend_schema( tags=["Orders"], summary="List mini orders (public)", responses={200: OrderMiniSerializer(many=True)}, ) def mini(self, request, *args, **kwargs): qs = self.get_queryset() page = self.paginate_queryset(qs) if page is not None: ser = OrderMiniSerializer(page, many=True) return self.get_paginated_response(ser.data) ser = OrderMiniSerializer(qs, many=True) return Response(ser.data) # -- Get order items -- (public) -- @action(detail=True, methods=["get"], url_path="items") @extend_schema( tags=["Orders"], summary="List order items (public)", responses={200: OrderItemReadSerializer(many=True)}, ) def items(self, request, pk=None): order = self.get_object() qs = order.items.select_related("product").all() ser = OrderItemReadSerializer(qs, many=True) return Response(ser.data) # -- Get order carrier -- (public) -- @action(detail=True, methods=["get"], url_path="carrier") @extend_schema( tags=["Orders"], summary="Get order carrier (public)", responses={200: CarrierReadSerializer}, ) def carrier_detail(self, request, pk=None): order = self.get_object() ser = CarrierReadSerializer(order.carrier) return Response(ser.data) # -- Get order payment -- (public) -- @action(detail=True, methods=["get"], url_path="payment") @extend_schema( tags=["Orders"], summary="Get order payment (public)", responses={200: PaymentReadSerializer}, ) def payment_detail(self, request, pk=None): order = self.get_object() ser = PaymentReadSerializer(order.payment) return Response(ser.data) # -- Mark carrier ready to pickup(store) (admin) -- @action( detail=True, methods=["patch"], url_path="carrier/ready-to-pickup", permission_classes=[IsAdminUser], ) @extend_schema( tags=["Orders"], summary="Mark carrier ready to pickup (admin)", request=None, responses={200: CarrierReadSerializer}, ) def carrier_ready_to_pickup(self, request, pk=None): order = self.get_object() if not order.carrier: return Response({"detail": "Carrier not set."}, status=400) order.carrier.ready_to_pickup() order.carrier.refresh_from_db() ser = CarrierReadSerializer(order.carrier) return Response(ser.data) # -- Start ordering shipping (admin) -- @action( detail=True, methods=["patch"], url_path="carrier/start-ordering-shipping", permission_classes=[IsAdminUser], ) @extend_schema( tags=["Orders"], summary="Start ordering shipping (admin)", request=None, responses={200: CarrierReadSerializer}, ) def carrier_start_ordering_shipping(self, request, pk=None): order = self.get_object() if not order.carrier: return Response({"detail": "Carrier not set."}, status=400) order.carrier.start_ordering_shipping() order.carrier.refresh_from_db() ser = CarrierReadSerializer(order.carrier) return Response(ser.data) # -- Invoice PDF for Order -- class OrderInvoice(viewsets.ViewSet): @action(detail=True, methods=["get"], url_path="generate-invoice") @extend_schema( tags=["Orders"], summary="Get Invoice PDF for Order (public)", responses={200: "PDF File"}, ) def get(order_id, request): try: order = Order.objects.get(pk=order_id) except Order.DoesNotExist: return Response({"detail": "Order not found."}, status=status.HTTP_404_NOT_FOUND) return Response(order.invoice.pdf_file, content_type='application/pdf') # ---------- Permissions helpers ---------- class AdminWriteOnlyOrReadOnly(AllowAny.__class__): def has_permission(self, request, view): if request.method in SAFE_METHODS: return True return IsAdminUser().has_permission(request, view) class AdminOnlyForPatchOtherwisePublic(AllowAny.__class__): def has_permission(self, request, view): if request.method in SAFE_METHODS or request.method == "POST": return True if request.method == "PATCH": return IsAdminUser().has_permission(request, view) # default to admin for other unsafe return IsAdminUser().has_permission(request, view) # ---------- Public/admin viewsets ---------- # -- Product -- @extend_schema_view( list=extend_schema(tags=["Products"], summary="List products (public)"), retrieve=extend_schema(tags=["Products"], summary="Retrieve product (public)"), create=extend_schema(tags=["Products"], summary="Create product (admin)"), partial_update=extend_schema(tags=["Products"], summary="Update product (admin)"), update=extend_schema(tags=["Products"], summary="Replace product (admin)"), destroy=extend_schema(tags=["Products"], summary="Delete product (admin)"), ) class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all().order_by("-created_at") serializer_class = ProductSerializer permission_classes = [AdminWriteOnlyOrReadOnly] # -- Category -- @extend_schema_view( list=extend_schema(tags=["Categories"], summary="List categories (public)"), retrieve=extend_schema(tags=["Categories"], summary="Retrieve category (public)"), create=extend_schema(tags=["Categories"], summary="Create category (admin)"), partial_update=extend_schema(tags=["Categories"], summary="Update category (admin)"), update=extend_schema(tags=["Categories"], summary="Replace category (admin)"), destroy=extend_schema(tags=["Categories"], summary="Delete category (admin)"), ) class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all().order_by("name") serializer_class = CategorySerializer permission_classes = [AdminWriteOnlyOrReadOnly] # -- Product Image -- @extend_schema_view( list=extend_schema(tags=["Product Images"], summary="List product images (public)"), retrieve=extend_schema(tags=["Product Images"], summary="Retrieve product image (public)"), create=extend_schema(tags=["Product Images"], summary="Create product image (admin)"), partial_update=extend_schema(tags=["Product Images"], summary="Update product image (admin)"), update=extend_schema(tags=["Product Images"], summary="Replace product image (admin)"), destroy=extend_schema(tags=["Product Images"], summary="Delete product image (admin)"), ) class ProductImageViewSet(viewsets.ModelViewSet): queryset = ProductImage.objects.all().order_by("-id") serializer_class = ProductImageSerializer permission_classes = [AdminWriteOnlyOrReadOnly] # -- Discount Code -- @extend_schema_view( list=extend_schema(tags=["Discount Codes"], summary="List discount codes (public)"), retrieve=extend_schema(tags=["Discount Codes"], summary="Retrieve discount code (public)"), create=extend_schema(tags=["Discount Codes"], summary="Create discount code (admin)"), partial_update=extend_schema(tags=["Discount Codes"], summary="Update discount code (admin)"), update=extend_schema(tags=["Discount Codes"], summary="Replace discount code (admin)"), destroy=extend_schema(tags=["Discount Codes"], summary="Delete discount code (admin)"), ) class DiscountCodeViewSet(viewsets.ModelViewSet): queryset = DiscountCode.objects.all().order_by("-id") serializer_class = DiscountCodeSerializer permission_classes = [AdminWriteOnlyOrReadOnly] # -- Refund -- @extend_schema_view( list=extend_schema(tags=["Refunds"], summary="List refunds (public)"), retrieve=extend_schema(tags=["Refunds"], summary="Retrieve refund (public)"), create=extend_schema(tags=["Refunds"], summary="Create refund (public)"), partial_update=extend_schema(tags=["Refunds"], summary="Update refund (admin)"), ) class RefundViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = Refund.objects.select_related("order").all().order_by("-created_at") serializer_class = RefundSerializer permission_classes = [AdminOnlyForPatchOtherwisePublic]