Как аннотировать измененную таблицу связанных объектов в Django
У меня есть небольшое бизнес-приложение, которое можно упростить в виде следующих моделей:
class Client(..):
name = CharField(...)
class Sale(..):
client = ForeignKey(Client, ...)
item = CharField(...)
time = DateTimeField(...)
value = DecimalField(...)
class Receive(..):
client = ForeignKey(Client, ...)
time = DateTimeField(...)
value = DecimalField(...)
Теперь мне нужен список клиентов, отображающий всех клиентов с общим объемом продаж, платежами, стоимостью дебиторской задолженности и самой ранней датой неоплаченных продаж ("edsu"), которую не может покрыть платеж.
Например, Клиент A
- 2022-01-01: купил предмет X за $10,
- 2022-02-15: купил предмет Y за $15,
- 2022-02-25: купил предмет X за $10, и
- 2022-03-10: заплатил $12.
Тогда клиент А имеет дебиторскую задолженность в размере $23 и edsu на 2022-02-15.
До сих пор я использовал сырой sql (как показано ниже) для выполнения запроса, и он работает хорошо.
def client_list_view(request):
...
clients = Client.objects.raw(
raw_query = '''WITH app_balance AS (
SELECT id, client_id, item, time, val,
SUM(sale) OVER wCN0 - SUM(receive) OVER wClt AS unpaid
FROM (
SELECT id, client_id, item, time, val AS sale, 0 AS receive
FROM app_sale
UNION SELECT id, client_id, '' AS item, time, 0 AS sale, val AS receive
FROM app_receive
) app_balance
WHERE time < %(te)s::TIMESTAMPTZ
WINDOW wClt (PARTITION BY client_id),
wCN0 (wClt ORDER BY time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
ORDER BY time
)
SELECT id, name, sales, receive, receivable, edsu
FROM app_client LEFT JOIN (
SELECT client_id,
SUM(sale) FILTER (WHERE time > %(te)s::TIMESTAMPTZ) AS sales,
SUM(receive) FILTER (WHERE time > %(te)s::TIMESTAMPTZ) AS receive,
SUM(sale-receive) AS receivable,
MIN(time) FILTER (WHERE unpaid > 0) AS edsu
FROM app_balance
GROUP BY client_id
) client_balance ON client_balance.client_id = app_client.id;''',
params = {'ts': time_start.isoformat(), 'te': time_end.isoformat()}
)
return render(request, 'app/clients.html', {'clients': clients})
Однако, по мере роста приложения, его становится все труднее поддерживать, что подталкивает меня к решению отказаться от raw sql и перейти на Django-стилизованный QuerySet, начиная со следующего SQL представления
CREATE VIEW app_balance_view AS
SELECT
id, 1 AS cat, client_id, item, time, val AS sale, 0 AS rcv
FROM app_sale
UNION SELECT
id, 2 AS cat, client_id, '' AS item, time, 0 AS sale, val AS rcv
FROM app_receive;
и неуправляемая модель
class Balance(..):
cat = IntegerField(choices=CATS, ...)
client = ForeignKey(Client, ...)
item = CharField(...)
time = DateTimeField(...)
sale = DecimalField(...)
rcv = DecimalField(...)
class Meta:
managed = False
db_table = 'app_balance_view'
Я застрял здесь, не найдя способа аннотировать клиента с пользовательским балансом QuerySet возможно следующим образом:
balance = Balance.objects.filter(time__lt=time_end).annotate(
total_sale = Window(Sum('sale'), partition_by=F('client_id')),
cur_paid = Window(Sum('rcv'), partition_by=F('client_id'), frame=RowRange(None,0))
).annotate(unpaid = F('total_sale') - F('cur_paid'))
настолько, что я могу сделать
clients = Client.objects.annotate(
edsu = Min('balance__time', filter=Q(balance__unpaid__gt=0)),
)
Если вышеуказанное невозможно (или не рекомендуется), что может быть альтернативой для достижения этой цели?
Окружающая среда:
- python == 3.10.2
- Django == 4.0.1
- PostgreSQL == 14
Я нашел решение, используя следующую функцию представления:
CREATE VIEW app_bal_view AS
SELECT
id, cat, client_id, time, item, sale, rcv,
SUM(sale) OVER wcn0 - SUM(rcv) OVER wclt AS unpaid
FROM (
SELECT
id, 1 AS cat, client_id, time, item, value AS sale, 0 AS rcv
FROM app_sale
UNION SELECT
id, 2 AS cat, client_id, time, ‘’ AS item, 0 AS sale, value AS rcv
FROM app_receive
) app_balance_tmp
WINDOW wclt AS (PARTITION BY client_id),
wcn0 AS (wclt ORDER BY time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
добавление поля unpaid к модели Balance
class Balance(..):
..
unpaid = DecimalField(..)
и используя тот же клиентский QuerySet, что и в Question.