Версия: 2.0.0
Дата: 2025-12-25
Статус: Стандарт
Правила валидации, обязательные поля, форматы данных, автозаполнение.
Минимальный набор для создания товара:
✅ article - уникальный, не NULL
✅ name - не NULL, минимум 3 символа
✅ brand - не NULL (по умолчанию "Unknown")
✅ category - не NULL
✅ product_type - не NULL (по умолчанию "simple")
✅ base_price - не NULL (по умолчанию 0.00)
✅ cost_price - не NULL (по умолчанию 0.00)
Рекомендуемые поля в зависимости от типа:
✅ article - должен существовать в pim_products
✅ salute_type - не NULL
⚠️ shots_count - РЕКОМЕНДУЕТСЯ для батарей, свечей, ракет
⚠️ caliber_inch - РЕКОМЕНДУЕТСЯ для батарей, свечей
⚠️ duration_sec - РЕКОМЕНДУЕТСЯ для фонтанов, дымов
| Поле | Формат | Regex | Пример |
|---|---|---|---|
code_1c |
НФ-NNNNNNNN | ^НФ-\d{8}$ |
НФ-00001234 |
article |
XXX-XXX-NNN | ^[A-Z0-9-]+$ |
SAL-PU-049 |
barcode |
13 цифр | ^\d{13}$ |
4607154781234 |
caliber_inch |
N.NN | ^\d\.\d{1,2}$ |
1.2, 1.75 |
certificate_number |
ТС RU ... | ^ТС RU C-CN\.[А-Я]{2}\d{2}\.В\.\d{5}$ |
ТС RU C-CN.МЛ01.В.00228 |
hazard_class |
N.NG | ^1\.[1-6]G$ |
1.4G |
un_code |
UNNNNNN | ^UN\d{4}$ |
UN0336 |
dimensions |
DDDxWWWxHHH | ^\d{2,4}x\d{2,4}x\d{2,4}$ |
300x200x150 |
| Поле | Минимум | Максимум | Типичное |
|---|---|---|---|
caliber_inch |
0.5" | 3.0" | 0.8" - 1.5" |
caliber_mm |
12.7 мм | 76.2 мм | 20 - 50 мм |
shots_count |
1 | 500 | 10 - 100 |
effects_count |
1 | 50 | 3 - 25 |
duration_sec |
5 | 600 | 30 - 120 |
height_mm |
5000 | 150000 | 20000 - 60000 |
safety_distance_m |
5 | 100 | 15 - 50 |
weight_kg |
0.01 | 50.0 | 0.5 - 10.0 |
base_price |
10.00 | 999999.99 | 100 - 10000 |
discount_percent |
0.00 | 99.99 | 10 - 40 |
ВАЖНО: Значения должны быть в нижнем регистре!
Допустимые значения:
VALID_SALUTE_TYPES = [
'батарея',
'фонтан',
'римская свеча',
'петарда',
'ракета',
'миномет',
'набор',
'вулкан',
'колесо',
'торт',
'дым',
'хлопушка',
'бенгальские',
'летающий',
'наземный'
]
Если заполнено одно поле, должно быть заполнено и другое:
| Поле 1 | → | Поле 2 | Формула |
|---|---|---|---|
caliber_inch |
→ | caliber_mm |
caliber_mm = caliber_inch × 25.4 |
calibers (массив) |
→ | is_combo = true |
Несколько калибров |
certificate_number |
→ | certificate_expiry |
Обязателен срок |
hazard_class |
→ | un_code |
Связаны |
base_price > 0 |
→ | cost_price > 0 |
Расчет себестоимости |
article - PRIMARY KEY, абсолютно уникальныйcode_1c - должен быть уникальным (индекс)barcode - должен быть уникальным, если заполненПеред вставкой проверяем:
-- Проверка перед вставкой
SELECT article FROM pim_products WHERE article = :article;
SELECT code_1c FROM pim_products WHERE code_1c = :code_1c;
SELECT barcode FROM pim_products WHERE barcode = :barcode AND barcode IS NOT NULL;
При импорте из 1С автоматически заполняются:
| Поле | Откуда | Как |
|---|---|---|
caliber_mm |
caliber_inch |
inch × 25.4 |
cost_price |
base_price + правила |
По discount_percent |
is_combo |
calibers |
Если len > 1 |
category |
Parent_Key |
По справочнику GUID |
salute_type |
category |
По маппингу |
created_at |
- | NOW() |
updated_at |
- | NOW() |
def calculate_cost_price(base_price: Decimal, brand: str) -> Decimal:
"""
Рассчитать себестоимость по правилам
1. Найти правило для бренда
2. Применить discount_percent
3. Вернуть cost_price
"""
# Получить правило из price_cost_rules
rule = db.query(PimCostRules).filter(
PimCostRules.brand == brand,
PimCostRules.is_active == True
).order_by(PimCostRules.priority.desc()).first()
if not rule:
# Нет правила - скидка 0%
return base_price
# Применить скидку
discount = rule.discount_percent / 100
cost_price = base_price * (1 - discount)
return cost_price.quantize(Decimal('0.01'))
# Обновляются все поля, кроме:
- article (никогда не меняется - PK)
- created_at (сохраняется оригинальное значение)
# Обязательно обновляется:
- updated_at (устанавливается в NOW())
# Если поле в новых данных NULL:
- НЕ затираем существующее значение
- Обновляем только если есть новое значение
async def update_product_from_1c(record: dict):
"""
Обновить существующий товар из 1С
1. Проверить существует ли товар
2. Нормализовать новые данные
3. Обновить только измененные поля
4. Сохранить в БД
"""
article = record['Артикул']
# Проверить существует
existing = await db.get(PimProduct, article=article)
if not existing:
# Создать новый
await create_product_from_1c(record)
return
# Нормализовать данные
normalized = await normalize_1c_record(record)
# Обновить поля (НЕ затирать NULL)
for field, value in normalized.items():
if value is not None:
setattr(existing, field, value)
# Обновить updated_at
existing.updated_at = datetime.utcnow()
await db.commit()
✅ salute_type ВСЕГДА в lowercase (батарея, фонтан, петарда)
✅ Калибр: дюймы → мм (× 25.4)
✅ Высота: метры → мм (× 1000)
✅ article - уникальный PRIMARY KEY
✅ Все цены в рублях (base_price, cost_price)
✅ Бренд обязателен (для расчета себестоимости)
✅ При обновлении НЕ затирать NULL (обновлять только если есть значение)
Версия: 2.0.0
Дата: 2025-12-25