diff --git a/engine/core/models.py b/engine/core/models.py index e044f437..06f15426 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -469,22 +469,16 @@ class Category(NiceModel, MPTTModel): ).distinct() @cached_property - def min_price(self) -> float: - return ( - self.products.filter(is_active=True, stocks__is_active=True).aggregate( - Min("stocks__price") - )["stocks__price__min"] - or 0.0 - ) + def min_price(self) -> Decimal: + return self.products.filter(is_active=True, stocks__is_active=True).aggregate( + Min("stocks__price") + )["stocks__price__min"] or Decimal("0.00") @cached_property - def max_price(self) -> float: - return ( - self.products.filter(is_active=True, stocks__is_active=True).aggregate( - Max("stocks__price") - )["stocks__price__max"] - or 0.0 - ) + def max_price(self) -> Decimal: + return self.products.filter(is_active=True, stocks__is_active=True).aggregate( + Max("stocks__price") + )["stocks__price__max"] or Decimal("0.00") @cached_property def seo_description(self) -> str: @@ -776,7 +770,7 @@ class Product(NiceModel): ).order_by("-discount_percent") @cached_property - def discount_price(self) -> float | None: + def discount_price(self) -> Decimal | None: promo = self.promos.first() return (self.price / 100) * promo.discount_percent if promo else None @@ -795,11 +789,11 @@ class Product(NiceModel): return Feedback.objects.filter(order_product__product_id=self.pk).count() @property - def price(self: Self) -> float: + def price(self: Self) -> Decimal: stock = ( self.stocks.filter(is_active=True).only("price").order_by("-price").first() ) - return round(stock.price, 2) if stock else 0.0 + return Decimal(round(stock.price, 2)) if stock else Decimal("0.00") @cached_property def quantity(self) -> int: diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py index 8d560611..34f73440 100644 --- a/engine/core/serializers/detail.py +++ b/engine/core/serializers/detail.py @@ -3,7 +3,7 @@ from contextlib import suppress from typing import Any from drf_spectacular.utils import extend_schema_field -from rest_framework.fields import JSONField, SerializerMethodField +from rest_framework.fields import DecimalField, JSONField, SerializerMethodField from rest_framework.serializers import ListSerializer, ModelSerializer from rest_framework_recursive.fields import RecursiveField @@ -64,8 +64,12 @@ class CategoryDetailSerializer(ModelSerializer): description = SerializerMethodField() filterable_attributes = SerializerMethodField() brands = BrandSimpleSerializer(many=True, read_only=True) - min_price = SerializerMethodField() - max_price = SerializerMethodField() + min_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) + max_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Category @@ -107,12 +111,6 @@ class CategoryDetailSerializer(ModelSerializer): return list(serializer.data) return [] - def get_min_price(self, obj: Category) -> float: - return obj.min_price - - def get_max_price(self, obj: Category) -> float: - return obj.max_price - class BrandDetailSerializer(ModelSerializer): categories = CategorySimpleSerializer(many=True) @@ -281,10 +279,18 @@ class ProductDetailSerializer(ModelSerializer): description = SerializerMethodField() rating = SerializerMethodField() - price = SerializerMethodField() + price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) quantity = SerializerMethodField() feedbacks_count = SerializerMethodField() - discount_price = SerializerMethodField() + discount_price = DecimalField( + max_digits=12, + decimal_places=2, + read_only=True, + allow_null=True, + coerce_to_string=False, + ) personal_orders_only = SerializerMethodField() class Meta: @@ -319,9 +325,6 @@ class ProductDetailSerializer(ModelSerializer): def get_rating(self, obj: Product) -> float: return obj.rating - def get_price(self, obj: Product) -> float: - return obj.price - def get_feedbacks_count(self, obj: Product) -> int: return obj.feedbacks_count @@ -331,9 +334,6 @@ class ProductDetailSerializer(ModelSerializer): def get_quantity(self, obj: Product) -> int: return obj.quantity - def get_discount_price(self, obj: Product) -> float | None: - return obj.discount_price - class PromotionDetailSerializer(ModelSerializer): products = ProductDetailSerializer( @@ -416,7 +416,9 @@ class OrderDetailSerializer(ModelSerializer): order_products = OrderProductDetailSerializer( many=True, ) - total_price = SerializerMethodField(read_only=True) + total_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Order @@ -433,6 +435,3 @@ class OrderDetailSerializer(ModelSerializer): "created", "modified", ] - - def get_total_price(self, obj: Order) -> float: - return obj.total_price # ty: ignore[invalid-return-type] diff --git a/engine/core/serializers/simple.py b/engine/core/serializers/simple.py index 249d2da5..81ae61d6 100644 --- a/engine/core/serializers/simple.py +++ b/engine/core/serializers/simple.py @@ -1,6 +1,6 @@ from typing import Any -from rest_framework.fields import JSONField, SerializerMethodField +from rest_framework.fields import DecimalField, JSONField, SerializerMethodField from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.serializers import ModelSerializer @@ -140,11 +140,19 @@ class ProductSimpleSerializer(ModelSerializer): description = SerializerMethodField() rating = SerializerMethodField() - price = SerializerMethodField() + price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) quantity = SerializerMethodField() feedbacks_count = SerializerMethodField() personal_orders_only = SerializerMethodField() - discount_price = SerializerMethodField() + discount_price = DecimalField( + max_digits=12, + decimal_places=2, + read_only=True, + allow_null=True, + coerce_to_string=False, + ) class Meta: model = Product @@ -176,9 +184,6 @@ class ProductSimpleSerializer(ModelSerializer): def get_rating(self, obj: Product) -> float: return obj.rating - def get_price(self, obj: Product) -> float: - return obj.price - def get_feedbacks_count(self, obj: Product) -> int: return obj.feedbacks_count @@ -188,9 +193,6 @@ class ProductSimpleSerializer(ModelSerializer): def get_personal_orders_only(self, obj: Product) -> bool: return obj.personal_orders_only - def get_discount_price(self, obj: Product) -> float | None: - return obj.discount_price - class VendorSimpleSerializer(ModelSerializer): class Meta: @@ -286,7 +288,9 @@ class OrderSimpleSerializer(ModelSerializer): billing_address = AddressSerializer(read_only=True, required=False) shipping_address = AddressSerializer(read_only=True, required=False) attributes = JSONField(required=False) - total_price = SerializerMethodField(read_only=True) + total_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Order @@ -305,6 +309,3 @@ class OrderSimpleSerializer(ModelSerializer): "created", "modified", ] - - def get_total_price(self, obj: Order) -> float: - return obj.total_price # ty: ignore[invalid-return-type] diff --git a/engine/core/utils/seo_builders.py b/engine/core/utils/seo_builders.py index 58f59b9f..00cb960e 100644 --- a/engine/core/utils/seo_builders.py +++ b/engine/core/utils/seo_builders.py @@ -56,7 +56,7 @@ def product_schema(product, images, rating=None): offers.append( { "@type": "Offer", - "price": round(stock.price, 2), + "price": float(round(stock.price, 2)), "priceCurrency": settings.CURRENCY_CODE, "availability": "https://schema.org/InStock" if stock.quantity > 0 diff --git a/engine/core/views.py b/engine/core/views.py index 43a4b447..85782849 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -135,6 +135,24 @@ class CustomGraphQLView(FileUploadGraphQLView): def get_context(self, request): return request + @staticmethod + def json_encode(request, d, pretty=False): + from decimal import Decimal + + import orjson + + def _default(obj): + if isinstance(obj, Decimal): + return float(obj) + raise TypeError( + f"Object of type {type(obj).__name__} is not JSON serializable" + ) + + opts = orjson.OPT_NON_STR_KEYS + if pretty or request.GET.get("pretty"): + opts |= orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS + return orjson.dumps(d, default=_default, option=opts).decode("utf-8") + @extend_schema_view(**CUSTOM_OPENAPI_SCHEMA) class CustomSpectacularAPIView(SpectacularAPIView):