Django annotate by sum of two values with multiple relations - django

I have 4 models:
class App(models.Model):
...
class AppVersion(models.Model):
app = models.ForeignKey(App)
version_code = models.IntegerField()
class Meta:
ordering = ('-version_code',)
...
class Apk(models.Model):
version = models.OneToOneField(AppVersion)
size = models.IntegerField()
class Obb(models.Model):
version = models.ForeignKey(AppVersion)
size = models.IntegerField()
AppVersion version always has one Apk, but may have 0, 1 or 2 Obb's
I want to annotate QuerySet by total size of the App (which is Apk.size + sum of all Obb.size for given AppVersion).
My App QuerySet looks like this:
qs = App.objects.filter(is_visible=True)
and versions subquery is:
latest_versions = Subquery(AppVersion.objects.filter(application=OuterRef(OuterRef('pk'))).values('pk')[:1])
This subquery always gives the latest AppVersion of the App.
So what subquery should I use to annotate qs with size attribute calculated as shown above?

How about something like this - from my understanding you want an Apps apk, and obb sizes summed. apk and obb_set can be replaced by the fields related name if you added one. What I chose should be the defaults for a django OneToOne and Fk related name.
from django.db.models import F, Value, Sum, IntegerField
qs = App.objects.filter(
is_visible=True
).annotate(
apk_size=Sum('apk__size'),
obb_size=Sum('obb_set__size')
).annotate(
total_size=Value(
F('apk_size') + F('obb_size'),
output_field=IntegerField()
)

Related

Django: Filtering a related field by date yields unwanted results

models:
class Vehicle(models.Model):
licence_plate = models.CharField(max_length=16)
class WorkTime(models.Model):
work_start = models.DateTimeField()
work_end = models.DateTimeField()
vehicle = models.ForeignKey(Vehicle, on_delete=models.SET_NULL, related_name="work_times")
However when I try to filter those working times using:
qs = Vehicle.objects.filter(
work_times__work_start__date__gte="YYYY-MM-DD",
work_times__work_end__date__lte="YYYY-MM-DD").distinct()
I get results that do not fit the timeframe given. Most commonly when the work_end fits to something, it returns everything from WorkTime
What I would like to have:
for vehicle in qs:
for work_time in vehicle.work_times:
print(vehicle, work_time.work_start, work_time.work_end)
The filter has no effect on the .work_times from the Vehicles, it only will ensure that the Vehicles in the qs will contain at least one WorkTime in the given range.
You can work with a Prefetch object [Django-doc] to allow filtering efficiently on a related manager:
from django.db.models import Prefetch
qs = Vehicle.objects.prefetch_related(
Prefetch(
'work_times',
WorkTime.objects.filter(
work_start__date__range=('2021-03-01', '2021-03-12')
),
to_attr='filtered_work_times'
)
)
and then you can work with:
for vehicle in qs:
for work_time in vehicle.filtered_work_times:
print(vehicle, work_time.work_start, work_time.work_end)

Django: cannot annotate using prefetch calculated attribute

Target is to sum and annotate workingtimes for each employee on a given time range.
models:
class Employee(models.Model):
first_name = models.CharField(max_length=64)
class WorkTime(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name="work_times")
work_start = models.DateTimeField()
work_end = models.DateTimeField()
work_delta = models.IntegerField(default=0)
def save(self, *args, **kwargs):
self.work_delta = (self.work_end - self.work_start).seconds
super().save(*args, **kwargs)
getting work times for each employee at a given date range:
queryset = Employee.objects.prefetch_related(
Prefetch(
'work_times',
queryset=WorkTime.objects.filter(work_start__date__range=("2021-03-01", "2021-03-15"]))
.order_by("work_start"),
to_attr="filtered_work_times"
)).all()
trying to annotate sum of work_delta to each employee:
queryset.annotate(work_sum=Sum("filtered_work_times__work_delta"))
This causes a FieldError:
Cannot resolve keyword 'filtered_work_times' into field. Choices are: first_name, id, work_times
How would one proceed from here? Using Django 3.1 btw.
You should use filtering on annotations.
I haven't tried, but I think the following code might help you:
from django.db.models import Sum, Q
Employee.objects.annotate(
work_sum=Sum(
'work_times__work_delta',
filter=Q(work_times__work_start__date__range=["2021-03-01", "2021-03-15"])
)
)
You cannot use the prefetch_related values in the query because simply the prefetching is done separately, Django would first fetch the current objects and then make queries to fetch the related objects so the field you try to refer is not even part of the query you want to add it to.
Instead of doing this simply add a filter [Django docs] keyword argument to your aggregation function:
from django.db.models import Q
start_date = datetime.date(2021, 3, 1)
end_date = datetime.date(2021, 3, 15)
result = queryset.annotate(work_sum=Sum("work_times__work_delta", filter=Q(work_times__work_start__date__range=(start_date, end_date))))

Annotations in django with model managers

