Аннотирование на основе связанной области, с фильтрами

class IncomeStream(models.Model):
    product = models.ForeignKey(Product, related_name="income_streams")
    from_date = models.DateTimeField(blank=True, null=True)
    to_date = models.DateTimeField(blank=True, null=True)
    value = MoneyField(max_digits=14, decimal_places=2, default_currency='USD')
    
    
class Product(models.Model):
    ...
    
    
class Sale(models.Model):
    product = models.ForeignKey(Product, related_name="sales")
    created_at = models.DateTimeField(auto_now_add=True)
    ...
    

Предположим, что я хочу добавить значение к некоторым Sale, используя .annotate.

Это значение называется cpa (cost per action): cpa - это значение IncomeStream, чьи from_date и to_date включают Sale created_at в свой диапазон.

Кроме того, from_date и to_date являются nullable, и в этом случае мы предполагаем, что они означают бесконечность.

Например:

<IncomeStream: from 2021-10-10 to NULL, value 10$, product TEST>
<IncomeStream: from NULL to 2021-10-09, value 5$, product TEST>

<Sale: product TEST, created_at 2019-01-01, [cpa should be 5$]>
<Sale: product TEST, created_at 2021-11-01, [cpa should be 10$]>

Мой вопрос: можно ли написать все эти условия, используя только Django ORM и аннотацию? Если да, то как?

Я знаю, что объекты F могут обходить отношения следующим образом:

Sale.objects.annotate(cpa=F('product__income_streams__value'))

Но тогда где именно я могу написать всю логику, чтобы определить, из какого именно income_stream он должен выбрать value?

Предположим, что ни один поток доходов не имеет пересекающихся дат для одного и того же продукта, поэтому вышеупомянутые спецификации никогда не приводят к конфликтам.

Что-то вроде этого должно помочь вам начать

    subquery = (
        IncomeStream
        .objects
        .values('product') # group by product primary key i.e. product_id
        .filter(product=OuterRef('product'))
        .filter(from_date__gte=OuterRef('created_at'))
        .filter(to_date__lte=OuterRef('created_at'))
        .annotate(total_value=Sum('value'))
    )

Затем с помощью подзапроса

    Sale
    .objects
    .annotate(
        cpa=Subquery(
            subquery
        ).values('total_value') # subquery should return only one row so
        # so just need the total_value column
    )

Не имея возможности самому поиграть с этим в оболочке, я не уверен на 100%. В любом случае, это должно быть близко.

Вернуться на верх