Django / Python - Querying calculated field - python

I have the below models, and want to run a calculation on a foreign key of allocation and then sum the amounts.
class Fundraising(models.Model):
#property
def amount_raised(self):
amount_raised = FundraisingDonation.objects.filter(
fundraising_event=self,
).aggregate(donationamount=Coalesce(Sum('donationamount'), 0.00))['donationamount']
return amount_raised
class FundraisingDonation(models.Model):
donationamount = models.DecimalField(max_digits=11, decimal_places=2, default=0)
class Testmodel(models.Model):
organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE)
allocation = models.ForeignKey(Fundraising, on_delete=models.CASCADE, related_name='items')
percentshare = models.DecimalField(max_digits=11, decimal_places=2, default=0) #SET BY USER
#property
def amount(self):
amount = self.allocation.amount_raised * self.percentshare
return amount
The 'amount' model property above calculates the amount field for each model instance.
I'm trying now to sum up the amounts for each organisation on a new model, however the following doesn't work as I don't believe I can run a query on a calculated field?
class Totals(models.Model):
totals = total + totalB + totalC
#property
def total(self):
total = Testmodel.objects.filter(
organisation=self.organisation
).aggregate(amount=Coalesce(Sum('amount'), 0.00))['amount']
Is there any way to amend the last line for this to work, or to reperform the calculation in this line? I also tried aggregate(Sum(amount_raised * percentshare)), but this didn't seem to work either?

You can not aggregate on a #property, since that property is unknown at the database side.
What you can do is .annotate(…) [Django-doc] the queryset of Organisations for example:
from django.db.models import F, Sum
Organisation.objects.annotate(
total=Sum(
F('testmodel__allocation__amount_raised') * F('testmodel__percentshare')
)
)
Each Organisation that arises from this queryset will have an extra attribute named .total that contains the sum of the amount_raised values for the related testmodel items, multiplied with percentshare.
EDIT: since the amount_raised is a property, we need to aggregate on the FundraisingDonation model, so:
from django.db.models import F, Sum, Value
from django.db.models.functions import Coalesce
Organisation.objects.annotate(
total=Coalesce(Sum(
F('testmodel__allocation__fundraising__donationamount') *
F('testmodel__percentshare'), Value(0))
)
)

Related

Django querying data out of models and get sum of field

I'm just beginning with Django and have the following question:
I have set up a model looking like
class Automation_Apps(models.Model):
app_name = models.CharField(max_length=50)
app_description = models.TextField(blank=True)
view_function_name = models.CharField(max_length=50)
time_saver_min = models.IntegerField()
implementation_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.app_name
class Automation_Usage(models.Model):
used_app = models.ForeignKey(Automation_Apps, on_delete=models.CASCADE)
used_by_user = models.ForeignKey(User, on_delete=models.CASCADE)
used_on_date = models.DateTimeField(auto_now_add=True)
I would like to query it like:
Select Sum(time_saver_min)
from Automation_Apps, Automation_Usage
where Automation_Usage.used_app = Automation_Apps.app_name
The goal of this is the following:
Automation_App will have the Apps and a field, how much time will be saved by using this app
Automation_Usage will have the records, when a user is utilizing the app.
With my query I would like to pull how often the apps where used and how much time was saved by using it (which comes from the Automation_App).
I would like to do this in my view.
To get sum of time utilized over all apps by each user:
from django.db.models import Sum
Automation_Usage.objects.values('used_by_user').annotate(time=Sum('used_app__time_saver_min'))
To get number of users and sum of time utilized for each app:
from django.db.models import Sum, Count
Automation_Usage.objects.values('used_app').annotate(users=Count('used_by_user'), time=Sum('used_app__time_saver_min'))
You can use the Model Sum to Sum the value of your model.
Do it this way;
from django.db.models import Sum
Then
data = Model.objects.aggregate(Sum('time_saver_min'))['time_saver_min__sum'] print(data)
You can use annotate in Django ORM.
My solution:
Sum(time_saver_min) based on Automation_Usage.used_app = Automation_Apps.app_name
=> queryset = Automation_Usage.objects.values('used_app').annotate(Coalesce(Sum('used_app__time_saver_min'), Value(0)))
=> print(queryset['used_app__time_saver_min__sum'])

