Django - Агрегат из агрегата

Я работаю с DRF и испытываю проблемы с определением набора queryset для использования в классе представления. Предположим, у меня есть три модели, например:

class ExchangeRate(...):
    date = models.DateField(...)
    rate = models.DecimalField(...)
    from_currency = models.CharField(...)
    to_currency = models.CharField(...)

class Transaction(...):
    amount = models.DecimalField(...)
    currency = models.CharField(...)
    group = models.ForeignKey("TransactionGroup", ...)

class TransactionGroup(...):
    ...

Я хочу создать набор запросов на TransactionGroup уровне со следующим:

    1. для каждой Transaction в группе транзакций добавьте аннотированное поле converted_amount, которое умножает amount на rate в том ExchangeRate случае, когда currency совпадает с to_currency соответственно
    1. затем суммируйте converted_amount для каждого Transaction и установите это на TransactionGroup уровне как аннотированное поле converted_amount_sum

Пример json-ответа для TransactionGroup с использованием этого желаемого набора запросов:

[
  {
    "id": 1,
    "converted_amount_sum": 5000,
    "transactions": [
      {
        "id": 1,
        "amount": 1000,
        "converted_amount": 500,
        "currency": "USD",
      },
      {
        "id": 2,
        "amount": 5000,
        "converted_amount": 4500,
        "currency": "EUR",
      },
  },
  ...
]

Моя попытка построить кверисет (есть ли способ построить его на TransactionGroup уровне?):

from django.db.models import F

annotated_transactions = Transaction.objects.annotate(
    converted_amount = F("amount") * exchange_rate.rate     # <-- simplifying here
).values(
    "transaction_group"
).annotate(
    amount=Sum("converted_amount"),
)

Я могу заставить аннотации правильно работать на модели Transaction, но при попытке их повторного суммирования на уровне TransactionGroup возникает ошибка:

FieldError: Cannot compute Sum('converted_amount'), `converted_amount` is an aggregate

Для дополнительного контекста - я хочу иметь возможность сортировать и фильтровать TransactionGroups по convreted_amount_sum без необходимости выполнять дополнительные поиски/операции в БД.

Вы можете работать с выражением Subquery [Django-doc]:

from datetime import date

from django.db.models import F, OuterRef, Subquery

FILTER_DATE = date.today()

annotated_transactions = (
    Transaction.objects.values('transaction_group')
    .annotate(
        amount=Sum(
            F('converted_amount')
            * Subquery(
                ExchangeRate.objects.filter(
                    from_currency=OuterRef('currency'),
                    to_currency='USD',
                    date=FILTER_DATE,
                ).values('rate')[:1]
            )
        ),
    )
    .order_by('transaction_group')
)

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

class Currency(models.Model):
    code = models.CharField(max_length=4, primary_key=True)


class ExchangeRate(models.Model):
    date = models.DateField()
    rate = models.DecimalField()
    from_currency = models.ForeignKey(
        Currency,
        db_column='from_currency',
        related_name='exchange_from',
        on_delete=models.PROTECT,
    )
    to_currency = models.ForeignKey(
        Currency,
        db_column='to_currency',
        related_name='exchange_to',
        on_delete=models.PROTECT,
    )


class Transaction(models.Model):
    amount = models.DecimalField()
    currency = models.ForeignKey(
        Currency,
        db_column='currency',
        related_name='transactions',
        on_delete=models.PROTECT,
    )
    group = models.ForeignKey('TransactionGroup', on_delete=models.PROTECT)

Тогда мы можем работать с:

from datetime import date

from django.db.models import F, Sum

FILTER_DATE = date.today()
TransactionGroup.objects.filter(
    transaction__currency__exchange_from__exchange_to_id='USD',
    transaction__currency__exchange_from__date=FILTER_DATE,
).annotate(
    total_amount=Sum(
        F('transaction__amount') * F('transaction__currency__exchange_from__rate')
    )
)

Для этого потребуется добавить "фиктивный обменный курс", желательно для каждой валюты, такой, что USD к USD на любую дату будет 1.0. Хотя можно обойтись и без такого фиктивного курса, это усложнит задачу.

Таким образом, мы соединяем баланс транзакции с курсами валют на определенную дату, а затем суммируем преобразованные значения.

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