Django: Annotating price to row, then grouping rows by field, and annotating price sums to groups - python-3.x

My situation: Working on a delivery thingie. I have a Customer, which has one price sheet. The price sheet has pricing zones that are based on postal codes. The customer has DeliveriesOrders, which can have multiple Deliveries (pickups, drop offs) and the price is calculated according the postal code.
The way I calculate prices to Delivery rows is:
sheet_zones = customer.price_sheet.price_sheet_zones.filter(postal_codes__code=OuterRef("postal_code"))
delivery_orders = DeliveryOrder.objects.select_related("customer") \
.prefetch_related(
Prefetch(
'deliveries',
queryset=Delivery.objects.annotate(price=Subquery(sheet_zones.values("price"))).order_by("-is_pickup"),
to_attr="priced_deliveries"
)
)
This enables me to have a price annotated for each Delivery row.
I however cannot annotate a Prefetch-field, as it results in an error, so .annotate(Sum("priced_deliveries")) doesn't work.
I am scratching my head quite hard to get a Sum of all deliveries in a delivery_order. But even more I am scratching my head how to group all deliveries by a field called "reference", and Sum all delivery prices per reference.
Pertinent models:
class DeliveryOrder(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, related_name="delivery_orders")
class Delivery(models.Model):
delivery_order = models.ForeignKey(DeliveryOrder, on_delete=models.SET_NULL, null=True, related_name="deliveries")
is_pickup = models.BooleanField(default=True)
reference = models.CharField(max_length=64, blank=True, null=True)
postal_code = models.IntegerField()
Customer related models:
class PriceSheet(models.Model):
name = models.CharField(max_length=255)
class Customer(models.Model):
name = models.CharField(max_length=255)
price_sheet = models.ForeignKey(PriceSheet,
on_delete=models.SET_NULL, blank=True, null=True, related_name="customers")
class PriceSheetZoneItem(models.Model):
price_sheet = models.ForeignKey(PriceSheet, on_delete=models.SET_NULL, null=True, related_name="price_sheet_zones")
zone = models.ForeignKey(Zone, on_delete=models.SET_NULL, null=True, related_name="price_sheet_zones")
postal_codes = models.ManyToManyField(PostalCode, related_name="price_sheet_postal_codes")
price = models.DecimalField(decimal_places=2, max_digits=9)
Postal code related:
class Town(models.Model):
name = models.CharField(max_length=64)
class Zone(models.Model):
name = models.CharField(max_length=32)
class PostalCode(models.Model):
code = models.IntegerField()
town = models.ForeignKey(Town, on_delete=models.SET_NULL, null=True, related_name="postal_codes")
I am open to all and any suggestions and help. I may very well be trying to do this the wrong way.
Using Django 3.2
Thank you in advance!

First, In the Delivery model, should postal_code be a ForeignKey to the PostalCode model? In the below I am going to assume that it is.
I think you want to annotate using a subquery. In SQL this looks something like:
SELECT
delivery_order.*
(SELECT SUM(<price computation from a single delivery>)
FROM delivery
JOIN <join to price sheet table so you can calculate price>
WHERE delivery.delivery_order_id=delivery_order.id
) AS total_price;
FROM
delivery_order;
And your ORM code is something like:
delivery_orders = DeliveryOrder.objects.annotate(
total_price=Subquery(
Delivery.objects.values(
'delivery_order' # This is necessary to get the proper group by
).annotate(
'customer_id'=OuterRef('id')
).annotate(
price=Sum(postal_code__pricesheetzoneitem__price)
).filter(
delivery_id=OuterRef('id'),
postal_code__pricesheetzoneitem__pricesheet__customer_id=OuterRef('id')
).values(
'price' # This is necessary to select only one value
)
)
)
This might be simpler using the django-sql-utils package
from sql_util.aggregates import SubquerySum
delivery_orders = DeliveryOrder.objects.annotate(
total_price=SubquerySum('delivery__postal_code__pricesheetzoneitem__price',
filter=Q(pricesheet__customer_id=OuterRef('id'))
)
)
I can't test this exact code, so I'm not sure it's 100% correct.
Edit: For postal_code not being a ForeignKey
First if you are storing the actual digits of the postal code, e.g. 90210, in the postal_code field, it should be a string, not an integer. Some postal codes start with 0.
You have a couple options. One is to make two fields, postal_code_string that stores the raw postal code info in case there is an error and postal_code which is a ForeignKey to PostalCode.
Another option is to use a JoinField. This allows you to keep everything the same, but the ORM can do the join on the postal_code field. JoinField is a little off topic for this question

