I am using Django to sort out a storefront environment to handle orders and am struggling on one of the annotations I am attempting to write.
The salient data models are as such
class Order(ClusterableModel):
"various model fields about the status, owner, etc of the order"
class OrderLine(Model):
order = ParentalKey("Order", related_name="lines")
product = ForeignKey("Product")
quantity = PositiveIntegerField(default=1)
base_price = DecimalField(max_digits=10, decimal_places=2)
class OrderLineOptionValue(Model):
order_line = ForeignKey("OrderLine", related_name="option_values")
option = ForeignKey("ProductOption")
value = TextField(blank=True, null=True)
price_adjustment = DecimalField(max_digits=10, decimal_places=2, default=0)
The OrderLine represents one or more of a specific product being bought at a specific base price and quantity. This base price has been copied from the product model in order to preserve the price of the product at the time the order was created.
The Order therefore is just a collection of multiple OrderLines
The complexity comes in the OrderLineOptionValue model which represents a modification to the base price based on the choices made by the user, and each order line may have multiple adjustments if the product has multiple options. Color, size, weight, etc may each have variable price impacts.
When querying the OrderLine model I have been able to successfully annotate each result with that line's appropriate line total of (base+sum(price_adjustments))*quantity using the following query:
annotation = {
"line_total": ExpressionWrapper((F("base_price")+Coalesce(Sum("option_values__price_adjustment", output_field=DecimalField(max_digits=10, decimal_places=2)), Value(0)))*F("quantity"), output_field=DecimalField(max_digits=10, decimal_places=2)),
}
OrderLine.objects.all().annotate(**annotation)
This annotation appears to work correctly for all of the tests I've tried throwing at it. Of note, OrderLines may have no price_adjustments, thus the Coalesce.
My problems start when trying to annotate each order with it's grand total summing all of it's respective line items together. My initial attempt resulted in the exception Cannot compute Sum('line_total'): 'line_total' is an aggregate which I can only assume is indeed an illegal SQL request, as my practical knowledge of SQL is a bit rusty.
lineItemSubquery=OrderLine.objects.filter(order=OuterRef('pk')).order_by()
#the same annotation as above
lineItemSubquery=lineItemSubquery.annotate(**annotation).values("order")
Order.objets.all().annotate(annotated_total=Coalesce(Subquery(lineItemSubquery.annotate(sum_total=Sum("line_total")).values('sum_total')), 0.0))
After stumbling across this question I attempted to restructure it a bit, and while I was able to get it to return a number, it does so incorrectly, appearing to return only the first line_total for each order.
lineItemSubquery=OrderLine.objects.filter(Q(order=OuterRef("pk"))).annotate(**annotation).values("line_total")
Order.objects.all().annotate(annotated_total=Coalesce(Subquery(lineItemSubquery), 0.0))
By slicing the lineItemSubquery [1:2] the annotation also works but then calculates out to the second line item's amount ignoring any other line items. I assume this is a byproduct of the referenced question and how they're requesting the maximum (first result as ordered) of a set of values instead of a Sum of the entire dataset.
My instinct says that I need to find a way to Sum() the Subquery, or that due to the multi-level aspect, I need some sort of additional OuterRef to bridge the relationships between all three models? I'm seriously considering simply caching each OrderLine's calculated total directly in a model field for the purpose in order to avoid the problem entirely.