feat(commerce): ensure precise decimal handling in aggregated calculations

replace float-based defaults with `Decimal` in Coalesce and set `DecimalField` as the output type to improve precision when aggregating monetary values.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-03-03 13:49:29 +03:00
parent e5553ac6dd
commit aabd8b18b4

View file

@ -1,9 +1,10 @@
from contextlib import suppress from contextlib import suppress
from datetime import date, timedelta from datetime import date, timedelta
from decimal import Decimal
from typing import Any from typing import Any
from constance import config from constance import config
from django.db.models import Count, F, QuerySet, Sum from django.db.models import Count, DecimalField, F, QuerySet, Sum
from django.db.models.functions import Coalesce, TruncDate from django.db.models.functions import Coalesce, TruncDate
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
@ -28,7 +29,11 @@ def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> f
order_products = get_period_order_products(period) order_products = get_period_order_products(period)
total: float = ( total: float = (
order_products.aggregate( order_products.aggregate(
total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0) total=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
)
).get("total") ).get("total")
or 0.0 or 0.0
) )
@ -78,7 +83,13 @@ def get_returns(period: timedelta = timedelta(days=30)) -> float:
order__buy_time__lte=current, order__buy_time__lte=current,
order__buy_time__gte=period_start, order__buy_time__gte=period_start,
) )
.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)) .aggregate(
total=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
)
)
.get("total") .get("total")
or 0.0 or 0.0
) )
@ -122,7 +133,13 @@ def get_daily_gross_revenue(
get_period_order_products(period, ["FINISHED"]) # OrderProduct queryset get_period_order_products(period, ["FINISHED"]) # OrderProduct queryset
.annotate(day=TruncDate("order__buy_time")) .annotate(day=TruncDate("order__buy_time"))
.values("day") .values("day")
.annotate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)) .annotate(
total=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
)
)
.order_by("day") .order_by("day")
) )
result: dict[date, float] = {} result: dict[date, float] = {}
@ -158,7 +175,11 @@ def get_top_returned_products(
.values("product") .values("product")
.annotate( .annotate(
returned_qty=Coalesce(Sum("quantity"), 0), returned_qty=Coalesce(Sum("quantity"), 0),
returned_amount=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), returned_amount=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
),
) )
.order_by("-returned_qty")[:limit] .order_by("-returned_qty")[:limit]
) )
@ -236,7 +257,11 @@ def get_top_categories_by_qty(
.values("product__category") .values("product__category")
.annotate( .annotate(
qty=Coalesce(Sum("quantity"), 0), qty=Coalesce(Sum("quantity"), 0),
gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), gross=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
),
) )
.order_by("-qty", "-gross")[:limit] .order_by("-qty", "-gross")[:limit]
) )
@ -277,7 +302,11 @@ def get_shipped_vs_digital_mix(
.values("product__is_digital") .values("product__is_digital")
.annotate( .annotate(
qty=Coalesce(Sum("quantity"), 0), qty=Coalesce(Sum("quantity"), 0),
gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), gross=Coalesce(
Sum(F("buy_price") * F("quantity")),
Decimal("0.00"),
output_field=DecimalField(),
),
) )
) )