Django ORM: Как округлить (усечь) число в запросе?
Я работаю с чувствительными валютными значениями. В моем случае мне нужно воспроизвести лист с его формулами. Дело в том, что мне нужно округлить в меньшую сторону валютное значение с 2 знаками после запятой. Практическим примером является число: 9809.4069, его нужно округлить до 9809.40, с усечением. В противном случае, обычная функция округления возвращает мне 9809.41.
Обс. По соображениям производительности мне нужно свести все значения в один запрос. Обычные способы, с помощью обычных функций, таких как round(), не работают внутри функций, не относящихся к запросам.
Ну, мой запрос работает полностью FINE и приносит все, что я хочу, проблема в полях с функцией Round(), которая возвращает мне "неправильное" значение.
Запрос:
activetime_qs = activetime_qs.values(
'user', 'user__team__name','user__foreign_id','user__location','user__team__cost_center'
).annotate(
full_name=Concat(('user__first_name'),Value(' '),('user__last_name')),
project_id=Subquery(UserProject.objects.filter(user_id=OuterRef('user')).values('project')[:1]),
item_id=Subquery(Project.objects.filter(id=OuterRef('project_id')).values('item')[:1],),
project_name=Subquery(Project.objects.filter(id=OuterRef('project_id')).values('name')[:1]),
project_code=Subquery(Project.objects.filter(id=OuterRef('project_id')).values('code')[:1]),
item_name=Subquery(Item.objects.filter(id=OuterRef('item_id')).values('name')[:1]),
item_value=Subquery(Item.objects.filter(id=OuterRef('item_id')).values('unitary_value')[:1]),
available_time=Sum('duration'),
completed_tasks_amount=Case(
When(user__activity_type=ActivityType.DELIVERABLE,
then=Subquery(TaskExecution.objects.filter(members=OuterRef('user'), completed=True,
start__date__gte=initial_date, start__date__lte=final_date)
.values('pk').annotate(count=Func(F('pk'), function='Count'))
.values('count'), output_field=IntegerField()
)
),
default=1,
),
availability_percentage=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Round(F('available_time') / expected_time, 3)),
default=0,
output_field=FloatField()
),
subtotal_value=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Round(F('item_value') * F('availability_percentage'),2)
),
default=Round(F('item_value') * F('completed_tasks_amount'),3),
output_field=FloatField()
),
availability_discount=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Round(-1 + F('availability_percentage'),3),
),
default=0,
output_field=FloatField()
),
discount_value=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Round(F('item_value') - F('subtotal_value'),2),
),
default=0,
output_field=FloatField()
),
)
return activetime_qs
Тогда я попробовал два варианта округления значений аннотаций в меньшую сторону.
- Используйте DecimalField в качестве output_field с точным количеством десятичных знаков, которое мне нужно, и установите метод десятичного округления на ROUND_DOWN.
*Мой пользовательский DecimalFieldClass:
class RoundedDecimalField(FloatField):
def __init__(self, *args, **kwargs):
super(RoundedDecimalField, self).__init__(*args, **kwargs)
self.context = decimal.Context(prec=self.max_digits, rounding=decimal.ROUND_DOWN)
def context(self):
return decimal.Context(prec=self.max_digits, rounding=decimal.ROUND_DOWN)
def to_python(self, value):
self.context = decimal.Context(prec=self.max_digits, rounding=decimal.ROUND_DOWN)
if value is None:
return value
if isinstance(value, float):
if math.isnan(value):
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
return self.context.create_decimal_from_float(value)
try:
return self.context.create_decimal(value)
except (decimal.InvalidOperation, TypeError, ValueError):
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
При таком подходе аннотируются значения:
completed_tasks_amount=Case(
When(user__activity_type=ActivityType.DELIVERABLE,
then=Subquery(TaskExecution.objects.filter(members=OuterRef('user'), completed=True,
start__date__gte=initial_date, start__date__lte=final_date)
.values('pk').annotate(count=Func(F('pk'), function='Count'))
.values('count'), output_field=IntegerField()
)
),
default=1,
),
availability_percentage=Case(
When(user__activity_type=ActivityType.SERVICE_STATION, then=Cast(Round(F('available_time') / expected_time, 3), output_field=RoundedDecimalField(max_digits=4,decimal_places=3))),
default=Cast(0, output_field=RoundedDecimalField(max_digits=4,decimal_places=3)),
output_field=RoundedDecimalField(max_digits=4,decimal_places=3)
),
subtotal_value=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Cast(F('item_value') * F('availability_percentage'),output_field=RoundedDecimalField(max_digits=9,decimal_places=3))
),
default=Cast(F('item_value') * F('completed_tasks_amount'),output_field=RoundedDecimalField(max_digits=9,decimal_places=3)),
output_field=RoundedDecimalField(max_digits=9,decimal_places=4), # DELIVERABLE employee
),
availability_discount=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Cast(-1 + F('availability_percentage'), output_field=RoundedDecimalField(max_digits=4,decimal_places=3)),
),
default=Cast(0,output_field=RoundedDecimalField(max_digits=4,decimal_places=3)),
output_field=RoundedDecimalField(max_digits=4,decimal_places=3)
),
discount_value=Case(
When(user__activity_type=ActivityType.SERVICE_STATION,
then=Cast(F('item_value') - F('subtotal_value'),output_field=RoundedDecimalField(max_digits=9,decimal_places=2)),
),
default=Cast(0, output_field=RoundedDecimalField(max_digits=9,decimal_places=2)),
output_field=RoundedDecimalField(max_digits=9,decimal_places=2)
),
Но значения еще не округлены в меньшую сторону.
- Я попытался воспроизвести тот же запрос, но передавая поле output_field как float и передавая пользовательский валидатор, который анализирует число с помощью функции math.truncate. .
def truncate(value):
try:
return math.trunc(value, 2)
except:
return 0
А в аннотации значений я изменил поле ouput_field на:
FloatField(validators[truncate])
Но я тоже не работаю.
Есть ли способ округлить числа в запросе в меньшую сторону? Я не нашел ничего об этом. Родная функция django.db.models.Round не имеет круглого типа в качестве параметра, и я не смог использовать Func() для создания своей собственной... Кто-нибудь может мне помочь?
Вы ограничены определенной версией django
? Если нет, то я полагаю, что в документации упоминается аргумент precision
в виде ключевого слова для функции Round
, который может подойти для ваших нужд.
https://docs.djangoproject.com/en/4.1/ref/models/database-functions/#django.db.models.functions.Round
Округляет числовое поле или выражение с точностью (должно быть целое число) до десятичных знаков. По умолчанию округляется до ближайшего целого числа. Округление половинных значений в большую или меньшую сторону зависит от базы данных.
На самом деле я неправильно понял. Вы уже используете точность в одном из ваших примеров, вы просто передаете ее как позиционный аргумент.
Глядя на вашу реализацию с Decimal
, я думаю, что у вас правильная идея. Однако, использование prec=2
или prec=3
, похоже, относится к точности значащих цифр, поэтому 9809.4069
с prec=2
будет 9800 = 9.8 * 10^2
, а prec=3
даст 9810 = 9.81 * 10^3
.
Итак, если ваши числа будут floats
, вам следует использовать create_decimal
, как и раньше, но не ограничивать точность.
context = decimal.Context()
context.create_decimal(9809.4069)
>>> Decimal('9809.468999999999999996023488224')
Тогда вы будете использовать quantize
, как описано в документации, для соответствующего отсечения при работе с валютой.
https://docs.python.org/3/library/decimal.html
Метод quantize() округляет число до фиксированной экспоненты. Этот метод полезен для денежных приложений, которые часто округляют результаты до фиксированного числа мест
.
Mysql имеет функцию "truncate" для округления значений, полученных из запросов. Вы можете использовать функцию базы данных с 'Truncate' в аргументе функции
from django.db.models import Func,F
Func(F('item_value') * F('availability_percentage'), 2, function='Truncate', output_field=FloatField())