diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py
index 71a8362..97b4d35 100644
--- a/lpon_site/frontend/models.py
+++ b/lpon_site/frontend/models.py
@@ -1,3 +1,380 @@
from django.db import models
# Create your models here.
+class TbArtist(models.Model):
+ s_artist = models.CharField(
+ max_length=255,
+ db_index=True,
+ verbose_name='Исполнитель',
+ help_text="Исполнитель или группа.",
+ )
+ s_artist_html = models.TextField(
+ blank=True, null=True, default='',
+ verbose_name='Исполнитель (типографированно в HTML)',
+ help_text='Исполнитель или группа, указанные на обложке релиза, с сохранением типографирования и спецсимволов в виде HTML-тегов.',
+ )
+ s_slug = models.SlugField(
+ max_length=64,
+ )
+ j_artist_in_source = models.JSONField(
+ 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="Дата Создания записи в БД",
+ )
+ t_updated = models.DateTimeField(
+ auto_now=True,
+ verbose_name="Дата последнего обновления записи в БД",
+ )
+
+ def __unicode__(self):
+ return f"product {self.id:0>4}: {self.s_artist}"
+
+ def __str__(self):
+ return self.__unicode__()
+
+ class Meta:
+ verbose_name = 'Исполнитель'
+ verbose_name_plural = 'Исполнители'
+ ordering = ('s_artist',)
+
+
+class TbProduct(models.Model):
+ # Абстрактный релиз / сущность / товар, который может быть представлен в виде одного или нескольких предложений
+ # от разных продавцов.
+ k_artists = models.ManyToManyField(
+ TbArtist,
+ blank=True,
+ null=True,
+ default=None,
+ related_name='+', # ← '+' отключает обратную связь
+ verbose_name='Исполнители',
+ help_text="Выберите исполнителей или группы для этого релиза (можно не указывать)",
+ )
+ s_title = models.CharField(
+ max_length=255, blank=False, null=False, default='',
+ # db_index=True,
+ verbose_name='Название товара (релиза)',
+ help_text='Название товара (релиза), как указано на обложке. Например: Abbey Road (remastered)'
+ ' или TDK, CDing I, 90.',
+ )
+ s_title_html = models.TextField(
+ blank=True, null=True, default='',
+ verbose_name='Название товара (типографированно в HTML)',
+ help_text='Название товара (релиза), как указано на обложке, с сохранением типографирования и спецсимволов'
+ ' в виде HTML-тегов.',
+ )
+ s_title_slug = models.SlugField(
+ blank=True, null=True, default='',
+ verbose_name='Слаг',
+ help_text='Слаг для товара, формируемый на основе названия товара (релиза)',
+ )
+ t_title_date = models.DateField(
+ blank=True,
+ null=True,
+ verbose_name='Дата релиза',
+ help_text='Дата релиза, если известна. Если известен только год, можно указать 1 января этого года.'
+ ' Например: 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-словаря,',
+ )
+ t_created = models.DateTimeField(
+ auto_now_add=True,
+ verbose_name="Дата Создания записи в БД",
+ )
+ t_updated = models.DateTimeField(
+ auto_now=True,
+ verbose_name="Дата последнего обновления записи в БД",
+ )
+
+ def __unicode__(self):
+ return f"product {self.id:0>4}: {self.s_title}"
+
+ def __str__(self):
+ return self.__unicode__()
+
+ class Meta:
+ verbose_name = 'Релиз (товар)'
+ verbose_name_plural = 'Релизы (товары)'
+ ordering = ('s_title',)
+
+
+class TbSeller(models.Model):
+ # Продавец товара
+ class SellerType(models.TextChoices):
+ SELLER = 'seller', 'Продавец'
+ LABEL = 'label', 'Лейбл (издатель)'
+ DIY = 'diy', 'Самиздат группы'
+ CROWD = 'crowdfunding', 'Краудфандинг'
+ OTHER = '++', 'Другое'
+
+ s_seller = models.CharField(
+ max_length=32, blank=False, null=False, default='',
+ 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-словаря',
+ )
+
+ def __unicode__(self):
+ return f"vendor {self.id:0>3}: {self.s_vendor}"
+
+ def __str__(self):
+ return self.__unicode__()
+
+ 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'
+ BX = 'bx', 'Box Set'
+ 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'
+ OTHER = '++', 'Other'
+
+ class Currency(models.TextChoices):
+ RUB = 'rub', 'RUB: российский рубль'
+ TRY = 'try', 'TRY: турецкая лира'
+ AMD = 'amd', 'AMD: армянский драм'
+ USD = 'usd', 'USD: американский доллар'
+ EUR = 'eur', 'EUR: евро'
+ JPY = 'jpy', 'JPY: японская иена'
+ GBP = 'gbp', 'GBP: британский фунт'
+ CNY = 'cny', 'CNY: китайский юань'
+ BYN = 'byn', 'BYN: белорусский рубль'
+ 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='+',
+ verbose_name='Релиз (товар)',
+ )
+ k_label = models.ForeignKey(
+ TbLabel,
+ null=True, default=None,
+ on_delete=models.SET_NULL, related_name='+',
+ verbose_name='Лейбл',
+ help_text='Лейбл, на котором был выпущен релиз, если он известен. Например: Atlantic или Мелодия',
+ )
+ k_seller = models.ForeignKey(
+ TbSeller,
+ null=True, default=None,
+ on_delete=models.SET_NULL,
+ related_name='+',
+ verbose_name='Продавец',
+ )
+ l_primary_media = models.CharField(
+ max_length=2,
+ choices=Format.choices,
+ default=Format.LP,
+ 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,
+ default=Condition.S,
+ verbose_name="Состояние носителя",
+ help_text='Состояние носителя (пластинки, CD и т.п.) по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).',
+ )
+ l_condition_sleeve = models.CharField(
+ max_length=2,
+ choices=Condition.choices,
+ default=Condition.S,
+ verbose_name="Состояние обложки",
+ help_text='Состояние обложки по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).',
+ )
+ f_price = models.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ 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='Количество',
+ )
+ b_is_available = models.BooleanField(
+ default=True,
+ 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='',
+ verbose_name='Доп.инфо',
+ help_text='Дополнительная информация или комментарий к предложению от продавца, например:'
+ ' "Пластинка запаяна в целлофан, угол обложки замят.".',
+ )
+ j_offer_metadata = models.JSONField(
+ default=dict, 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="Дата последнего обновления записи в БД",
+ )
+
+#
+# │ 1
+# │
+# │ N
+# ┌───────▼────────────┐
+# │ ProductImage │
+# ├────────────────────┤
+# │ id │
+# │ product_id │ FK
+# │ image_type │ ← cover/back/obi/matrix/etc
+# │ source │ ← discogs/manual/vendor
+# │ url │
+# │ local_path │
+# │ is_primary │
+# │ sort_order │
+# │ metadata_json │
+# └────────────────────┘
+#
+#
+# ┌────────────────────┐
+# │ ProductEnrichment │
+# ├────────────────────┤
+# │ id │
+# │ product_id │ FK
+# │ source │ ← discogs/api/scraper
+# │ confidence_score │
+# │ data_json │
+# │ fetched_at │
+# └────────────────────┘
+#
+#
+# ┌────────────────────┐
+# │ PriceHistory │
+# ├────────────────────┤
+# │ id │
+# │ offer_id │ FK
+# │ old_price │
+# │ new_price │
+# │ quantity_snapshot │
+# │ source │
+# │ changed_at │
+# └────────────────────┘
+#
+#
+# ┌────────────────────┐
+# │ Source │ ← Импорт / источник
+# ├────────────────────┤
+# │ id │
+# │ vendor_id │ FK
+# │ type │ ← excel/csv/api/html
+# │ source_url │
+# │ file_name │
+# │ file_hash │
+# │ imported_at │
+# │ parser_version │
+# │ meta_json │
+# └─────────┬──────────┘
+# │ 1
+# │
+# │ N
+# ┌─────────▼──────────┐
+# │ SourceItem │ ← Строка из Excel/CSV
+# ├────────────────────┤
+# │ id │
+# │ source_id │ FK
+# │ row_index │
+# │ raw_row_json │
+# │ parsed_data_json │
+# │ matching_status │
+# │ confidence_score │
+# │ matched_product_id │ FK nullable
+# │ created_offer_id │ FK nullable
+# │ created_at │
+# └────────────────────┘
\ No newline at end of file