I have two models with an one to many relation.
One model named repairorder, which can have one or more instances of work that is performed on that order.
What I need is to annotate the Repairorder queryset to sum the cummulative Work duration. On the Work model I annotated the duration of a single Work instance based on the start and end date time stamps. Now I need to use this annotated field to sum the total cummulative Work that is performed for each order. I tried to extend the base model manager:
from django.db import models
class WorkManager(models.Manager):
def get_queryset(self):
return super(OrderholdManager, self).get_queryset().annotate(duration=ExpressionWrapper(Coalesce(F('enddate'), Now()) - F('startdate'), output_field=DurationField()))
class Work(models.Model):
#...
order_idorder = models.ForeignKey('Repairorder', models.DO_NOTHING)
startdate = models.DateTimeField()
enddate = models.DateTimeField()
objects = WorkManager()
class RepairorderManager(models.Manager):
def get_queryset(self):
return super(RepairorderexternalManager, self).get_queryset().annotate(totalwork=Sum('work__duration'), output_field=DurationField())
class Repairorder(models.Model):
#...
idrepairorder = models.autofield(primary_key=True)
objects = RepairorderManager()
For each Repairorder I want to display the 'totalwork', however this error appears: QuerySet.annotate() received non-expression(s): . and if I remove the output_field=DurationField() from the RepairorderMananager, it says: Cannot resolve keyword 'duration' into field.
Doing it the 'Python way' by using model properties is not an option with big datasets.
You will need to add the calculation to the RepairorderManager as well:
class RepairorderManager(models.Manager):
def get_queryset(self):
return super(RepairorderexternalManager, self).get_queryset().annotate(
totalwork=ExpressionWrapper(
Sum(Coalesce(F('work__enddate'), Now()) - F('work__startdate')),
output_field=DurationField()
)
)
Django does not take into account annotations introduced by manager on related objects.

Filtering queryset if one value is greater than another value

I am trying to filter in view my queryset based on relation between 2 fields .
however always getting the error that my field is not defined .
My Model has several calculated columns and I want to get only the records where values of field A are greater than field B.
So this is my model
class Material(models.Model):
version = IntegerVersionField( )
code = models.CharField(max_length=30)
name = models.CharField(max_length=30)
min_quantity = models.DecimalField(max_digits=19, decimal_places=10)
max_quantity = models.DecimalField(max_digits=19, decimal_places=10)
is_active = models.BooleanField(default=True)
def _get_totalinventory(self):
from inventory.models import Inventory
return Inventory.objects.filter(warehouse_Bin__material_UOM__UOM__material=self.id, is_active = true ).aggregate(Sum('quantity'))
totalinventory = property(_get_totalinventory)
def _get_totalpo(self):
from purchase.models import POmaterial
return POmaterial.objects.filter(material=self.id, is_active = true).aggregate(Sum('quantity'))
totalpo = property(_get_totalpo)
def _get_totalso(self):
from sales.models import SOproduct
return SOproduct.objects.filter(product__material=self.id , is_active=true ).aggregate(Sum('quantity'))
totalso = property(_get_totalpo)
#property
def _get_total(self):
return (totalinventory + totalpo - totalso)
total = property(_get_total)
And this is line in my view where I try to get the conditional queryset
po_list = MaterialFilter(request.GET, queryset=Material.objects.filter( total__lte = min_quantity ))
But I am getting the error that min_quantity is not defined
What could be the problem ?
EDIT:
My problem got solved thank you #Moses Koledoye but in the same code I have different issue now
Cannot resolve keyword 'total' into field.Choices are: am, author, author_id, bom, bomversion, code, creation_time, description, id, inventory, is_active, is_production, itemgroup, itemgroup_id, keywords, materialuom, max_quantity, min_quantity, name, pomaterial, produce, product, slug, trigger_quantity, uom, updated_by, updated_by_id, valid_from, valid_to, version, warehousebin
Basically it doesn't show any of my calculated fields I have in my model.
Django cannot write a query which is conditioned on a field whose value is unknown. You need to use a F expression for this:
from django.db.models import F
queryset = Material.objects.filter(total__lte = F('min_quantity'))
And your FilterSet becomes:
po_list = MaterialFilter(request.GET, queryset = Material.objects.filter(total__lte=F('min_quantity')))
From the docs:
An F() object represents the value of a model field or annotated
column. It makes it possible to refer to model field values and
perform database operations using them without actually having to pull
them out of the database into Python memory

Use ifnull default in django aggregation

I have the following model classes:
class Goods(models.Model):
name = models.CharField(max_length=100)
class InRecord(models.Model):
goods = models.ForeignKey(Goods, related_name='in_records')
timestamp = models.DateTimeField()
quantity = models.IntegerField()
class OutRecord(models.Model):
goods = models.ForeignKey(Goods, related_name='out_records')
timestamp = models.DateTimeField()
quantity = models.IntegerField()
So, I want to get a QuerySet which contains all the goods having a positive repository.
Another way to describe it, I want to filter Goods which has a greater InRecord quantity summary than OutRecord summary.
What I've tried:
First, I use annotate to add the summary to the queryset:
qs = Goods.objects.annotate(
qty_in=Sum(in_records__quantity),
qty_out=Sum(out_records_quantity)
)
This seemed works, but have one problem, when there is no relative in_records or out_records of some goods, the fields annotated returns None.
Question: So, is there any way for me to set a default in this case, just like a ifnull(max(inreocrd.quantity), 0)* call in sql?
After this, I want to add a filter on that QuerySet:
I tried:
qs = qs.filter(qty_in__gt(F(qty_out)))
But still if there is no records on a goods, It doesn't work.
Please help.
You can use Django's Coalesce function. Something like this should work in Django 1.8 or later:
from django.db.models.functions import Coalesce
qs = Goods.objects.annotate(
qty_in=Sum(Coalesce(in_records__quantity, 0)),
qty_out=Sum(Coalesce(out_records__quantity, 0))
)