Related

In Django, how to create a model that stores proposed changes to another model?

I'm using Python 3.9 and Django 3.2. I have the following model
class Coop(models.Model):
objects = CoopManager()
name = models.CharField(max_length=250, null=False)
types = models.ManyToManyField(CoopType, blank=False)
addresses = models.ManyToManyField(Address, through='CoopAddressTags')
enabled = models.BooleanField(default=True, null=False)
phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
web_site = models.TextField()
description = models.TextField(null=True)
approved = models.BooleanField(default=False, null=True)
We would like to set up a situation where someone could propose a change to a row in the db that would be reviewed before being saved, so I've created this struture
class CoopChange(Coop):
"""
"""
created_at = models.DateTimeField(null=False, default=datetime.now)
The problem is that when I create a migration, the table that is created simply points back to the original model, instead of storing all the fields
Table "public.directory_coopchange"
Column | Type | Modifiers
-------------+--------------------------+-----------
coop_ptr_id | integer | not null
created_at | timestamp with time zone | not null
This is non-ideal because the original table would contain both finalized entries and those suggested for changes. Is there a way to create an entity that stores proposed changes that mirrors the structure of the original entity?
There are a few of ways you could tackle this
Change Coop to an abstract base class:
https://docs.djangoproject.com/en/4.0/topics/db/models/#abstract-base-classes
Then you can inherit two classes from it CoopProposed and CoopApplied and you can work out what logic happens to create each
Just use the one class Coop and add a property called approved defaulted to False and then set it to True when you approve the changes.
This might not be what you want as it will overwrite any previous changes and you might want to keep a history of changes.
Add a property proposed as a JSONField (check the docs if the DB you are using supports it) on the Coop class. This can store a whole dictionary with the keys and values of the proposed changes. When approved, the field can be read and applied to the model's properties in a function or something like that.
class Coop():
objects = CoopManager()
name = models.CharField(max_length=250, null=False)
types = models.ManyToManyField(CoopType, blank=False)
...
proposed = models.JSONField("Proposed Changes", null=True)
def apply_proposed_changes(self):
proposed = self.proposed
self.name = proposed.get('name')
for type in proposed.get('types'):
self.types.add(CoopType.objects.get(name=type))
...
In any case if you're trying to inherit all the properties from Coop you need to understand how Django model inheritance work and all the caveats before you get bitten later down the track:
https://docs.djangoproject.com/en/4.0/topics/db/models/#model-inheritance

Two properties that relate to each other in another model

Sorry that the title might be confusing but I'm not native english speaker and very new to django terms.
I have a scenario like this: A department can have many branches. I have a student model where he has two properties, Department and Branch.
When I select his department , I want it to accept (and show in admin panel) only the branches that are related to that department , my code so far is this:
class Entity(models.Model):
id = models.UUIDField(primary_key=True , default = uuid.uuid4, editable=False)
class Department(Entity):
name = models.CharField(max_length=100, null=False)
class Branch(Entity):
name = models.CharField(max_length=100, null=False)
dep = models.ForeignKey(Department, related_name='branches', on_delete=models.CASCADE)
class Student(Entity):
#Some Fields here
department = models.ForeignKey(Department, related_name='students', on_delete=models.CASCADE)
branch = models.ForeignKey(Branch, related_name='students', on_delete=models.CASCADE)
Assuming I have a 2 departments (CE and CS), CE has 2 branches and CS has 3 branches , What I want is, when I choose a student's department in the admin panel, I want the branches to be shown (to select from) only the one that exists on that department , what I'm getting is 5 branches (in this example).
How can I solve this ?
NOTE: I haven't played with anything related to the admin panel except registering the models.
Thanks in advance and sorry if the title or any other part is not very correct.
There are two solutions:
override save() function and check branch.
Check branch inside form by overriding clean_branch()
It's better to implement both.

