Я некоторое время искал по разным углам inte rnet и Django Проектной документации без успеха, поэтому вот мой вопрос:
В качестве фона я синхронизирую модели с внешним API, поэтому я бы предпочел не менять структуру модели.
У меня есть следующие модели (просто для краткости оставил соответствующие поля):
TimeEntry:
class TimeEntry(models.Model):
id = CharField(primary_key=True, max_length=255)
duration_decimal = DecimalField(max_digits=5, decimal_places=2, verbose_name='dec duration')
start = DateTimeField()
project = ForeignKey(Project, models.SET_NULL, blank=True, null=True)
user = ForeignKey(User, models.SET_NULL, blank=True, null=True)
Членство
class Membership(models.Model):
user = ForeignKey(User, models.SET_NULL, blank=True, null=True)
project = ForeignKey(Project, models.SET_NULL, blank=True, null=True)
hourly_rate = ForeignKey(HourlyRate, models.SET_NULL, blank=True, null=True)
active = BooleanField(default=True)
Пользователь
class User(models.Model):
hourly_rate = DecimalField(max_digits=7, decimal_places=2, blank=True, null=True)
HourlyRate
class HourlyRate(models.Model):
amount = MoneyField(max_digits=10, decimal_places=2, blank=True, null=True, default_currency='USD')
Почасовая ставка может быть установлена на уровне пользователя и может быть переопределена на уровне членства, поэтому я пытаюсь создать запрос Django -ORM, чтобы получить значение переопределенной почасовой ставки или пользовательской почасовой в случае, если значение на уровне членства не определено, умножьте его на десятичную продолжительность, а затем агрегируйте все за месяц, чтобы сгенерировать отчет о потребленных часах и его денежном значении.
Здесь У меня есть запрос, который в настоящее время работает для почасовой ставки на уровне пользователя:
Запрос уровня пользователя
TimeEntry.objects.filter(project_id='xxxxxx')
.annotate(year=ExtractYear('start'), month=ExtractMonth('start'))\
.values('year', 'month')\
.annotate(sum=ExpressionWrapper(Sum(F('user__hourly_rate')*F('duration_decimal')), DecimalField()), hours_sum=Sum('duration_decimal'), count=Count('id'))\
.values('year', 'month', 'count', 'sum').order_by('-year', '-month')
Вывод:
+----------------------------------------------+
| Results |
+----+------+-------+------+-----------+-------+
| No | year | month | sum | hours_sum | count |
+----+------+-------+------+-----------+-------+
| 1 | 2020 | 1 | 1000 | 95 | 15 |
+----+------+-------+------+-----------+-------+
| 2 | 2019 | 12 | 990 | 87 | 9 |
+----+------+-------+------+-----------+-------+
| 3 | 2019 | 11 | 1250 | 113 | 21 |
+----+------+-------+------+-----------+-------+
| | ... | | | | |
+----+------+-------+------+-----------+-------+
Это моя попытка получить переопределенные значения из модели членства
- Запрос уровня членства
TimeEntry.objects.filter(project_id='xxxxxx')
.annotate(year=ExtractYear('start'), month=ExtractMonth('start'))\
.values('year', 'month')\
.annotate(sum=ExpressionWrapper(Sum(F('project__membership__hourly_rate__amount')*F('duration_decimal')), DecimalField()), count=Count('id'))\
.order_by('-year', '-month').values('year', 'month', 'count', 'sum')
И вот сгенерированный SQL для этого запроса:
- Уровень участия генерирует SQL
SELECT EXTRACT('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "year",
EXTRACT('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "month",
SUM(("hourlyrate"."amount" * "timeentry"."duration_decimal")) AS "sum",
COUNT("timeentry"."id") AS "count"
FROM "timeentry"
INNER JOIN "project" ON ("timeentry"."project_id" = "project"."id")
LEFT OUTER JOIN "membership" ON ("project"."id" = "clockify_membership"."project_id")
LEFT OUTER JOIN "hourlyrate" ON ("membership"."hourly_rate_id" = "hourlyrate"."id")
WHERE "timeentry"."project_id" = 'xxxxxx'
GROUP BY EXTRACT ('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich'), EXTRACT ('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')
ORDER BY "year" DESC, "month" DESC
Проблема с сгенерированным SQL заключается в том, что первые два соединения объединены и не объединены в одну (что приводит к слишком большому количеству строк), вот SQL, который дает правильный результат:
- Уровень членства генерирует SQL [исправлено]
SELECT EXTRACT('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "year",
EXTRACT('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "month",
SUM(("hourlyrate"."amount" * "timeentry"."duration_decimal")) AS "sum",
COUNT("timeentry"."id") AS "count"
FROM "timeentry"
-- START> Here is the JOIN over two columns consolidating the two previous separated JOINs
LEFT OUTER JOIN "membership" ON ("timeentry"."user_id" = "membership"."user_id" AND "membership"."project_id" = "timeentry"."project_id")
-- <END
LEFT OUTER JOIN "hourlyrate" ON ("membership"."hourly_rate_id" = "hourlyrate"."id")
WHERE "timeentry"."project_id" = 'xxxxxx'
GROUP BY EXTRACT ('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich'), EXTRACT ('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')
ORDER BY "year" DESC, "month" DESC
Я думаю, что могу придумать альтернативное решение, используя SubQuery, но тогда я не знаю, как обработать переопределение в этом сценарии.