Queryset with annotate Subquery with OuterRef and Sum

I'm trying to return the sum of fields from another model inside a Subquery.
My main queryset returns all users of type company. I have to return the total of credits used by taking the data from CreditOrder and Sum the credit_used field.
I'm using ClusterableModel and ParentalKey from django-modelcluster
My CreditOrder model
class CreditOrder(ClusterableModel):
credit = ParentalKey(
Credit, on_delete=models.CASCADE, related_name="credit_order"
)
order = ParentalKey(Order, on_delete=models.CASCADE, related_name="credit_order")
credit_used = models.DecimalField(
max_digits=12, decimal_places=2, null=True, blank=True
)
My User model
class User(AbstractUser, ClusterableModel):
username = models.CharField(max_length=40, null=True, blank=True)
user_type = models.CharField(max_length=20, choices=TIPO_UTENTE, default="dipendente")
My queryset using the class model User
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.filter(user_type='company')
credits_used_subquery = Subquery(CreditOrder.objects.filter(credit__font__company__id=OuterRef('id')).order_by()
.values('credit_used').annotate(credit_used_sum=Sum('credit_used'))
.values('credit_used_sum'), output_field=DecimalField())
qs = qs.annotate(
_credits_used_sum=credits_used_subquery
)
return qs
But this error is returning me:
django.db.utils.ProgrammingError: more than one row returned by a subquery used as an expression
If you just need to sum all the credits used by the company, you can do:
qs.annotate(_credits_used_sum=Sum('font__credit__credit_used'))
Without seeing all the models between CreditOrder and User, it's hard to tell exactly what you've got wrong. It looks like the Credit model is linked to Font and Font might have an attribute called Company which is a foreign key to the User model?
In any case, your first values call has the wrong argument, you need to group by the the essentially the same thing you are linked to in the outer ref. So I'd suggest
.values('credit__font__company__id')
In the first call to values. And keep the annotate and second call to values the same.
Another answer suggests doing the Sum with a join instead of a Subquery, if you like the simplicity of that api, but you still want to use a Subquery, you can use the django-sql-utils package. After you pip install django-sql-utils
from sql_util.utils import SubquerySum
qs.annotate(_credits_used_sum=SubquerySum('font_credit_credit_used')
I solved my problem with this class:
class SubquerySum(Subquery):
template = "(SELECT COALESCE(SUM(%(field)s), %(zero_value)s) FROM (%(subquery)s) _sum)"
def as_sql(self, compiler, connection, template=None, **extra_context):
if 'field' not in extra_context and 'field' not in self.extra:
if len(self.queryset._fields) > 1:
raise FieldError('You must provide the field name, or have a single column')
extra_context['field'] = self.queryset._fields[0]
if 'zero_value' not in extra_context and 'zero_value' not in self.extra:
extra_context['zero_value'] = 0
return super().as_sql(compiler, connection, template=template, **extra_context)
and
def get_queryset(self, request):
credit_query=CreditOrder.objects.filter(credit__font__company__id=OuterRef('id')).order_by()
.values('credit_used')
qs = super().get_queryset(request)
qs = qs.filter(user_type='company')
qs = qs.annotate(
_credits_used_sum=SubquerySum(credit_query, zero_value=0, field='credit_used')
)
return qs

How to reduce quantity of an item in main table when it is being used in another table - django

I am creating my model in Django and I have a many to many relationship between supplies and van kits. The idea is that an "item" can belong to many "van kits" and a "van kit" can have many " items. I created an intermediary model that will hold the relationship, but I am struggling to figure out a way to relate the quantity in the van kit table to the quantity in the main supplies table. For example, if I wanted to mark an item in the van kit as damaged and reduce the quantity of that supply in the van kit, I would also want to reduce the total count of that supply in the main "supplies" table until it has been replenished. I am thinking that maybe I'll have to create a function in my views file to carry out that logic, but I wanted to know if it could be implemented in my model design instead to minimize chances of error. Here's my code:
class supplies(models.Model):
class Meta:
verbose_name_plural = "supplies"
# limit the user to selecting a pre-set category
choices = (
('CREW-GEAR','CREW-GEAR'),
('CONSUMABLE','CONSUMABLE'),
('BACK-COUNTRY','BACK-COUNTRY')
)
supplyName = models.CharField(max_length=30, blank=False) # if they go over the max length, we'll get a 500 error
category = models.CharField(max_length=20, choices = choices, blank=False)
quantity = models.PositiveSmallIntegerField(blank=False) # set up default
price = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) # inputting price is optional
def __str__(self):
return self.supplyName
class van_kit(models.Model):
supply_name = models.ManyToManyField(supplies, through='KitSupplies',through_fields=('vanKit','supplyName'), related_name="supplies")
van_kit_name = models.CharField(max_length=100)
vanName = models.ForeignKey(vans, on_delete=models.CASCADE)
def __str__(self):
return self.van_kit_name
class KitSupplies(models.Model):
supplyName = models.ForeignKey(supplies, on_delete=models.CASCADE)
vanKit = models.ForeignKey(van_kit, on_delete=models.CASCADE)
quantity = models.PositiveSmallIntegerField(blank=False)
def __str__(self):
return str(self.supplyName)
class Meta:
verbose_name_plural = 'Kit Supplies'
I am fairly new to django, I have to learn it for a class project so if my logic is flawed or if a better way to do it is obvious, please respectfully let me know. I'm open to new ways of doing it. Also, I've read through the documentation on using "through" and "through_fields" to work with the junction table, but I'm worried I may not be using it correctly. Thanks in advance.
One option would be to drop/remove the field quantity from your supplies model and just use a query to get the total quantity.
This would be a bit more expensive, as the query would need to be run each time you want to know the number, but on the other hand it simplifies your design as you don't need any update logic for the field supplies.quantity.
The query could look as simple as this:
>>> from django.db.models import Sum
>>> supplies_instance.kitsupplies_set.aggregate(Sum('quantity'))
{'quantity__sum': 1234}
You could even make it a property on the model for easy access:
class supplies(models.Model):
...
#property
def quantity(self):
data = self.kitsupplies_set.aggregate(Sum('quantity'))
return data['quantity__sum']

