diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index c531d78..251c2ac 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -1,8 +1,13 @@ from django.db import models import datetime -# Create your models here. +# ============================================================================ +# ИСПОЛНИТЕЛИ +# ============================================================================ + class TbArtist(models.Model): + """Исполнитель или музыкальная группа.""" + s_artist = models.CharField( max_length=255, db_index=True, @@ -10,33 +15,34 @@ class TbArtist(models.Model): help_text="Исполнитель или группа.", ) s_artist_html = models.TextField( - blank=True, null=True, default='', + blank=True, + null=True, + default='', verbose_name='Исполнитель (типографированно в HTML)', - help_text='Исполнитель или группа, указанные на обложке релиза, с сохранением типографирования и спецсимволов в виде HTML-тегов.', + help_text='С сохранением типографирования и спецсимволов.', ) s_slug = models.SlugField( max_length=64, + verbose_name='Слаг', ) j_artist_in_source = models.JSONField( - default=list, blank=True, null=True, - verbose_name='Исполнитель в источнике', - help_text='Список вариантов написания исполнителя, встречающихся в источниках (в разных релизах/каталогах' - ' или у разных поставщиков). Например: ["The Beatles", "Beatles", "Beatles, The"].', + default=list, + blank=True, + null=True, + verbose_name='Варианты написания в источниках', + help_text='Список вариантов: ["The Beatles", "Beatles", "Beatles, The"]', ) t_created = models.DateTimeField( auto_now_add=True, - verbose_name="Дата Создания записи в БД", + verbose_name="Дата создания", ) t_updated = models.DateTimeField( auto_now=True, - verbose_name="Дата последнего обновления записи в БД", + verbose_name="Дата обновления", ) - def __unicode__(self): - return f"product {self.id:0>4}: {self.s_artist}" - def __str__(self): - return self.__unicode__() + return f"product {self.id:0>4}: {self.s_artist}" class Meta: verbose_name = 'Исполнитель' @@ -45,63 +51,88 @@ class TbArtist(models.Model): class TbProduct(models.Model): - # Абстрактный релиз / сущность / товар, который может быть представлен в виде одного или нескольких предложений - # от разных продавцов. + """ + Абстрактный релиз (альбом, сингл, компиляция). + Может быть представлен в виде одного или нескольких предложений от разных продавцов. + """ + + # Исполнители (ManyToMany для поддержки коллабораций) + # Например: "David Bowie & Queen", "Elton John & Tim Rice" k_artists = models.ManyToManyField( TbArtist, blank=True, + related_name='artist_to_products', # artist.products.all() — найти все релизы артиста verbose_name='Исполнители', - help_text="Выберите исполнителей или группы для этого релиза (можно не указывать)", + help_text="Один или несколько для коллабораций", ) + s_title = models.CharField( max_length=255, blank=False, null=False, default='', # db_index=True, verbose_name='Название товара (релиза)', - help_text='Название товара (релиза), как указано на обложке. Например: Abbey Road (remastered)' + help_text='Название товара (релиза), как указано на обложке. Например: Abbey Road' ' или TDK, CDing I, 90.', ) + s_title_html = models.TextField( - blank=True, null=True, default='', - verbose_name='Название товара (типографированно в HTML)', - help_text='Название товара (релиза), как указано на обложке, с сохранением типографирования и спецсимволов' - ' в виде HTML-тегов.', + blank=True, + null=True, + default='', + verbose_name='Название (типографированно)', + help_text='С HTML-тегами для типографирования', ) + s_title_slug = models.SlugField( - blank=True, null=True, default='', + blank=True, + null=True, + default='', verbose_name='Слаг', help_text='Слаг для товара, формируемый на основе названия товара (релиза)', ) - t_title_date = models.DateField( + + # Год выпуска (важно для коллекционеров) + s_release_date = models.CharField( + max_length=10, + blank=True, + null=True, + default='XXXX-XX-XX', + verbose_name='Дата релиза (str)', + help_text='Например: 1969-05-25, или 1969-05-XX (если день неизвестен), или 1969-XX-XX (если известен только' + ' год, или XXXX-XX-XX (если дата релиза неизвестна). Более приоритетное поле для отображения даты' + ' релиза, чем t_release_date, так как может содержать неполную дату и/или текстовую информацию.' + ) + + t_release_date = models.DateField( blank=True, null=True, verbose_name='Дата релиза', - help_text='Дата релиза, если известна. Если известен только год, можно указать 1 января этого года.' - ' Например: 1969-09-26 или не указывать вовсе.', + help_text='Полная дата если известна, например: 1969-09-26. Если точно известен.', ) + i_discogs_master_id = models.IntegerField( blank=True, null=True, default=None, verbose_name='ID на мастер-релиз Discogs', help_text='Уникальный идентификатор мастер-релиза на Discogs, если он там есть. Например: 306323', ) + + # Метаданные релиза (страна, жанр, количество треков и т.д.) j_product_metadata = models.JSONField( default=dict, blank=True, null=True, verbose_name='Дополнительные данные', - help_text='Дополнительные данные о релизе в виде JSON-словаря,', + help_text='Дополнительные данные о релизе в виде JSON-словаря. Сюда же включены варианты написания релиза в источниках', ) + t_created = models.DateTimeField( auto_now_add=True, - verbose_name="Дата Создания записи в БД", + verbose_name="Дата создания", ) t_updated = models.DateTimeField( auto_now=True, - verbose_name="Дата последнего обновления записи в БД", + verbose_name="Дата обновления", ) - def __unicode__(self): - return f"product {self.id:0>4}: {self.s_title}" - def __str__(self): - return self.__unicode__() + return f"[{self.id:0>4}] {self.s_title}" class Meta: verbose_name = 'Релиз (товар)' @@ -109,8 +140,68 @@ class TbProduct(models.Model): ordering = ('s_title',) +# ============================================================================ +# ЛЕЙБЛЫ (производители релизов) +# ============================================================================ + +class TbLabel(models.Model): + """ + Лейбл или издатель релиза. + Примеры: для винила, CD, Blu-Ray это: Sony, Мелодия, Atlantic, EMI ... + для кассет под запись это: TDK, AXIA, Maxell, JVC ... + для hi-fi это: Sony, Pioneer, Technics, Marantz ... + """ + + s_label = models.CharField( + max_length=128, + blank=False, + null=False, + default='', + verbose_name='Название лейбла', + help_text='Например: "Sony Records" или "Мелодия"', + ) + + s_label_slug = models.SlugField( + max_length=64, + blank=False, + null=False, + default='', + verbose_name='Слаг', + ) + + j_label_metadata = models.JSONField( + default=dict, + blank=True, + null=True, + verbose_name='Дополнительные данные', + help_text='JSON: страна лейбла, официальный сайт и т.д.', + ) + + t_created = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + ) + t_updated = models.DateTimeField( + auto_now=True, + verbose_name="Дата обновления", + ) + + def __str__(self): + return self.s_label + + class Meta: + verbose_name = 'Лейбл' + verbose_name_plural = 'Лейблы' + ordering = ('s_label',) + + +# ============================================================================ +# ПРОДАВЦЫ / МАГАЗИНЫ +# ============================================================================ + class TbSeller(models.Model): - # Продавец товара + """Продавец или магазин, который продаёт товары.""" + class SellerType(models.TextChoices): SELLER = 'seller', 'Продавец' LABEL = 'label', 'Лейбл (издатель)' @@ -123,73 +214,71 @@ class TbSeller(models.Model): verbose_name='Название продавца', help_text='Название продавца или магазина, например: Клюква Рекодс', ) + s_seller_slug = models.SlugField( max_length=32, blank=False, null=False, default='', verbose_name='Слаг', help_text='Слаг для продавца, формируемый на основе названия продавца', ) + l_seller_type = models.CharField( max_length=9, default=SellerType.SELLER, choices=SellerType.choices, verbose_name='Тип продавца', ) + j_seller_metadata = models.JSONField( default=dict, blank=True, null=True, verbose_name='Дополнительные данные', - help_text='Дополнительные данные о продавце в виде JSON-словаря', + help_text='Дополнительные данные о продавце в виде JSON-словаря. Телефон, email, адрес, ссылка на сайт и т.д.', ) - def __unicode__(self): - return f"vendor {self.id:0>3}: {self.s_vendor}" - + t_created = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания", + ) + t_updated = models.DateTimeField( + auto_now=True, + verbose_name="Дата обновления", + ) + def __str__(self): - return self.__unicode__() - + return self.s_seller + class Meta: verbose_name = 'Продавец' verbose_name_plural = 'Продавцы' ordering = ('s_seller',) - -class TbLabel(models.Model): - # Лейблы: производители (Улитка Рекородс, Мелодия, Sony... а так же производители TDK, AXIA, Maxwell и т.д.) - s_label = models.CharField( - max_length=32, blank=False, null=False, default='', - verbose_name='Название лейбла', - help_text='Название лейбла, например: Мелодия или TDK', - ) - s_label_slug = models.SlugField( - max_length=32, blank=False, null=False, default='', - verbose_name='Слаг', - help_text='Слаг для лейбла, формируемый на основе названия лейбла', - ) - j_label_metadata = models.JSONField( - default=dict, blank=True, null=True, - verbose_name='Дополнительные данные', - help_text='Дополнительные данные о лейбле в виде JSON-словаря', - ) +# ============================================================================ +# ПРЕДЛОЖЕНИЯ / ОФФЕРЫ +# ============================================================================ -# class TbOffer(models.Model): - # Конкретное предложение продавца + """ + Конкретное предложение от продавца. + Один и тот же релиз может быть несколько раз в системе от разных продавцов. + """ + class Format(models.TextChoices): LP = 'lp', 'vinyl' CD = 'cd', 'CD' BD = 'bd', 'Blu-ray' - CS = 'cs', 'Cassette' - MD = 'md', 'Minidisc' + CR = 'cr', 'Кассета с записью (фирменная)' + CS = 'cs', 'Кассета под запись (новая или б/у)' + MD = 'md', 'MiniDisc' BX = 'bx', 'Box Set' - HI = 'hi', 'Hi-Fi' - AC = 'ac', 'Accessory' + HI = 'hi', 'Hi-Fi (аппаратура)' + AC = 'ac', 'Accessory (Аксессуар)' OTHER = '++', 'Other' class Condition(models.TextChoices): S = 's', 'Still Sealed (новое, запечатано)' - M = 'm', 'Mint' - NM = 'nm', 'Near Mint' - VG = 'vg', 'Very Good' - G = 'g', 'Good' - F = 'f', 'Fair' - P = 'p', 'Poor' + M = 'm', 'Mint (новое, распакованное)' + NM = 'nm', 'Near Mint (почти новое)' + VG = 'vg', 'Very Good (очень хорошее)' + G = 'g', 'Good (хорошее)' + F = 'f', 'Fair (удовлетворительное)' + P = 'p', 'Poor (плохое)' OTHER = '++', 'Other' class Currency(models.TextChoices): @@ -205,32 +294,46 @@ class TbOffer(models.Model): TON = 'ton', 'TON: криптовалюта TON' OTHER = '++', 'Other' - s_catalog_num = models.TextField( - blank=True, default='', - verbose_name='Каталожный номер или barcode', - help_text='Каталожный номер релиза, если он есть. Например: SD 16023 для ABBA — Super Trouper' - ' — 1980 — Atlantic (USA)', - ) + # Связи k_product = models.ForeignKey( TbProduct, - blank=True, default=None, - on_delete=models.SET_NULL, related_name='+', + blank=True, + null=True, + default=None, + on_delete=models.SET_NULL, + related_name='product_to_offer', # ← product.product_offers.all() verbose_name='Релиз (товар)', ) + k_label = models.ForeignKey( TbLabel, - null=True, default=None, - on_delete=models.SET_NULL, related_name='+', + null=True, + default=None, + on_delete=models.SET_NULL, + related_name='label_to_offer', # ← label.label_offers.all() verbose_name='Лейбл', help_text='Лейбл, на котором был выпущен релиз, если он известен. Например: Atlantic или Мелодия', ) - k_seller = models.ForeignKey( - TbSeller, - null=True, default=None, - on_delete=models.SET_NULL, - related_name='+', - verbose_name='Продавец', + + k_source = models.ForeignKey( + to='TbSource', + null=True, + default=None, + on_delete=models.CASCADE, # ← если удалён источник, удалены офферы + related_name='source_to_offer', + verbose_name='Источник данных', + help_text='Обязательно - каждый оффер должен иметь источник. Через источник, так же получаем данные' + ' продавца `offer.k_source.k_seller`.', ) + + # Характеристики + s_catalog_num = models.TextField( + blank=True, + default='', + verbose_name='Каталожный номер / Barcode', + help_text='Например: "SD 16023" или "5099923452355"', + ) + l_primary_media = models.CharField( max_length=2, choices=Format.choices, @@ -238,11 +341,13 @@ class TbOffer(models.Model): verbose_name='Формат', help_text='Основной формат носителя (пластинка, CD, кассета и т.п.)' ) + i_discogs_id = models.IntegerField( blank=True,default=0, verbose_name='ID на релиз Discogs', help_text='Уникальный идентификатор релиза на Discogs, если он там есть. Например: 306323', ) + l_condition_media = models.CharField( max_length=2, choices=Condition.choices, @@ -250,6 +355,7 @@ class TbOffer(models.Model): verbose_name="Состояние носителя", help_text='Состояние носителя (пластинки, CD и т.п.) по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).', ) + l_condition_sleeve = models.CharField( max_length=2, choices=Condition.choices, @@ -257,166 +363,198 @@ class TbOffer(models.Model): verbose_name="Состояние обложки", help_text='Состояние обложки по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).', ) + + # Цена и наличие f_price = models.DecimalField( max_digits=10, decimal_places=2, - blank=True, default=0.00, + blank=True, + default=0.00, verbose_name='Цена', ) + l_currency = models.CharField( max_length=3, choices=Currency.choices, default=Currency.RUB, verbose_name="Валюта", ) + i_quantity = models.IntegerField( - blank=True, default=0, - verbose_name='Количество', + blank=True, + default=0, + verbose_name='Количество в наличии', ) + b_is_available = models.BooleanField( default=True, - verbose_name='В наличии', + verbose_name='В наличии?', ) i_discount_to_daily_sale = models.IntegerField( blank=True, default=0, verbose_name='Скидка', help_text='Процент скидки, для ежедневной распродажи', ) + + # Комментарии s_offer_comment = models.TextField( - blank=True, default='', + blank=True, + default='', verbose_name='Доп.инфо', help_text='Дополнительная информация или комментарий к предложению от продавца, например:' ' "Пластинка запаяна в целлофан, угол обложки замят.".', ) + j_offer_metadata = models.JSONField( + # Метаданные оффера (сырые данные из источника, координаты в Excel и т.д.) default=dict, null=True, verbose_name='Дополнительные данные', help_text='Дополнительные данные о предложении в виде JSON-словаря.', ) t_offer_created = models.DateTimeField( auto_now_add=True, - verbose_name="Дата Создания записи в БД", + verbose_name="Дата создания", ) t_offer_updated = models.DateTimeField( + auto_now=True, + verbose_name="Дата обновления", + ) + + def __str__(self): + return f"offer {self.id:0>4} for product {self.k_product_id} from seller {self.k_seller_id}" + +# ============================================================================ +# ИСТОЧНИКИ ДАННЫХ +# ============================================================================ +class TbSource(models.Model): + """ + Источник данных, из которого был импортирован оффер. + Например, это может быть Excel-файл от продавца или издателя, CSV-файл, URL страницы с данными + (например, HTML-страница с каталогом товаров) и т.д. + """ + class SourceType(models.TextChoices): + EXCEL = 'excel', 'Excel-файл от продавца или издателя' + CSV = 'csv', 'CSV-файл от продавца или издателя' + URL = 'url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)' + OTHER = '++', 'Другое' + + k_seller = models.ForeignKey( + TbSeller, + null=True, + default=None, + on_delete=models.SET_NULL, + verbose_name='Продавец', + ) + s_source_name = models.CharField( + max_length=128, + blank=True, + default='', + verbose_name='Название источника', + help_text='Название источника данных (для удобства), например: Предзаказ на RSD-2025 от Полуэкта.', + ) + l_source_type = models.CharField( + max_length=5, default=SourceType.EXCEL, choices=SourceType.choices, + verbose_name='Тип источника', + help_text='Тип источника данных, например: Excel-файл от продавца или издателя, URL страницы' + ' с данными и т.д.', + ) + t_source_data = models.DateField( + blank=True, default=datetime.date.today, + verbose_name='Дата данных', + help_text='Дата, к которой относятся данные в источнике. Например, если это исторический Excel-файл.', + ) + file_source = models.FileField( + blank=True, default='', upload_to='sources/', + verbose_name='Файл-источник', + help_text='Файл-источник, например, Excel-файл от продавца или издателя. Если данные в источнике' + ' представлены на странице в интернете, можно не указывать файл, а указать URL в поле ниже.', + ) + s_source_url = models.TextField( + max_length=255, blank=True, default='', + verbose_name='URL источника', + help_text='URL страницы с данными, например, HTML-страница с каталогом товаров. Если данные в источнике' + ' представлены в виде файла, можно не указывать URL, а загрузить файл в поле выше.', + ) + s_source_file_hash = models.CharField( + max_length=128, blank=True, default='', + verbose_name='Хэш файла-источника', + help_text='Контрольная сумма, хэш файла-источника (например, MD5 или SHA256), для проверки целостности' + ' и отслеживания изменений в файлах-источниках при повторных загрузках.', + ) + j_source_metadata = models.JSONField( + default=dict, blank=True, + verbose_name='Дополнительные данные', + help_text='Дополнительные данные об источнике (внутреннем устройстве: вкладках и стоkбцах Excel-файла,' + ' структуре HTML-страницы и т.п.) в виде JSON-словаря', + ) + t_source_created = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата Создания записи в БД", + ) + t_source_updated = models.DateTimeField( auto_now=True, verbose_name="Дата последнего обновления записи в БД", ) - class TbSource(models.Model): - # Источник данных (абстрактный), "место", откуда данные могут приходить многократно. + def __str__(self): + return f"source {self.id:0>4}: {self.s_source_name}" - class SoueceType(models.TextChoices): - EXCEL = 'excel', 'Excel-файл от продавца или издателя' - CSV = 'csv', 'CSV-файл от продавца или издателя' - URL = 'url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)' - OTHER = '++', 'Другое' + class Meta: + verbose_name = 'Источник данных' + verbose_name_plural = 'Источники данных' + ordering = ('-t_source_data', '-t_source_created') - k_seller = models.ForeignKey( - TbSeller, - null=True, default=None, - on_delete=models.SET_NULL, - verbose_name='Продавец', - ) - s_source_name = models.CharField( - max_length=128, blank=True, default='', - verbose_name='Название источника', - help_text='Название источника данных (для удобства), например: Предзаказ на RSD-2025 от Полуэкта.', - ) - l_source_type = models.CharField( - max_length=5, default=SoueceType.EXCEL, choices=SoueceType.choices, - verbose_name='Тип источника', - help_text='Тип источника данных, например: Excel-файл от продавца или издателя, URL страницы' - ' с данными и т.д.', - ) - t_source_data = models.DateField( - blank=True, default=datetime.date.today, - verbose_name='Дата данных', - help_text='Дата, к которой относятся данные в источнике. Например, если это исторический Excel-файл.', - ) - file_source = models.FileField( - blank=True, default='', upload_to='sources/', - verbose_name='Файл-источник', - help_text='Файл-источник, например, Excel-файл от продавца или издателя. Если данные в источнике' - ' представлены на странице в интернете, можно не указывать файл, а указать URL в поле ниже.', - ) - s_source_url = models.TextField( - max_length=255, blank=True, default='', - verbose_name='URL источника', - help_text='URL страницы с данными, например, HTML-страница с каталогом товаров. Если данные в источнике' - ' представлены в виде файла, можно не указывать URL, а загрузить файл в поле выше.', - ) - s_source_file_hash = models.CharField( - max_length=128, blank=True, default='', - verbose_name='Хэш файла-источника', - help_text='Контрольная сумма, хэш файла-источника (например, MD5 или SHA256), для проверки целостности' - ' и отслеживания изменений в файлах-источниках при повторных загрузках.', - ) - j_source_metadata = models.JSONField( - default=dict, blank=True, - verbose_name='Дополнительные данные', - help_text='Дополнительные данные об источнике (внутреннем устройстве: вкладках и стоkбцах Excel-файла,' - ' структуре HTML-страницы и т.п.) в виде JSON-словаря', - ) - t_source_created = models.DateTimeField( - auto_now_add=True, - verbose_name="Дата Создания записи в БД", - ) - t_source_updated = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего обновления записи в БД", - ) - def __unicode__(self): - return f"source {self.id:0>4}: {self.s_source_name}" - - def __str__(self): - return self.__unicode__() - - class Meta: - verbose_name = 'Источник данных' - verbose_name_plural = 'Источники данных' - ordering = ('-t_source_data', '-t_source_created') +# ============================================================================ +# ИСТОРИЯ ИЗМЕНЕНИЙ ОФФЕРОВ +# ============================================================================ class TbOfferHistory(models.Model): - # История изменений оффера. Снапшот цены. - # - # Создаётся: - # - только если что-то реально изменилось (обычно при новом импорте Excel) + """ + История изменений оффера (снапшот цены, количества, наличия). + Создаётся при каждом импорте, если что-то изменилось. + """ + k_offer = models.ForeignKey( TbOffer, on_delete=models.CASCADE, - related_name='history' + related_name='history_to_offer', # ← offer.history_to_offer.all() + verbose_name='Оффер', ) - f_histopy_price = models.DecimalField( - max_digits=12, decimal_places=2, null=True, blank=True, default=0.00, - verbose_name='Цена (историческая)', - help_text='Цена оффера в момент изменения (например, при новом импорте данных). Это историческая' - ' цена, которая может отличаться от текущей цены в оффере.' + f_history_price = models.DecimalField( + max_digits=12, + decimal_places=2, + null=True, + blank=True, + default=0.00, + verbose_name='Старая цена', ) - i_histopy_quantity = models.IntegerField( + + i_history_quantity = models.IntegerField( default=0, - verbose_name='Количество (историческое)', - help_text='Количество товара в оффере в момент изменения (например, при новом импорте данных).' - ' Это историческое количество, которое может отличаться от текущего количества в оффере.' + verbose_name='Старое количество', ) - b_histopy_available = models.BooleanField( + + b_history_available = models.BooleanField( + # Подумать о необходимости этого поля. Устанавливая количество в ноль, можно указать, что предложение более + # не доступно. Если предложение вернется, то через новую запись в TbOfferHistory можно будет отследить, + # что оно было в наличии, пропало, а потом снова появилось (со старой или новой ценой). default=True, - verbose_name='В наличии (историческое)', - help_text='Наличие товара в оффере в момент изменения (например, при новом импорте данных из Excel).' - ' False если оффер был в наличии, а при новом импорте стал недоступен.' - ' Это историческое наличие, которое может отличаться от текущего наличия в оффере.' + verbose_name='Был в наличии', ) - # Откуда приехало + + # Откуда приехало изменение k_source = models.ForeignKey( - to='TbSource', - null=True, default=None, + TbSource, + null=True, + default=None, on_delete=models.SET_NULL, - verbose_name='Источник данных', - help_text='Источник данных, из которого были импортированы изменения (например, конкретный Excel-файл' - ' или URL страницы с данными). Указывать не обязательно, т.к. ' + related_name='source_to_offer_history', # ← source.source_to_offer_history.all() + verbose_name='Источник изменений', ) - j_histopy_source_metadata = models.JSONField( + + j_history_metadata = models.JSONField( default=dict, blank=True, verbose_name='Метаданные', @@ -424,25 +562,20 @@ class TbOfferHistory(models.Model): ' название вкладки, номер строки, номер столбца с ценой и количеством,' ' или URL + CSS-селектор для HTML-страницы и т.п.', ) - t_histopy_source_created = models.DateTimeField( + t_history_created = models.DateTimeField( auto_now_add=True, - verbose_name="Дата создания записи в БД", + verbose_name="Дата создания", ) - t_histopy_source_updated = models.DateTimeField( - auto_now=True, - verbose_name="Дата последнего обновления записи в БД", - ) - - def __unicode__(self): - return f"offer history {self.id:0>5} for offer {self.k_offer_id:0>4}" + # Нам не нужен `t_history_updated` потому что это "снимок состояния" и его не нужно менять + # после создания. И если вдруг понадобится, то правильнее будет добавить новую запись. def __str__(self): - return self.__unicode__() + return f"History #{self.id} for offer {self.k_offer_id}" class Meta: verbose_name = 'История оффера' verbose_name_plural = 'Истории офферов' - ordering = ('-t_histopy_source_created',) + ordering = ('-t_history_created',)