add: модели (draft 03)

This commit is contained in:
2026-05-30 18:49:41 +03:00
parent 3f45a7e397
commit efd4695960

View File

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