Django-Tables2 add extra columns from dictionary

I apologize if this question has been asked before but I couldn't find my specific use case answered.
I have a table that displays basic product information. Product details such as price, number of sales, and number of sellers are scraped periodically and stored in a separate database table. Now I want to display both the basic product information and scraped details in one table on the frontend using tables2. To do this, I wrote a function in my Product model to fetch the latest details and return them as a dictionary this way I can use a single Accessor call.
# models.py
class Product(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=256)
brand = models.ForeignKey(Brand)
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES)
def __unicode__(self):
return self.name
def currentState(self):
currentDetailState = ProductDetailsState.objects.filter(
product=self
).latest('created_at')
# return current details as a dictionary
return {
price: currentDetailState.price,
num_sellers: currentDetailState.num_sellers,
num_sales: currentDetailState.num_sales
}
class ProductDetailsState(models.Model):
product = models.ForeignKey(Product)
created_at = models.DateTimeField(auto_now_add=True)
price = models.DecimalField(max_digits=6, decimal_places=2, null=True)
num_sellers = models.IntegerField(null=True)
num_sales = models.IntegerField(null=True)
def __unicode__(self):
return self.created_at
# tables.py
class ProductTable(tables.Table):
productBrand = tables.Column(
accessor=Accessor('brand.name'),
verbose_name='Brand'
)
currentRank = tables.Column(
accessor=Accessor('currentRank')
)
class Meta:
model = Product
...
How do I now use this returned dictionary and split it into columns in my Product table? Is there another way to use an Accessor than how I am doing it?
You can use the Accessor to traverse into the dict, so something like this should work:
class ProductTable(tables.Table):
# brand is the name of the model field, if you use that as the column name,
# and you have the __unicode__ you have now, the __unicode__ will get called,
# so you can get away with jus this:
brand = tables.Column(verbose_name='Brand')
currentRank = tables.Column()
# ordering on the value of a dict key is not possible, so better to disable it.
price = tables.Column(accessor=tables.A('currentState.price'), orderable=False)
num_sellers = tables.Column(accessor=tables.A('currentState.num_sellers'), orderable=False)
num_sales = tables.Column(accessor=tables.A('currentState.num_sales'), orderable=False)
class Meta:
model = Product
While this works, sorting is also nice to have. In order to do that, your 'currentState' method is a bit in the way, you should change the QuerySet you pass to the table. This view shows how that could work:
from django.db.models import F, Max
from django.shortcuts import render
from django_tables2 import RequestConfig
from .models import Product, ProductDetailsState
from .tables import ProductTable
def table(request):
# first, we make a list of the post recent ProductDetailState instances
# for each Product.
# This assumes the id's increase with the values of created_at,
# which probably is a fair assumption in most cases.
# If not, this query should be altered a bit.
current_state_ids = Product.objects.annotate(current_id=Max('productdetailsstate__id')) \
.values_list('current_id', flat=True)
data = Product.objects.filter(productdetailsstate__pk__in=current_state_ids)
# add annotations to make the table definition cleaner.
data = data.annotate(
price=F('productdetailsstate__price'),
num_sellers=F('productdetailsstate__num_sellers'),
num_sales=F('productdetailsstate__num_sales')
)
table = ProductTable(data)
RequestConfig(request).configure(table)
return render(request, 'table.html', {'table': table})
This simplifies the table definition, using the annotations created above:
class ProductTable(tables.Table):
brand = tables.Column(verbose_name='Brand')
currentRank = tables.Column()
price = tables.Column()
num_sellers = tables.Column()
num_sales = tables.Column()
class Meta:
model = Product
You can find the complete working django project at github

