diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py
index f5bd565..11a1cfe 100644
--- a/lpon_site/frontend/admin.py
+++ b/lpon_site/frontend/admin.py
@@ -1289,10 +1289,10 @@ class OfferAdmin(RequestInFormMixin, admin.ModelAdmin):
list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format')
search_fields = ('s_offer',)
inlines = [OfferImageInline] # Управление картинками в inline
- readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated',)
+ readonly_fields = ('s_offer_code', 't_offer_created', 't_offer_updated',)
fieldsets = (
('Код товара в базе', {
- 'fields': ('s_offer_skip32',),
+ 'fields': ('s_offer_code',),
'description': 'Уникальный код товара в базе. Используется вместо ID для генерации ссылок на оффер'
' и складского учёта с помощью QR-кодов. Не редактируется вручную.',
}),
diff --git a/lpon_site/frontend/migrations/0004_alter_tbarticle_k_article_to_image_and_more.py b/lpon_site/frontend/migrations/0004_alter_tbarticle_k_article_to_image_and_more.py
new file mode 100644
index 0000000..9137e7e
--- /dev/null
+++ b/lpon_site/frontend/migrations/0004_alter_tbarticle_k_article_to_image_and_more.py
@@ -0,0 +1,64 @@
+# Generated by Django 6.0.5 on 2026-07-15 14:00
+
+import django.db.models.deletion
+import django.utils.timezone
+import filer.fields.image
+import frontend.models
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('frontend', '0003_remove_tbimagemetadata_f_img_confidence_score_and_more'),
+ migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='tbarticle',
+ name='k_article_to_image',
+ field=filer.fields.image.FilerImageField(blank=True, help_text='Обложка или иллюстрация статьи из файлового хранилища (django_filer). Метаданные: image.metadata.i_img_sort и image.metadata.j_img_metadata.
ВАЖНО: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям, её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае рекомендуется использовать изображение с прозрачным фоном (движок поддерживает SVG, WebP и PNG).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='image_to_article', to=settings.FILER_IMAGE_MODEL, verbose_name='Изображение для статьи'),
+ ),
+ migrations.AlterField(
+ model_name='tbimagemetadata',
+ name='j_img_metadata',
+ field=models.JSONField(blank=True, default=frontend.models.get_image_metadata_default, help_text='JSON с дополнительными данными: тип (реальное/абстрактное), источник (парсер/админка), URL источника и заметки. Предзаполнено структурой для удобства.', verbose_name='Метаданные'),
+ ),
+ migrations.AlterField(
+ model_name='tboffer',
+ name='d_offer_date_release',
+ field=models.DateField(blank=True, db_index=True, default=None, help_text='Дата релиза (например, дата выпуска переиздания) или выхода по предзаказу, если она известна', null=True, verbose_name='Дата релиза'),
+ ),
+ migrations.AlterField(
+ model_name='tboffer',
+ name='i_offer_discount_to_daily_sale',
+ field=models.IntegerField(blank=True, db_index=True, default=0, help_text='Процент возможной скидки, если участвует в "ежедневной распродаже" или акции. Если указано 0 то данное предложение не может участвовать в распродажах, спецпредложениях и акциях', verbose_name='Возможна cкидка (%)'),
+ ),
+ migrations.AlterField(
+ model_name='tboffer',
+ name='j_offer_metadata',
+ field=models.JSONField(default=frontend.models.get_offer_metadata_default, help_text='Дополнительные данные о предложении в виде JSON-словаря. Например:
{\n "OFR_NT": "Уценка: замят угол конверта, повреждены наклейки.",\n "OFR_MF": {\n "lp": 2,\n "cd": 1\n }\n}Ключи:',
)
- s_offer_skip32 = models.CharField(
+ s_offer_code = models.CharField(
max_length=12,
unique=True,
verbose_name='Код товара',
help_text='Уникальный код товара для идентификации в корзине и при заказе (чтобы не светить id).'
- ' Например: "4gfFCJ". Формируется автоматически связкой Skip32 (хаотичное перемешивание) и'
- ' Base62 (компактная упаковка) из id оффера в методе save().',
+ ' Например: "4gfFCJ". Формируется автоматически через Hashids на основе ID оффера.'
+ ' Не редактировать вручную!',
)
i_offer_views = models.IntegerField(
default=0,
@@ -1332,7 +1352,7 @@ class TbOffer(models.Model):
def __str__(self):
seller = self.k_offer_to_source.k_source_to_seller.s_seller if (self.k_offer_to_source
and self.k_offer_to_source.k_source_to_seller) else "?"
- return (f"[{self.s_offer_skip32:0>6}]\u00A0«{self.s_offer}»"
+ return (f"[{self.s_offer_code:0>6}]\u00A0«{self.s_offer}»"
f" ⟶\u00A0item\u00A0«{self.k_offer_to_item_id}»"
f" ⟶\u00A0seller\u00A0«{seller}»")
@@ -1351,10 +1371,10 @@ class TbOffer(models.Model):
def save(self, *args, **kwargs):
"""
Переопределенный метод save для:
- 1. Автоматического формирования s_offer_skip32 (криптографический код)
+ 1. Автоматического формирования s_offer_code (криптографический код)
2. Записи истории изменений цены и количества в TbOfferHistory
- ЛОГИКА s_offer_skip32:
+ ЛОГИКА s_offer_code:
- Для новых оферов: генерируем код после получения ID
- Для старых оферов БЕЗ кода: кодируем существующий ID (миграция)
- Для старых оферов С кодом: не трогаем
@@ -1364,25 +1384,25 @@ class TbOffer(models.Model):
- При обновлении: если цена или количество изменилось → создаем новую запись
- Это позволяет отследить полную историю изменений
"""
- # 1. Проверяем нужно ли генерировать s_offer_skip32:
+ # 1. Проверяем нужно ли генерировать s_offer_code:
# - ИЛИ это новый объект (self.pk == None)
- # - ИЛИ это старый объект без s_offer_skip32 (миграция)
- if not self.pk or not self.s_offer_skip32:
+ # - ИЛИ это старый объект без s_offer_code (миграция)
+ if not self.pk or not self.s_offer_code:
# Если это новый оффер, сначала сохраняем, чтобы получить ID
if not self.pk:
- # Сохраняем БЕЗ s_offer_skip32 чтобы Django создал запись и присвоил pk
+ # Сохраняем БЕЗ s_offer_code чтобы Django создал запись и присвоил pk
super().save(*args, **kwargs)
# После save() Django автоматически заполнит self.pk
# Кодируем pk в компактный, необратимый код
# Пример: pk=42 → "QBErd8"
- self.s_offer_skip32 = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH).encode(self.pk)
+ self.s_offer_code = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH).encode(self.pk)
- # Сохраняем только поле s_offer_skip32 (не перезаписываем остальное)
- super().save(update_fields=['s_offer_skip32'])
+ # Сохраняем только поле s_offer_code (не перезаписываем остальное)
+ super().save(update_fields=['s_offer_code'])
else:
- # Оффер существует И уже имеет s_offer_skip32: сохраняем как обычно
- # Не трогаем s_offer_skip32, он был сформирован при создании
+ # Оффер существует И уже имеет s_offer_code: сохраняем как обычно
+ # Не трогаем s_offer_code, он был сформирован при создании
super().save(*args, **kwargs)
# 2. Записывает историю изменений цены и количества в TbOfferHistory.
diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py
index f3c8888..95064ad 100644
--- a/lpon_site/lpon_site/settings.py
+++ b/lpon_site/lpon_site/settings.py
@@ -48,7 +48,7 @@ ALLOWED_HOSTS = env.list(
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=['127.0.0.1', 'localhost', 'testserver'])
-# ====== HASHIDS настройка для s_offer_skip32 ======
+# ====== HASHIDS настройка для s_offer_code ======
# Криптографическое кодирование ID оферов в компактные, необратимые коды.
# Благодаря hashids невозможно восстановить исходный ID из кода,
# даже зная алгоритм (в отличие от простого XOR).