projects/org/pirotehnika/app/pim/standards/VALIDATION.md

ПРАВИЛА ВАЛИДАЦИИ И ВЕДЕНИЯ ДАННЫХ

Версия: 2.0.0
Дата: 2025-12-25
Статус: Стандарт


НАЗНАЧЕНИЕ

Правила валидации, обязательные поля, форматы данных, автозаполнение.


СОДЕРЖАНИЕ

  1. Обязательные поля
  2. Правила валидации
  3. Связанные поля
  4. Уникальность
  5. Автозаполнение
  6. Правила обновления

1. ОБЯЗАТЕЛЬНЫЕ ПОЛЯ

1.1 Для ВСЕХ товаров (pim_products)

Минимальный набор для создания товара:

 article - уникальный, не NULL
 name - не NULL, минимум 3 символа
 brand - не NULL (по умолчанию "Unknown")
 category - не NULL
 product_type - не NULL (по умолчанию "simple")
 base_price - не NULL (по умолчанию 0.00)
 cost_price - не NULL (по умолчанию 0.00)

1.2 Для пиротехники (pim_pirotehnika)

Рекомендуемые поля в зависимости от типа:

 article - должен существовать в pim_products
 salute_type - не NULL
⚠️ shots_count - РЕКОМЕНДУЕТСЯ для батарей, свечей, ракет
⚠️ caliber_inch - РЕКОМЕНДУЕТСЯ для батарей, свечей
⚠️ duration_sec - РЕКОМЕНДУЕТСЯ для фонтанов, дымов

2. ПРАВИЛА ВАЛИДАЦИИ

2.1 Форматы полей

Поле Формат 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

2.2 Диапазоны значений

Поле Минимум Максимум Типичное
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

2.3 Валидация salute_type

ВАЖНО: Значения должны быть в нижнем регистре!

Допустимые значения:

VALID_SALUTE_TYPES = [
    'батарея',
    'фонтан',
    'римская свеча',
    'петарда',
    'ракета',
    'миномет',
    'набор',
    'вулкан',
    'колесо',
    'торт',
    'дым',
    'хлопушка',
    'бенгальские',
    'летающий',
    'наземный'
]

3. СВЯЗАННЫЕ ПОЛЯ

Если заполнено одно поле, должно быть заполнено и другое:

Поле 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 Расчет себестоимости

4. УНИКАЛЬНОСТЬ

4.1 Уникальные поля

4.2 Проверка на дубликаты

Перед вставкой проверяем:

-- Проверка перед вставкой
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;

5. АВТОЗАПОЛНЕНИЕ

При импорте из 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()

5.1 Формула расчета себестоимости

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'))

6. ПРАВИЛА ОБНОВЛЕНИЯ

6.1 При обновлении существующего товара

# Обновляются все поля, кроме:
- article (никогда не меняется - PK)
- created_at (сохраняется оригинальное значение)

# Обязательно обновляется:
- updated_at (устанавливается в NOW())

# Если поле в новых данных NULL:
- НЕ затираем существующее значение
- Обновляем только если есть новое значение

6.2 Алгоритм обновления

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()

7. КЛЮЧЕВЫЕ ПРАВИЛА (РЕЗЮМЕ)

salute_type ВСЕГДА в lowercase (батарея, фонтан, петарда)
Калибр: дюймы → мм (× 25.4)
Высота: метры → мм (× 1000)
article - уникальный PRIMARY KEY
Все цены в рублях (base_price, cost_price)
Бренд обязателен (для расчета себестоимости)
При обновлении НЕ затирать NULL (обновлять только если есть значение)


Версия: 2.0.0
Дата: 2025-12-25