diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 64444297f3..ffe42e925d 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1195,8 +1195,6 @@ class BaseDatabaseOperations(object): Transform a decimal.Decimal value to an object compatible with what is expected by the backend driver for decimal (numeric) columns. """ - if value is None: - return None return utils.format_number(value, max_digits, decimal_places) def year_lookup_bounds_for_date_field(self, value): diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 8f025ef41e..6443d9514d 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -312,9 +312,7 @@ WHEN (new.%(col_name)s IS NULL) return value def convert_decimalfield_value(self, value, field): - if value is not None: - value = backend_utils.typecast_decimal(field.format_number(value)) - return value + return backend_utils.typecast_decimal(field.format_number(value)) # cx_Oracle always returns datetime.datetime objects for # DATE and TIMESTAMP columns, but Django wants to see a diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index e85bfaf031..db2ac07ce9 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -277,9 +277,7 @@ class DatabaseOperations(BaseDatabaseOperations): return converters def convert_decimalfield_value(self, value, field): - if value is not None: - value = backend_utils.typecast_decimal(field.format_number(value)) - return value + return backend_utils.typecast_decimal(field.format_number(value)) def convert_datefield_value(self, value, field): if value is not None and not isinstance(value, datetime.date): diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py index a95255b862..7aa0cfdad5 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -191,9 +191,18 @@ def format_number(value, max_digits, decimal_places): Formats a number into a string with the requisite number of digits and decimal places. """ + if value is None: + return None if isinstance(value, decimal.Decimal): context = decimal.getcontext().copy() - context.prec = max_digits - return "{0:f}".format(value.quantize(decimal.Decimal(".1") ** decimal_places, context=context)) - else: + if max_digits is not None: + context.prec = max_digits + if decimal_places is not None: + value = value.quantize(decimal.Decimal(".1") ** decimal_places, context=context) + else: + context.traps[decimal.Rounded] = 1 + value = context.create_decimal(value) + return "{:f}".format(value) + if decimal_places is not None: return "%.*f" % (decimal_places, value) + return "{:f}".format(value) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 996a215da3..63c2951075 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -255,7 +255,7 @@ class ExpressionNode(CombinableMixin): elif internal_type.endswith('IntegerField'): return int(value) elif internal_type == 'DecimalField': - return backend_utils.typecast_decimal(field.format_number(value)) + return backend_utils.typecast_decimal(value) return value def get_lookup(self, lookup): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 3b42c998b7..de754c83e5 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1536,7 +1536,7 @@ class DecimalField(Field): ) def _format(self, value): - if isinstance(value, six.string_types) or value is None: + if isinstance(value, six.string_types): return value else: return self.format_number(value) diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt index 5dadf35873..24e4f42834 100644 --- a/docs/ref/models/expressions.txt +++ b/docs/ref/models/expressions.txt @@ -257,7 +257,10 @@ placeholder within the ``template``. The ``output_field`` argument requires a model field instance, like ``IntegerField()`` or ``BooleanField()``, into which Django will load the value -after it's retrieved from the database. +after it's retrieved from the database. Usually no arguments are needed when +instantiating the model field as any arguments relating to data validation +(``max_length``, ``max_digits``, etc.) will not be enforced on the expression's +output value. Note that ``output_field`` is only required when Django is unable to determine what field type the result should be. Complex expressions that mix field types @@ -318,8 +321,10 @@ values into their corresponding database type. The ``output_field`` argument should be a model field instance, like ``IntegerField()`` or ``BooleanField()``, into which Django will load the value -after it's retrieved from the database. - +after it's retrieved from the database. Usually no arguments are needed when +instantiating the model field as any arguments relating to data validation +(``max_length``, ``max_digits``, etc.) will not be enforced on the expression's +output value. Technical Information ===================== diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index e4b821b43d..88a5f11053 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -17,7 +17,7 @@ with warnings.catch_warnings(record=True) as w: from django.test import TestCase from django.test.utils import Approximate from django.test.utils import CaptureQueriesContext -from django.utils import six +from django.utils import six, timezone from django.utils.deprecation import RemovedInDjango20Warning from .models import Author, Publisher, Book, Store @@ -689,6 +689,19 @@ class BaseAggregateTestCase(TestCase): self.assertNotIn('order by', qstr) self.assertEqual(qstr.count(' join '), 0) + def test_decimal_max_digits_has_no_effect(self): + Book.objects.all().delete() + a1 = Author.objects.first() + p1 = Publisher.objects.first() + thedate = timezone.now() + for i in range(10): + Book.objects.create( + isbn="abcde{}".format(i), name="none", pages=10, rating=4.0, + price=9999.98, contact=a1, publisher=p1, pubdate=thedate) + + book = Book.objects.aggregate(price_sum=Sum('price')) + self.assertEqual(book['price_sum'], Decimal("99999.80")) + class ComplexAggregateTestCase(TestCase): fixtures = ["aggregation.json"] @@ -755,8 +768,8 @@ class ComplexAggregateTestCase(TestCase): self.assertEqual(b2.sums, 383.69) b3 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'), - output_field=DecimalField(max_digits=6, decimal_places=2))).get(pk=4) - self.assertEqual(b3.sums, Decimal("383.69")) + output_field=DecimalField())).get(pk=4) + self.assertEqual(b3.sums, Approximate(Decimal("383.69"), places=2)) def test_complex_aggregations_require_kwarg(self): with six.assertRaisesRegex(self, TypeError, 'Complex expressions require an alias'): diff --git a/tests/backends/tests.py b/tests/backends/tests.py index db79cd0325..1c01b4a1eb 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import copy import datetime -from decimal import Decimal +from decimal import Decimal, Rounded import re import threading import unittest @@ -1059,6 +1059,22 @@ class BackendUtilTests(TestCase): '0.1') equal('0.1234567890', 12, 0, '0') + equal('0.1234567890', None, 0, + '0') + equal('1234567890.1234567890', None, 0, + '1234567890') + equal('1234567890.1234567890', None, 2, + '1234567890.12') + equal('0.1234', 5, None, + '0.1234') + equal('123.12', 5, None, + '123.12') + with self.assertRaises(Rounded): + equal('0.1234567890', 5, None, + '0.12346') + with self.assertRaises(Rounded): + equal('1234567890.1234', 5, None, + '1234600000') class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase):