Как получить встроенные объекты в методе save в models.py
У меня есть класс Invoice, который (упрощенный) имеет следующие атрибуты:
class Invoice(models.Model)
number = models.CharField(verbose_name="Number", max_length=16)
issue_date = models.DateTimeField(verbose_name="Issue date", default=datetime.now)
total = models.FloatField(verbose_name="Total", blank=True, null=True)
И затем, у меня есть класс InvoiceLine, который представляет строку / строки, которые может иметь счет-фактура:
class InvoiceLine(models.Model):
invoice = models.ForeignKey(Invoice, verbose_name="Invoice")
description = models.CharField(verbose_name="Description", max_length=64)
line_total = models.FloatField(verbose_name="Line total")
InvoiceLine - это строка Invoice, и я хочу, чтобы, когда в администраторе кто-то сохранял счет-фактуру с его строками (на одну или более), вычислялась сумма счета-фактуры. Я попытался сделать это, переопределив метод save:
class Invoice(models.Model)
number = models.CharField(verbose_name="Number", max_length=16)
issue_date = models.DateTimeField(verbose_name="Issue date", default=datetime.now)
total = models.FloatField(verbose_name="Total", blank=True, null=True)
def save(self, *args, **kwargs):
invoice_lines = InvoiceLine.objects.filter(invoice=self.id)
self.total = 0
for line in invoice_lines:
self.total=self.total+line.line_total
super(Invoice, self).save(*args, **kwargs)
Проблема в том, что когда я добавляю элементы в InvoiceLine, при первом сохранении и вызове функции save новые элементы в inline (InvoiceLine) еще не сохраняются, поэтому, когда я делаю InvoiceLine.objects.filter(invoice=self.id)
, они не учитываются. Таким образом, единственный способ это работает, это экономить дважды. Я также попробовал:
def save(self, *args, **kwargs):
super(Invoice, self).save(*args, **kwargs)
invoice_lines = InvoiceLine.objects.filter(invoice=self.pk)
self.total = 0
for line in invoice_lines:
self.total=self.total+line.line_total
super(Invoice, self).save(*args, **kwargs)
Но имеет тот же результат. Любая идея? Заранее спасибо!
3 ответа
Наконец, я нашел его в сообщении Изменить объект после сохранения всех строк в Django Admin, что мне очень помогло. Ключ находится в admin.py, где у меня уже был мой класс InvoiceHeaderAdmin(admin.ModelAdmin)
, но мне пришлось поставить три функции, чтобы изменить общий атрибут после того, как все строки были сохранены: таким образом, запрос invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk)
это не сработало раньше, теперь работает отлично. Функция Tue InvoiceHeaderAdmin была следующей:
class InvoiceHeaderAdmin(admin.ModelAdmin):
inlines = [InvoiceLineInline]
list_filter = ('format_line','issue_date',)
list_display = ('number','organization','issue_date','total',)
fields = ('format_line','organization','issue_date',)
#the following functions are for calculating the total price of the invoice header based on the lines
def response_add(self, request, new_object):
obj = self.after_saving_model_and_related_inlines(new_object)
return super(InvoiceHeaderAdmin, self).response_add(request, obj)
def response_change(self, request, obj):
obj = self.after_saving_model_and_related_inlines(obj)
return super(InvoiceHeaderAdmin, self).response_change(request, obj)
def after_saving_model_and_related_inlines(self, obj):
invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk)
obj.total = 0
for line in invoice_lines:
obj.total=obj.total+line.line_total
obj.save()
return obj
Подсказка - последняя функция, где все строки были сохранены, и теперь я могу вычислить атрибут из объекта (счет-фактура) и изменить его.
Для того, чтобы поддерживать общее количество в актуальном состоянии, я выполняю агрегацию line_total, когда это необходимо. Это устраняет избыточность базы данных и еще больше предотвращает любые неточности, которые могут быть созданы несколькими людьми, обновляющими строки счета-фактуры одновременно.
class Invoice(models.Model)
@property
def total(self):
result = self.invoiceline_set.aggregate(total=Sum('line_total'))
return result['total']
Объяснение:
Для данного экземпляра Invoice вы можете следить за отношением один-ко-многим, созданным ForeignKey InvoiceLine, используя invoiceline_set. Это на самом деле эквивалентно InvoiceLine.filter(invoice_id=self.pk)
,
Когда у вас есть набор строк, вы можете предварительно сформировать агрегации базы данных, такие как SUM, AVG, COUNT, без вытягивания объектов из базы данных, что часто происходит намного быстрее.
Декоратор свойств заставит этот метод выступать в качестве атрибута только для чтения. Если вы намереваетесь вызывать общее свойство несколько раз в одном и том же экземпляре, вам следует использовать @cached_property, чтобы оно вычислялось только один раз.
Поэтому, когда вы печатаете invoice.total
в базу данных будет отправлен запрос, который отфильтрует все строки InvoiceLine, принадлежащие к взаимосвязи, определенной ForeignKey, выполнит предварительное вычисление SUM столбца line_total и вернет вам это значение в качестве ключа, например: / / "всего": 1234]
Просто какое-то обновление для других людей, которые оказались здесь: в статье, на которую вы ссылались, говорится, что здесь есть метод, который мы можем переопределить и получить тот же результат. Используя ваш пример:
def save_related(self, request, form, formsets, change):
super(AjudaCustoProjetoAdmin, self).save_related(request, form, formsets, change)
obj = form.instance
invoice_lines = InvoiceLine.objects.filter(invoice_header=obj.pk)
obj.total = 0
for line in invoice_lines:
obj.total=obj.total+line.line_total
obj.save()