diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..30cf57e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..430eb9f
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..452c296
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vontor-cz.iml b/.idea/vontor-cz.iml
new file mode 100644
index 0000000..cc7388e
--- /dev/null
+++ b/.idea/vontor-cz.iml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/account/views.py b/backend/account/views.py
index 16a0fef..b28cbfe 100644
--- a/backend/account/views.py
+++ b/backend/account/views.py
@@ -250,10 +250,19 @@ class UserView(viewsets.ModelViewSet):
# Fallback - deny access (prevents AttributeError for AnonymousUser)
return [OnlyRolesAllowed("admin")()]
- # Any authenticated user can retrieve (view) any user's profile
- #FIXME: popřemýšlet co vše může získat
+ # Users can only view their own profile, admins can view any profile
elif self.action == 'retrieve':
- return [IsAuthenticated()]
+ user = getattr(self, 'request', None) and getattr(self.request, 'user', None)
+ # Admins can view any user profile
+ if user and getattr(user, 'is_authenticated', False) and getattr(user, 'role', None) == 'admin':
+ return [IsAuthenticated()]
+
+ # Users can view their own profile
+ if user and getattr(user, 'is_authenticated', False) and self.kwargs.get('pk') and str(getattr(user, 'id', '')) == self.kwargs['pk']:
+ return [IsAuthenticated()]
+
+ # Deny access to other users' profiles
+ return [OnlyRolesAllowed("admin")()]
return super().get_permissions()
diff --git a/backend/advertisement/tasks.py b/backend/advertisement/tasks.py
index a62019d..d6b7845 100644
--- a/backend/advertisement/tasks.py
+++ b/backend/advertisement/tasks.py
@@ -38,12 +38,15 @@ def send_newly_added_items_to_store_email_task_last_week():
created_at__gte=last_week_date
)
+ config = SiteConfiguration.get_solo()
+
send_email_with_context(
- recipients=SiteConfiguration.get_solo().contact_email,
+ recipients=config.contact_email,
subject="Nový produkt přidán do obchodu",
template_path="email/advertisement/commerce/new_items_added_this_week.html",
context={
"products_of_week": products_of_week,
+ "site_currency": config.currency,
}
)
diff --git a/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html b/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html
index 98eea1a..a959536 100644
--- a/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html
+++ b/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html
@@ -60,7 +60,7 @@
{% if product.price %}
- {{ product.price|floatformat:0 }} {{ product.currency|default:"Kč" }}
+ {{ product.price|floatformat:0 }} {{ site_currency|default:"€" }}
{% endif %}
diff --git a/backend/commerce/currency_info_view.py b/backend/commerce/currency_info_view.py
new file mode 100644
index 0000000..576312e
--- /dev/null
+++ b/backend/commerce/currency_info_view.py
@@ -0,0 +1,45 @@
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from drf_spectacular.utils import extend_schema
+from configuration.models import SiteConfiguration
+
+class CurrencyInfoView(APIView):
+ """
+ Get current site currency and display information.
+ """
+
+ @extend_schema(
+ summary="Get site currency information",
+ description="Returns the current site currency and available options",
+ tags=["configuration"]
+ )
+ def get(self, request):
+ config = SiteConfiguration.get_solo()
+
+ currency_symbols = {
+ 'EUR': '€',
+ 'CZK': 'Kč',
+ 'USD': '$',
+ 'GBP': '£',
+ 'PLN': 'zł',
+ 'HUF': 'Ft',
+ 'SEK': 'kr',
+ 'DKK': 'kr',
+ 'NOK': 'kr',
+ 'CHF': 'Fr'
+ }
+
+ return Response({
+ 'current_currency': config.currency,
+ 'currency_symbol': currency_symbols.get(config.currency, config.currency),
+ 'currency_name': dict(SiteConfiguration.CURRENCY.choices)[config.currency],
+ 'available_currencies': [
+ {
+ 'code': choice[0],
+ 'name': choice[1],
+ 'symbol': currency_symbols.get(choice[0], choice[0])
+ }
+ for choice in SiteConfiguration.CURRENCY.choices
+ ]
+ })
\ No newline at end of file
diff --git a/backend/commerce/management/__init__.py b/backend/commerce/management/__init__.py
new file mode 100644
index 0000000..0cf6859
--- /dev/null
+++ b/backend/commerce/management/__init__.py
@@ -0,0 +1 @@
+# Management commands module
\ No newline at end of file
diff --git a/backend/commerce/management/commands/__init__.py b/backend/commerce/management/commands/__init__.py
new file mode 100644
index 0000000..fc58c38
--- /dev/null
+++ b/backend/commerce/management/commands/__init__.py
@@ -0,0 +1 @@
+# Commerce management commands
\ No newline at end of file
diff --git a/backend/commerce/management/commands/migrate_to_global_currency.py b/backend/commerce/management/commands/migrate_to_global_currency.py
new file mode 100644
index 0000000..8f2c5fe
--- /dev/null
+++ b/backend/commerce/management/commands/migrate_to_global_currency.py
@@ -0,0 +1,74 @@
+"""
+Management command to migrate from per-product currency to global currency system.
+Usage: python manage.py migrate_to_global_currency
+"""
+from django.core.management.base import BaseCommand
+from commerce.models import Product, Order
+from configuration.models import SiteConfiguration
+
+
+class Command(BaseCommand):
+ help = 'Migrate from per-product currency to global currency system'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--target-currency',
+ type=str,
+ default='EUR',
+ help='Target currency to migrate to (default: EUR)'
+ )
+ parser.add_argument(
+ '--dry-run',
+ action='store_true',
+ help='Show what would be changed without making changes'
+ )
+
+ def handle(self, *args, **options):
+ target_currency = options['target_currency']
+ dry_run = options['dry_run']
+
+ self.stdout.write(
+ self.style.SUCCESS(f"Migrating to global currency: {target_currency}")
+ )
+
+ # Check current state
+ config = SiteConfiguration.get_solo()
+ self.stdout.write(f"Current site currency: {config.currency}")
+
+ if hasattr(Product.objects.first(), 'currency'):
+ # Products still have currency field
+ product_currencies = Product.objects.values_list('currency', flat=True).distinct()
+ self.stdout.write(f"Product currencies found: {list(product_currencies)}")
+
+ if len(product_currencies) > 1:
+ self.stdout.write(
+ self.style.WARNING(
+ "Multiple currencies detected in products. "
+ "Consider currency conversion before migration."
+ )
+ )
+
+ order_currencies = Order.objects.values_list('currency', flat=True).distinct()
+ order_currencies = [c for c in order_currencies if c] # Remove empty strings
+ self.stdout.write(f"Order currencies found: {list(order_currencies)}")
+
+ if not dry_run:
+ # Update site configuration
+ config.currency = target_currency
+ config.save()
+ self.stdout.write(
+ self.style.SUCCESS(f"Updated site currency to {target_currency}")
+ )
+
+ # Update orders with empty currency
+ orders_updated = Order.objects.filter(currency='').update(currency=target_currency)
+ self.stdout.write(
+ self.style.SUCCESS(f"Updated {orders_updated} orders to use {target_currency}")
+ )
+
+ else:
+ self.stdout.write(self.style.WARNING("DRY RUN - No changes made"))
+
+ self.stdout.write(
+ self.style.SUCCESS("Migration completed successfully!")
+ )
\ No newline at end of file
diff --git a/backend/commerce/migrations/0003_remove_product_currency.py b/backend/commerce/migrations/0003_remove_product_currency.py
new file mode 100644
index 0000000..3e47ff7
--- /dev/null
+++ b/backend/commerce/migrations/0003_remove_product_currency.py
@@ -0,0 +1,36 @@
+# Generated migration to remove Product.currency field and use global currency system
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('commerce', '0002_alter_productimage_options_carrier_deutschepost_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='product',
+ name='currency',
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='currency',
+ field=models.CharField(
+ default='',
+ help_text='Order currency - auto-filled from site configuration',
+ max_length=10
+ ),
+ ),
+ migrations.AlterField(
+ model_name='discountcode',
+ name='amount',
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ help_text='Fixed discount amount in site currency',
+ max_digits=10,
+ null=True
+ ),
+ ),
+ ]
\ No newline at end of file
diff --git a/backend/commerce/models.py b/backend/commerce/models.py
index 4fa9302..07e7e51 100644
--- a/backend/commerce/models.py
+++ b/backend/commerce/models.py
@@ -71,7 +71,7 @@ class Product(models.Model):
# -- CENA --
price = models.DecimalField(max_digits=10, decimal_places=2, help_text="Net price (without VAT)")
- currency = models.CharField(max_length=3, default="CZK")
+ # Currency is now global from SiteConfiguration, not per-product
# VAT rate - configured by business owner in configuration app!!!
vat_rate = models.ForeignKey(
@@ -127,7 +127,8 @@ class Product(models.Model):
return self.price * vat_rate.rate_decimal
def __str__(self):
- return f"{self.name} ({self.get_price_with_vat()} {self.currency.upper()} inkl. MwSt)"
+ config = SiteConfiguration.get_solo()
+ return f"{self.name} ({self.get_price_with_vat()} {config.currency} inkl. MwSt)"
#obrázek pro produkty
class ProductImage(models.Model):
@@ -164,7 +165,8 @@ class Order(models.Model):
# Stored order grand total; recalculated on save
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
- currency = models.CharField(max_length=10, default="CZK")
+# Currency - captured from site configuration at creation time, never changes
+ currency = models.CharField(max_length=10, default="", help_text="Order currency - captured from site configuration at order creation and never changes")
# fakturační údaje (zkopírované z user profilu při objednávce)
user = models.ForeignKey(
@@ -257,13 +259,25 @@ class Order(models.Model):
# Validate order has items
if self.pk and not self.items.exists():
raise ValidationError("Order must have at least one item.")
+
+ def get_currency(self):
+ \"\"\"Get order currency - falls back to site configuration if not set\"\"\"
+ if self.currency:
+ return self.currency
+ config = SiteConfiguration.get_solo()
+ return config.currency
def save(self, *args, **kwargs):
- # Keep total_price always in sync with items and discount
- self.total_price = self.calculate_total_price()
-
- is_new = self.pk is None
-
+ is_new = self.pk is None
+
+ # CRITICAL: Set currency from site configuration ONLY at creation time
+ # Once set, currency should NEVER change to maintain order integrity
+ if is_new and not self.currency:
+ config = SiteConfiguration.get_solo()
+ self.currency = config.currency
+
+ # Keep total_price always in sync with items and discount
+ self.total_price = self.calculate_total_price()
if self.user and is_new:
self.import_data_from_user()
@@ -274,6 +288,15 @@ class Order(models.Model):
from .tasks import notify_order_successfuly_created
notify_order_successfuly_created.delay(order=self, user=self.user)
+ def cancel_order(self):
+ """Cancel the order if possible"""
+ if self.status == self.OrderStatus.CREATED:
+ self.status = self.OrderStatus.CANCELLED
+ self.save()
+ #TODO: udělat ještě kontrolu jestli už nebyla odeslána zásilka a pokud bude už zaplacena tak se uděla refundace a pokud nebude zaplacena tak se zruší brána.
+ else:
+ raise ValidationError("Only orders in 'created' status can be cancelled.")
+
# ------------------ DOPRAVCI A ZPŮSOBY DOPRAVY ------------------
@@ -480,7 +503,7 @@ class DiscountCode(models.Model):
)
# nebo fixní částka
- amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Fixní sleva v CZK")
+ amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text=\"Fixed discount amount in site currency\")
valid_from = models.DateTimeField(default=timezone.now)
valid_to = models.DateTimeField(null=True, blank=True)
diff --git a/backend/commerce/urls.py b/backend/commerce/urls.py
index 4c8d90f..a306808 100644
--- a/backend/commerce/urls.py
+++ b/backend/commerce/urls.py
@@ -15,6 +15,7 @@ from .views import (
AdminWishlistViewSet,
AnalyticsView,
)
+from .currency_info_view import CurrencyInfoView
router = DefaultRouter()
router.register(r'orders', OrderViewSet)
@@ -33,4 +34,5 @@ urlpatterns = [
path('refunds/public/', RefundPublicView.as_view(), name='RefundPublicView'),
path('reviews/create/', ReviewPostPublicView.as_view(), name='ReviewCreate'),
path('analytics/', AnalyticsView.as_view(), name='analytics'),
+ path('currency/info/', CurrencyInfoView.as_view(), name='currency-info'),
]
diff --git a/backend/commerce/views.py b/backend/commerce/views.py
index cf9ab0c..462dca4 100644
--- a/backend/commerce/views.py
+++ b/backend/commerce/views.py
@@ -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[^/.]+)')
@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:
diff --git a/backend/configuration/models.py b/backend/configuration/models.py
index 6abf4bf..a21d89b 100644
--- a/backend/configuration/models.py
+++ b/backend/configuration/models.py
@@ -44,9 +44,17 @@ class SiteConfiguration(models.Model):
addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón")
class CURRENCY(models.TextChoices):
- CZK = "CZK", "Czech Koruna"
EUR = "EUR", "Euro"
- currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices)
+ CZK = "CZK", "Czech Koruna"
+ USD = "USD", "US Dollar"
+ GBP = "GBP", "British Pound"
+ PLN = "PLN", "Polish Zloty"
+ HUF = "HUF", "Hungarian Forint"
+ SEK = "SEK", "Swedish Krona"
+ DKK = "DKK", "Danish Krone"
+ NOK = "NOK", "Norwegian Krone"
+ CHF = "CHF", "Swiss Franc"
+ currency = models.CharField(max_length=10, default=CURRENCY.EUR, choices=CURRENCY.choices)
class Meta:
verbose_name = "Shop Configuration"
diff --git a/backend/thirdparty/stripe/client.py b/backend/thirdparty/stripe/client.py
index ce5ad4e..fe59348 100644
--- a/backend/thirdparty/stripe/client.py
+++ b/backend/thirdparty/stripe/client.py
@@ -20,7 +20,11 @@ class StripeClient:
Returns:
stripe.checkout.Session: Vytvořená Stripe Checkout Session.
"""
-
+ from configuration.models import SiteConfiguration
+
+ # Use order currency or fall back to site configuration
+ currency = order.get_currency().lower()
+
session = stripe.checkout.Session.create(
mode="payment",
payment_method_types=["card"],
@@ -31,11 +35,11 @@ class StripeClient:
client_reference_id=str(order.id),
line_items=[{
"price_data": {
- "currency": "czk",
+ "currency": currency,
"product_data": {
- "name": f"Objednávka {order.id}",
+ "name": f"Order #{order.id}",
},
- "unit_amount": int(order.total_price * 100), # cena v haléřích
+ "unit_amount": int(order.total_price * 100), # amount in smallest currency unit
},
"quantity": 1,
}],