У меня есть приложение Django с Django Rest Framework, которое хранит записи в Postgres. Модель Vehicle
имеет end_date
DateField
, представляющую дату окончательной оплаты финансового соглашения, и monthly_payment
FloatField
, представляющую сумму платежа. Финансовый платеж производится ежемесячно, в тот же день месяца, что и последний платеж (например, если end_date
- 25.01.2020, платеж производится 25 числа каждого месяца с настоящего момента до 25.01.2020 года включительно). .)
У меня есть ListVehicle
ListCreateAPIView
, который возвращает нумерованный список записей транспортных средств. Я использую собственный класс PageNumberPagination
для возврата объекта data
вместе с массивом results
, который заполняется путем агрегирования некоторых полей в модели Vehicle
. Я хочу включить в этот объект data
поле, содержащее общую оставшуюся сумму, оставшуюся для оплаты всех объектов Vehicle
в базе данных.
Я пытался использовать @property
поля в модели, которые вычисляют общую оставшуюся сумму для каждого Vehicle
, но вы не можете агрегировать по вычисленным свойствам (по крайней мере, не с queryset.aggregate
), поэтому следующее решение не сработало:
@property
def remaining_balance(self):
return max(self.remaining_monthly_payments * self.monthly_payment, 0)
@property
def remaining_monthly_payments(self):
now = datetime.datetime.now()
end = self.end_date
months = (end.year - now.year) * 12
months -= now.month + 1
months += end.month
today = now.day
final_day = end.day
if today < final_day:
months += 1
return months
Я также пытался использовать ExpressionWrapper
s в классе нумерации страниц, чтобы сначала аннотировать каждый Vehicle
полем time_remaining
, а затем аннотироватьчтобы получить поле remaining_balance
, рассчитанное путем извлечения месяца из time_remaining
и умножения его на monthly_payment
. Затем remaining_balance
агрегируется.
class VehicleFinancePagination(pagination.PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
duration = ExpressionWrapper(F('end_date') - Now(), output_field=fields.DurationField())
queryset = queryset.annotate(duration=duration)
queryset = queryset.annotate(remaining_balance=ExpressionWrapper(ExtractMonth('duration') * F('monthly_payment'), output_field=FloatField()))
self.total_remaining_balance = queryset.aggregate(total_remaining_balance=Sum('remaining_balance'))[
"total_remaining_balance"]
return super(VehicleFinancePagination, self).paginate_queryset(queryset, request, view)
def get_paginated_response(self, data):
paginated_response = super(VehicleFinancePagination, self).get_paginated_response(data)
paginated_response.data['total_remaining_balance'] = self.total_remaining_balance
return paginated_response
Я пробовал несколько комбинаций аннотаций в этом стиле. (включая выполнение всего расчета в одной аннотации). Это возвращает 0 каждый раз. Я получаю значение, если использую ExtractDay
вместо ExtractMonth
, поэтому я думаю, что проблема здесь в том, что ExtractMonth
получает месяц года из DateField
, но не количество полных месяцев в DurationField
как я и надеялся, хотя этот ответ подсказывал бы иначе
Еще одна вещь, которая не работает, - это сохранение остатка / оставшихся месяцев с Vehicle
, когда он сохранен. Как только истекает дата ежемесячного платежа, Vehicle
будет устаревать до следующего сохранения, и поэтому суммарные итоги будут неверными.
Неправильный ли я подход? Можно ли добиться того, что я пытаюсь сделать, с помощью пользовательского запроса к базе данных, и если да, то будет ли это лучшим способом?
Использование PostgreSQL 10.10, Django 2.2, DRF 3.9.4 и Python 3.6
РЕДАКТИРОВАТЬ: Testcase для полноты
@mock.patch('workshop.models.vehicle.datetime')
def test_remaining_balance(self, mocked_datetime):
mocked_datetime.datetime.now.return_value = datetime.datetime(2019, 11, 7, 1, 2, 3)
Vehicle.objects.create(registration="TS01 TST", monthly_payment=500, end_date=datetime.date(2020, 3, 1)) #2000
Vehicle.objects.create(registration="TS02 TST", monthly_payment=250, end_date=datetime.date(2020, 3, 25)) #1250
Vehicle.objects.create(registration="TS03 TST", monthly_payment=400, end_date=datetime.date(2020, 5, 1)) #2400
Vehicle.objects.create(registration="TS04 TST", monthly_payment=300, end_date=datetime.date(2020, 5, 25)) #2100
Vehicle.objects.create(registration="TS03 TST", monthly_payment=400, end_date=datetime.date(2018, 5, 1)) #0
Vehicle.objects.create(registration="TS04 TST", monthly_payment=300, end_date=datetime.date(2018, 5, 25)) #0
url = reverse('vehicles')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['total_remaining_balance'], 7750)