Search code examples
djangoaggregatedjango-queryset

Django format DecimalField in query / annotation


Is it possible to format a DecimalField in an annotation similar to intcomma template tag?

class Product(models.Model):
    plu = models.CharField(max_length=8)
    description = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=9, decimal_places=2, default=D('0'))

For example if instance.price = Decimal('1000') the annotated attribute would return the number formatted with a comma separator (1,000.00)

I'm creating PDF reports in ReportLab and table data is a list of lists. If the queryset could return the comma separated number there would be speed improvements rather than having to iterate over each object in the queryset to apply the formatting.

from django.contrib.humanize.templatetags.humanize import intcomma

products = Product.objects.all()
table_data = [['PLU', 'Description', 'Price']]
for product in products:
    table_data.append([product.plu, product.description, intcomma(product.price)])

Could be simplified to

table_data = [['PLU', 'Description', 'Price']] + list(
    Product.objects.all()
    .annotate(comma_sep_price=...)
    .values_list('plu', 'descripton', 'comma_sep_price')
)

Solution

  • I was able to solve this using the PostgreSQL data type formatting function TO_CHAR.

    Using an ExpressionWrapper:

    from django.db.models import CharField, ExpressionWrapper, Func, F, Value
    
    data = (
        Product.objects
        .annotate(
            formatted_price=ExpressionWrapper(
                Func(F('price'), Value('FM999,999,999.00'), function='TO_CHAR'),
                output_field=CharField()
            )
        )
        .values_list('plu', 'description', 'formatted_price')
    )
    

    Or as a reusable custom query expression:

    class CommaSeparated(Func):
        function = 'TO_CHAR'
        template = "%(function)s(%(expressions)s, 'FM999,999,999.00')"
    
    data = (
        Product.objects
        .annotate(formatted_price=CommaSeparated(F('price'), output_field=CharField()))
        .values('plu', 'description', 'formatted_price')
    )