Refactored commerce models to support refunds, invoices, and improved carrier/payment logic. Added new serializers and viewsets for products, categories, images, discount codes, and refunds. Introduced Stripe client integration and removed legacy Stripe admin/model code. Updated Dockerfile for PDF generation dependencies. Removed obsolete migration files and updated configuration app initialization. Added invoice template and tasks for order cleanup.
383 lines
13 KiB
Python
383 lines
13 KiB
Python
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]
|
|
|
|
|
|
|
|
|