Calculate a number in a Django model from another model's data

I want to take data (amount_spent) from the field of each user and add those numbers up and display them in another field (total_revenue) from a different model (RevenueInfo).
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.db import models
from django import forms, views
# Create your models here.
#LoginInfo is being used, LoginForms in forms.py is
class LoginInfo(models.Model):
username = models.CharField('', max_length=10)
password = models.CharField('', max_length=15)
class ExtendedProfile(models.Model):
user = models.OneToOneField(User)
amount_spent = models.DecimalField(max_digits=6, decimal_places=2)
class RevenueInfo(models.Model):
total_amount_spent = models.DecimalField(max_digits=6, decimal_places=2, default=0)
total_revenue = models.ForeignKey(ExtendedProfile, null=True)
class Product(models.Model):
category = models.CharField(max_length=100)
name = models.CharField(max_length=100)
description = models.TextField()
#photo = models.ImageField()
price_CAD = models.DecimalField(max_digits=6, decimal_places=2)
quantity = models.DecimalField(max_digits=2, decimal_places=0, null=True)
How could I go about this? Would I iterate of each Usermodel and find User.amount_spent then add that to RevenueInfo.total_revenue? I'm not sure how to put that into code. Also I'm pretty sure I don't need both total_amount_spent and total_revenue but I feel like I need a ForeignKey
You could add a classmethod to the ExtendedProfile model to aggregate the amount_spent value for each User (which bypasses the need for a separate RevenueInfo model):
from django.db.models import Sum
class ExtendedProfile(models.Model):
....
#classmethod
def total_user_spend(cls):
return cls.objects.aggregate(total=Sum('amount_spent'))
Then you can get the total spend using ExtendedProfile.total_user_spend():
>>> ExtendedProfile.total_user_spend()
{'total': Decimal('1234.00')}
Yes, you can write a method for that in your model. There are 2 ways for it.
1) Writing a method that calculates the values and sets it to a instance value.
2) Writing a method that calculates the value and directly returns it.
For example purpose, here is the code for 2nd type.
# models.py
def total_amount_spent(self):
temp_values = [int(user.amount_spent) for user in ExtendedProfile.objects.all()]
return sum(temp_values)
And for using that value in views , but remeber it would be an integer by default
#views.py
value = RevenueInfo.total_amount_spent()
Avoid iterating over database entities in python (it can get really slow). Look into aggregation, it allows you to efficiently get sum (average, max, min, etc...) of values in a database:
>>> from django.db.models import Sum
>>> ExtendedProfile.objects.all().aggregate(Sum('amount_spent'))
{'amount_spent__sum': Decimal('1234.56')}
>>> # ... do whatever you want with the return value

Categories

Resources