Queryset Union in Django - python

I'm trying to do a simple double entry accounting with Django. Every transaction is currently one object with debit and credit accounts being foreign keys. Now I'm trying to get all transaction objects out so that there's debit and credit object for every transaction and I can easily order and represent every account's transactions and totals in the template. So every transaction object needs to have debit object and credit object in the final outcome. I think in MySQL you could achieve this with something like:
SELECT date, description, amount, debit_account AS account, debit AS 1 FROM Transaction
UNION
SELECT date, description, amount, credit_account AS account, debit AS 0 FROM Transaction
What is the right way to do this with Python's objects in Django? Union seems to override the changes within the for loop I currently have and I end up with no distinction between debit and credit.
models.py
class Transaction(models.Model):
date = models.DateField()
description = models.CharField(max_length=150)
debit_account = models.ForeignKey('ledger.Account', related_name='debit_account', blank=True, null=True, on_delete=models.CASCADE)
credit_account = models.ForeignKey('ledger.Account', related_name='credit_account', blank=True, null=True, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
debit_in_ledger = models.BooleanField(default=False)
views.py
def ledger(request):
transaction_list_debit = Transaction.objects.all()
transaction_list_credit = Transaction.objects.all()
for a in transaction_list_debit:
a.debit_in_ledger = True
transaction_list = transaction_list_debit.union(transaction_list_credit, all=True).order_by('debit_account', 'public_date')
return render(request, 'pages/ledger.html', {'transaction_list' : transaction_list})

You can do this as follows:
from django.db.models import BooleanField, F, Value
q1 = Transaction.objects.annotate(
account = F('debit_account')
debit = Value(True, output_field=BooleanField())
)
q2 = Transaction.objects.annotate(
account = F('credit_account')
debit = Value(False, output_field=BooleanField())
)
qs = q1 | q2
where qs is thus our "final" queryset. This QuerySet will contain Transaction objects, and every Transaction object in the queryset will have two extra attributes: account and debit. Note that other attributes like debit_account, etc. will still exist. Furthermore every real Transaction in the database will thus occur twice: once from q1, and once from q2.
Furthermore the .account will contain the primary key of the related Account, so not a reference to the Account. You can then fetch the Account with Account.objects.get(pk=some_transaction.account).

Related

Django Get Last Object for each Value in List

I have a model called Purchase, with two fields, User and amount_spent.
This is models.py:
class Purchase(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
amount_spent = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
I want to get the last purchases from a list of users.
On views.py I have a list with some User's objects, and I want to get the last purchase for each user in the list. I can't find a way of doing this in a single query, I checked the latest() operator on QuerySets, but it only returns one object.
This is views.py:
purchases = Purchase.objects.filter(user__in=list_of_users)
# purchases contains all the purchases from users, now I need to get the most recent onces for each user.
I now I could group the purchases by user and then get the most recent ones, but I was wondering it there is a way of making this as a single query to DB.
try this:
Purchase.objects.filter(user__in=list_of_users).values("user_id", "amount_spent").order_by("-id").distinct("user_id")
You can annotate the Users with the last_purchase_pks and then fetch these and adds that to these users:
from django.db.models import OuterRef, Subquery
users = User.objects.annotate(
last_purchase_pk=Subquery(
purchase.objects.order_by('-created_at')
.filter(user_id=OuterRef('pk'))
.values('pk')[:1]
)
)
purchases = {
p.pk: p
for p in Purchase.objects.filter(
pk__in=[user.last_purchase_pk for user in users]
)
}
for user in users:
user.last_purchase = purchases.get(user.last_purchase_pk)
After this code snippet, the User objects in users will all have a last_purchase attribute that contains the last Purchase for that user, or None in case there is no such purchase.

Django: query filter

I have two models that are related: one is a list of participants. The other is a list of times they have checked in or out of an office.
The table (Checkin) has one record for every checkin/checkout pair. So, there can be many records for any participant.
How can I retrieve only the very last (most recent) record for a participants checkin and then pass the participant and only that most recent Checkin record to my template?
From what I can tell there's no ability to do something like a last() in my template, so how would I go about filtering to get just that single record?
Thank you.
Models:
class Participant(models.Model):
first_name = models.CharField(max_length=50)
middle_initial = models.CharField(max_length=50, blank=True)
class CheckIn(models.Model):
adult = models.ForeignKey(
Participant, on_delete=models.CASCADE, blank=True, null=True, related_name='adult_checkin')
checkin = models.DateTimeField(blank=True, null=True)
checkout = models.DateTimeField(blank=True, null=True)
View snipit:
p_checkins = Participant.objects.all().order_by('created')
queryset = p_checkins
context_object_name = "the_list"
template_name = 'list_of_checkins.html'
You can fetch data through most recent checkin or checkout.
For checkin :
p_checkins = CheckIn.objects.all().order_by('-checkin')[0]
For checkout :
p_checkins = CheckIn.objects.all().order_by('-checkout')[0]
To get the participant name by :
name = p_checkins.adult.first_name
When you use (-) your latest update will be query from database.
p_checkins = CheckIn.objects.all().order_by('-checkin')
or
p_checkins = CheckIn.objects.all().order_by('-checkout')
you can annotate the latest value via a subquery to the participant
from django.db.models import OuterRef, Subquery
checkin_q = CheckIn.objects.filter(adult=OuterRef('pk')).order_by('-checkin')
queryset = Participant.objects.annotate(last_checkin=Subquery(checkin_q.values('checkin')[:1]))
see https://docs.djangoproject.com/en/4.0/ref/models/expressions/#subquery-expressions
Most of the answers so far are correct in several aspects. One thing to note is that if your check_in or check_out values (whichever you use) isn't chronological (and by "most recent", you mean the last added), you'll want to add a created_at datetime field with auto_now option True, or order by the pk.
In addition to the other answers provided and my comment above, you can also get the most recent check in by using the related manager on the participant object.

Count & Sum of Order Values for each customer (through iteration) in Django

I have Customer & Order models as below:
class Customer(models.Model):
name = models.CharField(max_length = 100)
city = models.CharField(max_length = 100)
class Order(models.Model):
value = models.FloatField()
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
Now I would like to generate a table of (distinct) customers along with a count of number of orders placed by each of them and the sum of values of those orders. I tried this in the views.py:
def customers(request):
customer_orders = Order.objects.distinct().annotate(Sum('value'))
Then in my html template, I tried the following:
<ul>
{% for customer in customer_orders %}
<li>{{customer.customer}} - {{customer.value__sum}}<li>
{% endfor %}
</ul>
After all this, instead of getting unique customers (and respective order records), I'm getting a list of all orders and customers are getting repeated (as shown below). Not sure what I'm missing here.
Bosco-Ward - 16,700.0
Ernser PLC - 51,200.0
Murphy Ltd - 21,400.0
Kohler-Veum - 29,200.0
Schmidt-Legros - 96,800.0
Brown-Weissnat - 8,200.0
Bosco-Ward - 36,400.0
Ernser PLC - 66,600.0
Murphy Ltd - 84,200.0
Also wanted to know if there's a possibility to generate a table of city names with order count and total value of orders received from that city (note that my order model doesn't have city field).
Since you want a queryset of Customer instances make your query on the Customer model itself instead of on Order, next I believe you will not need to use distinct here since the customer instances should be considered unique. Hence, you can make a query like:
from django.db.models import Count, Sum
customers = Customer.objects.annotate(order_count=Count('order'), order_value_sum=Sum('order__value'))
for customer in customers:
print(customer.name, customer.order_count, customer.order_value_sum)
Kindly note im typing the solution from my phone without testing it but this is what i think:
Give a related name to customer in Order model:
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name=“orders”)
Get orders count:
Customer.objects.get(id=1).orders.count()
Or access this attribute in template something like:
{{ customer.orders.count }}
Get orders count values by city
From django.db.models import Count, Sum
Customer.objects.values(“city”).annotate(order_count=Count(“orders”)).annotate(totals=Sum(“value”))
Add realted_name to Order customer field:
class Customer(models.Model):
name = models.CharField(max_length = 100)
city = models.CharField(max_length = 100)
class Order(models.Model):
value = models.FloatField()
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='orders')
query:
Customer.objects.values(“city”).annotate(order_count=Count(“orders”)).annotate(totals=Sum(“orders__value”))