how to make a field unique based on another filed in django models?

I want to make a field unique based on another field in the same model, this is my model:
class Shop(models.Model):
name = models.CharField(max_length=50)
class Product(models.Model):
name = models.CharField(max_length=50, unique=True)
shop = models.ForignKey(Shop, on_delete=models.CASCADE)
I want the product's name to be unique only based on Shop, for example, if we have the product a from shop a, shop a can not make another product with the name a but shop b can make a product with name a.
for example we have name = models.CharField(unique_for_date=date_field) in models, which make the name unique for the date at date_field.
is there anything like unique_for_date?
can I handle this operation in models or I should try to handle it in view or form?
On your Product table:
class Product(...):
...
class Meta:
unique_together = ('shop', 'name')
This will ensure Products must have a unique name across the Shop they are related to.

How do I add 3 different licenses with 3 different prices in django Models

I have a product and I wanted to add three purchase options that depend on the license you buy.
For example:
class Product(models.Model):
productname = models.CharField(max_length=200)
license = models.????
There should be a multiselector here in order to select all three
licenses when applicable. But how?
discription = models.TextField()
image = models.CharField(max_length=5000, null=True, blank=True)
class License(models.Model):
Here should be some kind of choice field that connects the price to
the choice.... but how???
Help please.
I tried this and it didn't work
class Song(models.Model):
class License(models.IntegerChoices):
BASIC = 35
PREMIUM = 50
PROFESSIONAL = 150
FREE = 0
license = models.IntegerField(choices=License.choices, default='License.FREE')
album = models.ForeignKey(Album, on_delete=models.CASCADE)
artist = models.ForeignKey(Artist, blank=True, on_delete=models.CASCADE)
title = models.CharField(max_length=254)
description = models.TextField()
First of all, you need to keep Product outside of licenses as it will be same for all pricing models.
You can store license info for user in the User model directly or in some custom model that is assigned to the user as OneToOne and contains extra fields for that user.
For pricing, you will just define different prices for each license and product. To make it simpler, you can add ProductPrice as an inline for the Product admin.
class Product(models.Model):
...product related fields here like name and etc.
default_price = models.FloatField("Default price", default=0.0)
def user_price(self, user):
"""Will return ProductPrice model or None if there is no results."""
return self.prices.filter(license=user.license).first()
class License(models.Model):
"""
I would recommend using a custom user model or OneToOne model where you can
assign which license the user have and use `user.license` for that license
value or user.information.license if you use OneToOne model.
"""
...fields related to the License definition only.
class ProductPrice(models.Model):
product = models.ForeignKey(
Product, on_delete=models.CASCADE, related_name="prices"
)
license = models.ForeignKey(
License, on_delete=models.CASCADE, related_name="prices"
)
price = models.FloatField("Price", default=0.0)
...other fields related to license pricing if you need
Thank you everyone that responded. the solution was actually pretty straight forward. It was just like Emin said. I need to make licensing it's own class and a product pricing class with licensing and price. So thanks everything is working

join the results of two query on each others with Django

I have 2 models, i simplify them for the example:
class CustomerOrder(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT)
isPaid = models.BooleanField(default=False)
and
class EventParticipant(models.Model):
customerOrder = models.ForeignKey(CustomerOrder, on_delete=models.CASCADE)
event = models.ForeignKey(Product, on_delete=models.CASCADE)
What i need to do is to display in a table every participants for an event but link the order to the participant so i can display the isPaid status for each participant.
I think its similar to a join on SQL.
So i tried something like:
participants = EventParticipant.objects.filter(event=event_pk).select_related('customerOrder')
but when i try to access it like
participants.cusomerOrder
i get: 'QuerySet' object has no attribute 'customerOrder'
so i guess is missunderstand something.
Thanks
participants is an EventParticipant QuerySet, whcih is an iterable, So you need to iterate over it
for participant in participants:
print(participant.customerOrder.isPaid)

Resources