python 93 lines · 3 tabs

Daily Order Stats Rollup with Django TruncDate Aggregation and a JSON API View

Shared by codesnips Aug 2026
3 tabs
from django.db import models
from django.db.models import Avg, Count, Sum, DecimalField
from django.db.models.functions import Coalesce, TruncDate
from django.utils import timezone


class OrderQuerySet(models.QuerySet):
    def paid(self):
        return self.filter(status=Order.Status.PAID)

    def in_range(self, start, end):
        return self.filter(created_at__date__gte=start, created_at__date__lte=end)

    def daily_stats(self, tz=None):
        tz = tz or timezone.get_current_timezone()
        zero = Coalesce(Sum("total"), 0, output_field=DecimalField())
        return (
            self.paid()
            .annotate(day=TruncDate("created_at", tzinfo=tz))
            .values("day")
            .annotate(
                orders=Count("id"),
                revenue=zero,
                avg_order=Coalesce(Avg("total"), 0, output_field=DecimalField()),
            )
            .order_by("day")
        )


class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        PAID = "paid", "Paid"
        REFUNDED = "refunded", "Refunded"

    created_at = models.DateTimeField(default=timezone.now, db_index=True)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING)

    objects = OrderQuerySet.as_manager()

    class Meta:
        indexes = [models.Index(fields=["status", "created_at"])]
3 files · python Explain with highlit

This snippet shows how to build a compact daily sales report in Django using the ORM's date-truncation and aggregation features, exposed through a lightweight JSON endpoint. The pattern is common in reporting dashboards: instead of pulling every Order row into Python and grouping by hand, the database does the grouping and the arithmetic, returning one row per day.

In models.py, the Order model is intentionally simple — a created_at timestamp, a total decimal, and a status. The OrderQuerySet carries the reporting logic so it can be reused from views, management commands, or the admin. Its paid() method narrows the set to revenue-bearing orders, and daily_stats() is the core rollup: annotate(day=TruncDate('created_at', tzinfo=tz)) collapses each timestamp to a calendar date, .values('day') establishes the GROUP BY, and the trailing .annotate(...) computes Count, Sum, and Avg per group. Coalesce guards against NULL sums on empty days, and ordering by day gives a stable series.

A subtle but important detail is timezone handling. TruncDate truncates in whatever timezone is passed via tzinfo; without it, days would be bucketed in UTC and could disagree with what an operator sees locally. The queryset accepts an explicit tz so the caller controls the boundary, which matters when a sale just before midnight local time must land on the correct day.

In views.py, DailyStatsView reads start and end query parameters, defaults to a trailing window, and calls the queryset method. _parse_date fails soft, returning None for bad input so the view can fall back rather than raising. Results are serialized manually into plain dicts, with Decimal values cast to strings to avoid float rounding surprises in JSON, and the response is cached briefly with cache_page since yesterday's totals rarely change.

urls.py wires the endpoint at /api/stats/daily/. The trade-off of this approach is that the aggregation runs on every uncached request; for high-traffic dashboards one would persist a materialized daily rollup table instead. For moderate volumes, letting TruncDate and annotate push the work into Postgres is fast, correct, and far less code than manual grouping.


Related snips

Share this code

Here's the card — post it anywhere.

Daily Order Stats Rollup with Django TruncDate Aggregation and a JSON API View — share card
Link copied