Django count with group in annotation

I have a profile model with a ManyToMany field called that relates to a Stock. In my admin dashboard I'm trying to show the number of watchlists each stock is in. The annotation query I have is:
qs.annotate(watchlist_count=Count('profile__watchlist__symbol'))
But it's returning incorrect results
Here are the models:
class Profile(models.Model):
user = OneToOneField(User, on_delete=models.CASCADE)
watchlist = ManyToManyField(Stock, blank=True)
def __str__(self):
return self.user.email
class Stock(models.Model):
symbol = CharField(max_length=15, unique=True)
name = CharField(max_length=100)
category = CharField(max_length=30, choices=CATEGORY_CHOICES, blank=True)
about = TextField(help_text='About this company')
def __str__(self):
return f'{self.symbol} - {self.name}'
The equivalent SQL query is:
select stock_id, count(stock_id) from api_profile_watchlist group by stock_id;
What is wrong with my annotation query?
You do too much joins. By joining twice over the many-to-many relation, you "blow up" the count.
You can simply count the amount of watchlists with that Stock, with:
from django.db.models import Count
Stock.objects.annotate(
watchlist_count=Count('profile')
)
This works since, by default, the related_query_name=… [Django-doc] has the name of the model (or the related_name if you specified one). So the implicit relation you wrote from Stock to Profile (the reverse one of Profile to Stock in your watchlist relation), is profile (in lowercase). We thus ask Django to count, for a given Stock object, the number of relations to a Profile.

Save a ForeignKey "child" without saving the "parent" first?

Suppose I have a view for saving an order to a database based on cart contents:
def cart_checkout(request):
order = Order()
order.first_name = 'x'
order.last_name = 'y'
order.address = 'z'
order.save()
cart = Cart(request)
for product_id, product_quantity in cart:
product = Product.objects.get(pk=product_id)
order_item = OrderItem()
order_item.order = order
order_item.name = product.name
order_item.price = product.price
order_item.amount = product_quantity
order_item.save()
order.update_total_price() # updates the Order total price field with the sum of order items prices
order.save()
return HttpResponse('Checked-out!')
As you can see, I am calling order.save() twice in this view: first to create an Order instance the OrderItems can be attached to in the for loop, and then to update the total price of the order based on order items in it. If I removed the first .save(), I would get an error on the second one telling me the order needs to be saved first.
Calling the .save() method twice does not seem DRY enough to me. Is there a way to do it only once?
Note that I am not subclassing ModelForm, so I cannot use .save(commit=False). Also, I do not want to just hide the save() method in the update_total_price() method.
Models.py:
from django.db import models
from .mixins import DisplayNameMixin
class Product(DisplayNameMixin, models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=6, decimal_places=2)
amount = models.IntegerField()
class Order(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
address = models.CharField(max_length=255)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
def update_total_price(self):
order_items = self.orderitem_set.all()
self.total_price = sum([
x.price * x.amount
for x in order_items
])
class OrderItem(models.Model):
order = models.ForeignKey('Order', on_delete=models.CASCADE)
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=6, decimal_places=2)
amount = models.IntegerField()
I think, you can't help but save the order twice, as you need to have an order_id to create the OrderItems, and then update the order with the items' amount.
I have a few suggestions to make though.
You can make total_price a calculated property, so that you would not have to save the order:
class Order(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
address = models.CharField(max_length=255)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
#property
def total_price(self):
return sum([
x.price * x.amount
for x in self.orderitem_set.all()
])
From DB theory perspective your DB structure is wrong. It needs to be normalized first.
Why it is wrong?
Order.total_price is redundant table column. That information can be found with aggregation. At DB level there are no protections preventing DB users (Django app in your case) from entering compromised data. So your DB can be telling two different total prices (Order.total_price != SUM(OrderItem.price * OrderItem.amount)) at the same time.
So to appease DB normalization gods you need to drop total_price field and use Django aggregations/annotations (https://docs.djangoproject.com/en/3.0/topics/db/aggregation/) when you need to access it.
Saying that, there could be a totally valid reason to put total_price inside Order table. That reason usually is performance. Sometimes SQL query complexity (It is very annoying to filter by an aggregated column).
But there is a price. And that price is de-normalization of your DB. In your case you are paying breaking DRY principle.
Just make sure that you are calling both save()'s in the same transaction.
To expand on petraszd's answer (i.e. remove the total_price field) and engin_ipek's answer (i.e. add total_price as a calculated property), you could try making total_price a cached property, to avoid calculating the same value more than once - as long as the same Order instance is passed around.
You would also probably make the calculation a little less expensive if you used aggregation to calculate the total price, as petraszd mentioned, e.g. adding the products of price and amount.

Categories