Compare commits

..

2 commits

Author SHA1 Message Date
6955e6bec6 feat(models/serializers): migrate to Decimal for monetary values
Replaced `float` with `Decimal` for monetary calculations to prevent precision loss and enhance accuracy. Updated related models, serializers, and utility functions accordingly.
2026-03-05 11:03:27 +03:00
e0e3c93e63 refactor(scripts): centralize static files collection in Docker entrypoint
Moved static files collection from platform-specific scripts to the Docker entrypoint script. Simplifies maintenance by eliminating redundant logic across Windows, Unix, and restart scripts.

Updated logo/icon settings to use `static()` for better flexibility.
2026-03-05 00:07:41 +03:00
11 changed files with 55 additions and 73 deletions

View file

@ -469,21 +469,21 @@ class Category(NiceModel, MPTTModel):
).distinct()
@cached_property
def min_price(self) -> float:
def min_price(self) -> Decimal:
return (
self.products.filter(is_active=True, stocks__is_active=True).aggregate(
Min("stocks__price")
)["stocks__price__min"]
or 0.0
or Decimal("0.00")
)
@cached_property
def max_price(self) -> float:
def max_price(self) -> Decimal:
return (
self.products.filter(is_active=True, stocks__is_active=True).aggregate(
Max("stocks__price")
)["stocks__price__max"]
or 0.0
or Decimal("0.00")
)
@cached_property
@ -776,7 +776,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 +795,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:

View file

@ -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,11 +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):
@ -281,10 +280,15 @@ 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 +323,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 +332,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 +414,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 +433,3 @@ class OrderDetailSerializer(ModelSerializer):
"created",
"modified",
]
def get_total_price(self, obj: Order) -> float:
return obj.total_price # ty: ignore[invalid-return-type]

View file

@ -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,16 @@ 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 +181,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 +190,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 +285,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 +306,3 @@ class OrderSimpleSerializer(ModelSerializer):
"created",
"modified",
]
def get_total_price(self, obj: Order) -> float:
return obj.total_price # ty: ignore[invalid-return-type]

View file

@ -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

View file

@ -135,6 +135,22 @@ 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):

View file

@ -34,8 +34,8 @@ UNFOLD: dict[str, Any] = {
"SITE_URL": STOREFRONT_DOMAIN,
"SITE_TITLE": f"{PROJECT_NAME} Dashboard",
"SITE_HEADER": PROJECT_NAME,
"SITE_LOGO": "favicon.png",
"SITE_ICON": "favicon.ico",
"SITE_LOGO": lambda request: static("favicon.png"),
"SITE_ICON": lambda request: static("favicon.ico"),
"SITE_SYMBOL": "money",
"SITE_DROPDOWN": [
{

View file

@ -2,6 +2,7 @@
set -e
uv run manage.py await_services
uv run manage.py collectstatic --noinput --verbosity 0
if [ "${DEBUG:-0}" = "1" ]; then
uv run uvicorn --host 0.0.0.0 --port 8000 --reload --log-level debug --ws-ping-interval 20 --ws-ping-timeout 20 schon.asgi:application

View file

@ -56,13 +56,6 @@ if ! output=$(docker compose exec -T app uv run manage.py search_index --rebuild
exit 1
fi
log_info " → Collecting static files..."
if ! output=$(docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1); then
log_error "Static files collection failed"
echo "$output"
exit 1
fi
log_success "Pre-run tasks completed successfully!"
# Cleanup

View file

@ -64,13 +64,6 @@ if ! output=$(docker compose exec -T app uv run manage.py search_index --rebuild
exit 1
fi
log_info " → Collecting static files..."
if ! output=$(docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1); then
log_error "Static files collection failed"
echo "$output"
exit 1
fi
log_success "Pre-run tasks completed successfully!"
# Cleanup

View file

@ -69,14 +69,6 @@ if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Write-Info " Collecting static files..."
$output = docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error-Custom "Static files collection failed"
Write-Host $output
exit $LASTEXITCODE
}
Write-Success "Pre-run tasks completed successfully!"
# Cleanup

View file

@ -84,14 +84,6 @@ if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Write-Info " Collecting static files..."
$output = docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error-Custom "Static files collection failed"
Write-Host $output
exit $LASTEXITCODE
}
Write-Success "Pre-run tasks completed successfully!"
# Cleanup