I have a Django application with Django Rest Framework that is storing records in Postgres.
A Vehicle
model has an end_date
DateField
that represents the final payment date of a finance agreement, and a monthly_payment
FloatField
that represents the payment amount. The finance payment is made monthly, on the same day of the month as the final payment (e.g. if end_date
is 25/01/2020, a payment is made on the 25th of every month between now and 25/01/2020 inclusive.)
I have a ListVehicle
ListCreateAPIView
that returns a paginated list of vehicle records.
I am using a custom PageNumberPagination
class to return a data
object alongside the results
array that is populated by aggregating some of the fields in the Vehicle
model.
I want to include a field in this data
object that contains the total remaining amount left to pay on all of the Vehicle
entities in the database.
I have tried using @property
fields in the model that calculate the total remaining amount for each Vehicle
, but you can't aggregate over calculated properties (at least not with queryset.aggregate
), so the following solution did not work:
@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
I have also tried using ExpressionWrapper
s in the pagination class to first annotate each Vehicle
with a time_remaining
field and then annotated to have a remaining_balance
field calculated by extracting the month from time_remaining
and multiplying it by monthly_payment
. Then remaining_balance
is aggregated.
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
I have tried several combinations of annotations in this style. (including doing the entire calculation in one annotation). This returns 0 every time. I get a value if I use ExtractDay
instead of ExtractMonth
, so I think the problem here is that ExtractMonth
gets the month of a year from a DateField
, but not the number of full months in a DurationField
like I'd hoped, although this answer would suggest otherwise
Another thing that doesn't work is storing the remaining balance/remaining months with the Vehicle
when it is saved. As soon as a monthly payment date passes, a Vehicle
will be out of date until the next time it is saved and as such the aggregated totals will be incorrect.
Have I got the wrong approach? Is it possible to achieve what I'm trying to do with a custom database query, and if so would it be the best way?
Using PostgreSQL 10.10, Django 2.2, DRF 3.9.4 and Python 3.6
EDIT: Testcase for completeness
@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)