Migrate to global currency system in commerce app

Removed per-product currency in favor of a global site currency managed via SiteConfiguration. Updated models, views, templates, and Stripe integration to use the global currency. Added migration, management command for migration, and API endpoint for currency info. Improved permissions and filtering for orders, reviews, and carts. Expanded supported currencies in configuration.
This commit is contained in:
2026-01-24 21:51:56 +01:00
parent 8f6d864b4b
commit 775709bd08
19 changed files with 371 additions and 102 deletions

View File

@@ -15,7 +15,7 @@ from .analytics import (
)
from django.db import transaction
from django.db import transaction, models
from rest_framework import viewsets, mixins, status
from rest_framework.permissions import AllowAny, IsAdminUser, SAFE_METHODS
from rest_framework.decorators import action
@@ -63,14 +63,39 @@ from .serializers import (
#FIXME: uravit view na nový order serializer
@extend_schema_view(
list=extend_schema(tags=["commerce", "public"], summary="List Orders (public)"),
retrieve=extend_schema(tags=["commerce", "public"], summary="Retrieve Order (public)"),
list=extend_schema(tags=["commerce", "public"], summary="List Orders (public - anonymous orders, authenticated - user orders, admin - all orders)"),
retrieve=extend_schema(tags=["commerce", "public"], summary="Retrieve Order (public - anonymous orders, authenticated - user orders, admin - all orders)"),
create=extend_schema(tags=["commerce", "public"], summary="Create Order (public)"),
)
class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
queryset = Order.objects.select_related("carrier", "payment").prefetch_related(
"items__product", "discount"
).order_by("-created_at")
permission_classes = [AllowAny]
permission_classes = [permissions.AllowAny]
def get_permissions(self):
"""Allow public order access (for anonymous orders) and creation, require auth for user orders"""
if self.action in ['create', 'list', 'retrieve', 'mini', 'items', 'carrier_detail', 'payment_detail', 'verify_payment', 'download_invoice']:
return [permissions.AllowAny()]
return super().get_permissions()
def get_queryset(self):
"""Filter orders by user - admins see all, authenticated users see own orders, anonymous users see anonymous orders only"""
queryset = super().get_queryset()
# Admin users can see all orders
if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin':
return queryset
# Authenticated users see only their own orders (both user-linked and email-matched anonymous orders)
if self.request.user.is_authenticated:
return queryset.filter(
models.Q(user=self.request.user) |
models.Q(user__isnull=True, email=self.request.user.email)
)
# Anonymous users can only see anonymous orders (no user linked)
return queryset.filter(user__isnull=True)
def get_serializer_class(self):
if self.action == "mini":
@@ -81,51 +106,17 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
return OrderCreateSerializer
return OrderReadSerializer
@extend_schema(
tags=["commerce", "public"],
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, context={"request": request})
serializer.is_valid(raise_exception=True)
order = serializer.save()
out = OrderReadSerializer(order)
return Response(out.data, status=status.HTTP_201_CREATED)
# Order creation is now handled by CreateModelMixin with proper serializer
# -- List mini orders -- (public) --
@action(detail=False, methods=["get"], url_path="detail")
@extend_schema(
tags=["commerce", "public"],
summary="List mini orders (public)",
summary="List mini orders (public - anonymous orders, authenticated - user orders)",
responses={200: OrderMiniSerializer(many=True)},
)
def mini(self, request, *args, **kwargs):
qs = self.get_queryset()
qs = self.get_queryset() # Already filtered by user/anonymous status
page = self.paginate_queryset(qs)
if page is not None:
@@ -139,11 +130,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
@action(detail=True, methods=["get"], url_path="items")
@extend_schema(
tags=["commerce", "public"],
summary="List order items (public)",
summary="List order items (public - anonymous orders, authenticated - user orders)",
responses={200: OrderItemReadSerializer(many=True)},
)
def items(self, request, pk=None):
order = self.get_object()
order = self.get_object() # get_object respects get_queryset filtering
qs = order.items.select_related("product").all()
ser = OrderItemReadSerializer(qs, many=True)
return Response(ser.data)
@@ -152,11 +143,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
@action(detail=True, methods=["get"], url_path="carrier")
@extend_schema(
tags=["commerce", "public"],
summary="Get order carrier (public)",
summary="Get order carrier (public - anonymous orders, authenticated - user orders)",
responses={200: CarrierReadSerializer},
)
def carrier_detail(self, request, pk=None):
order = self.get_object()
order = self.get_object() # get_object respects get_queryset filtering
ser = CarrierReadSerializer(order.carrier)
return Response(ser.data)
@@ -164,11 +155,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
@action(detail=True, methods=["get"], url_path="payment")
@extend_schema(
tags=["commerce", "public"],
summary="Get order payment (public)",
summary="Get order payment (public - anonymous orders, authenticated - user orders)",
responses={200: PaymentReadSerializer},
)
def payment_detail(self, request, pk=None):
order = self.get_object()
order = self.get_object() # get_object respects get_queryset filtering
ser = PaymentReadSerializer(order.payment)
return Response(ser.data)
@@ -216,21 +207,6 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
ser = CarrierReadSerializer(order.carrier)
return Response(ser.data)
# -- Get user's own orders (authenticated) --
@action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated])
@extend_schema(
tags=["commerce"],
summary="Get authenticated user's orders",
responses={200: OrderMiniSerializer(many=True)},
)
def my_orders(self, request):
orders = Order.objects.filter(user=request.user).order_by('-created_at')
page = self.paginate_queryset(orders)
if page:
serializer = OrderMiniSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = OrderMiniSerializer(orders, many=True)
return Response(serializer.data)
# -- Cancel order (authenticated) --
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
@@ -240,11 +216,7 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
responses={200: {"type": "object", "properties": {"status": {"type": "string"}}}},
)
def cancel(self, request, pk=None):
order = self.get_object()
# Check if user owns order
if order.user != request.user:
return Response({'detail': 'Not authorized'}, status=403)
order = self.get_object() # get_object respects get_queryset filtering
# Can only cancel if not shipped
if order.carrier and order.carrier.state != Carrier.STATE.PREPARING:
@@ -303,20 +275,7 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
filename=f'invoice_{order.invoice.invoice_number}.pdf'
)
def retrieve(self, request, *args, **kwargs):
"""Override retrieve to filter by user if authenticated and not admin"""
order = self.get_object()
# If user is authenticated and not admin, check if they own the order
if request.user.is_authenticated and not request.user.is_staff:
if order.user and order.user != request.user:
return Response({'detail': 'Not found.'}, status=404)
# Also check by email for anonymous orders
elif not order.user and order.email != request.user.email:
return Response({'detail': 'Not found.'}, status=404)
serializer = self.get_serializer(order)
return Response(serializer.data)
# retrieve method removed - get_queryset handles filtering automatically
# ---------- Public/admin viewsets ----------
@@ -577,6 +536,23 @@ class ReviewPublicViewSet(viewsets.ModelViewSet):
ordering_fields = ["rating", "created_at"]
ordering = ["-created_at"]
def get_queryset(self):
"""Filter reviews - admins see all, users see all reviews but can only modify their own"""
queryset = super().get_queryset()
# Admin users can see and modify all reviews
if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin':
return queryset
# For modification actions, users can only modify their own reviews
if self.action in ['update', 'partial_update', 'destroy']:
if self.request.user.is_authenticated:
return queryset.filter(user=self.request.user)
return queryset.none()
# For viewing, everyone can see all reviews (they're public)
return queryset
@action(detail=False, methods=['get'], url_path='product/(?P<product_id>[^/.]+)')
@extend_schema(
tags=["commerce", "public"],
@@ -608,6 +584,24 @@ class CartViewSet(viewsets.GenericViewSet):
serializer_class = CartSerializer
permission_classes = [AllowAny]
def get_queryset(self):
"""Filter carts by user/session - users only see their own cart"""
queryset = super().get_queryset()
# Admin users can see all carts
if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin':
return queryset
# Authenticated users see only their cart
if self.request.user.is_authenticated:
return queryset.filter(user=self.request.user)
# Anonymous users see only their session cart
session_key = self.request.session.session_key
if session_key:
return queryset.filter(session_key=session_key, user__isnull=True)
return queryset.none()
def get_or_create_cart(self, request):
"""Get or create cart for current user/session"""
if request.user.is_authenticated: