architect/standards/data/PRICE_ADAPTERS.md

Стандарт адаптеров прайс-листов

Версия: 1.0.0
Дата: 2025-12-24
Статус: Active


Проблема

Каждый поставщик присылает прайс-лист в своём формате:
- Разные структуры (xlsx, csv, xml)
- Разные названия колонок
- Разные единицы измерения
- Разные правила расчета цен

Без стандартизации импорт становится хаосом.


Решение: Два типа адаптеров

ПРАЙС-ЛИСТ → АДАПТЕР ТОВАРОВ → PIM → АДАПТЕР ЦЕН → ERP
              (маппинг полей)   ↑    (формулы)
                                |
                    Хранит всё из прайса

Важно: Всё из прайса (включая price_raw) сначала попадает в PIM.
Адаптер цен потом читает из PIM и рассчитывает цены для ERP.

1. Адаптер товаров (для PIM)

Назначение: Извлечь товарные данные из прайса и привести к единому формату

Ответственность:
- Маппинг полей (колонка "Артикул" → article)
- Форматирование (строка → число, дата)
- Валидация (обязательные поля)
- Извлечение атрибутов (калибр, залпы, размер)

НЕ делает:
- Не рассчитывает цены
- Не применяет скидки
- Не знает о бизнес-логике

Выход:

PIM запись:
  article: "СС7335"
  name: "С Днем Рождения (1"х12)"
  brand: "Супер Салют"
  supplier: "ИП Гордеев"
  price_raw: 1784.00  ← базовая из прайса (как есть)
  stock_qty: 177
  ...

2. Адаптер цен (для ERP)

Назначение: Рассчитать все цены по бизнес-правилам для работы ERP

Ответственность:
- Расчет себестоимости (cost_price) со скидками по брендам
- Расчет базовой цены (base_price) с наценками
- Расчет розничной цены (retail_price)
- Учет маржинальности
- Применение персональных скидок B2B клиентам

Выход:

ERP запись:
  article: "СС7335"
  price_raw: 1784.00        ← из прайса Гордеев
  base_price: 1784.00       ← базовая (× 1 для обычных)
  cost_price: 892.00        ← себестоимость (СуперСалют -50%)
  retail_price: 1784.00     ← розничная (без доп. наценки)
  margin: 892.00            ← маржа (base - cost)
  margin_percent: 50%

Архитектура

┌─────────────────────────────────────────────────────────────────┐
│ ПРАЙС-ЛИСТ (xlsx)                                               │
│ Гордеев: [Артикул, Номенклатура, Производитель, Базовая, ...]  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ АДАПТЕР ТОВАРОВ (supplier_gordeev_products.py)                  │
│ Извлекает: article, name, brand, stock_qty, price_raw           │
│ Не считает цены - только маппинг полей                          │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ PIM (NocoDB)                                                    │
│ Хранит: товары с базовыми ценами из прайса                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ АДАПТЕР ЦЕН (supplier_gordeev_pricing.py)                       │
│ Рассчитывает:                                                   │
│   base_price = price_raw × markup (1 или 2)                     │
│   cost_price = base_price × discount (по бренду)                │
│   retail_price = base_price × retail_markup                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ ERP (Odoo / 1C)                                                 │
│ Использует: полный набор цен для продаж и учета                │
└─────────────────────────────────────────────────────────────────┘

Пример: Гордеев

Адаптер товаров

# data/connectors/gordeev/adapter_products.py

class GordeevProductsAdapter:
    """Адаптер товаров Гордеев → PIM"""

    FIELD_MAPPING = {
        'Артикул': 'article',
        'Номенклатура': 'name',
        'Производитель': 'brand',
        'Базовая': 'price_raw',  # ← сохраняем как есть
        'Остаток на складе': 'stock_qty',
        'Видео': 'video_url',
        'Калибр в дюймах': 'caliber',
        'Количество залпов': 'shots',
    }

    def extract(self, row):
        """Извлечь товар из строки прайса"""
        return {
            'article': row['Артикул'].strip(),
            'name': row['Номенклатура'].strip(),
            'brand': row['Производитель'].strip(),
            'price_raw': float(row['Базовая']),  # как есть!
            'stock_qty': int(row['Остаток на складе']),
            'supplier': 'ИП Гордеев',
            # ... остальные поля
        }

Адаптер цен

# data/connectors/gordeev/adapter_pricing.py

class GordeevPricingAdapter:
    """Адаптер цен Гордеев → ERP"""

    BRAND_DISCOUNTS = {
        'Супер Салют': 0.50,  # -50%
        'default': 0.60,      # -40%
    }

    def calculate_prices(self, product):
        """Рассчитать цены по правилам Гордеев"""
        price_raw = product['price_raw']
        brand = product['brand']
        is_fireworks = self._is_fireworks(product['name'])

        # Базовая цена (для продажи)
        base_price = price_raw * 2 if is_fireworks else price_raw

        # Себестоимость (со скидкой по бренду)
        discount = self.BRAND_DISCOUNTS.get(
            brand,
            self.BRAND_DISCOUNTS['default']
        )
        cost_price = base_price * discount

        # Розничная цена (пока = базовая)
        retail_price = base_price

        return {
            'base_price': base_price,
            'cost_price': cost_price,
            'retail_price': retail_price,
            'margin': base_price - cost_price,
        }

    def _is_fireworks(self, name):
        """Проверка на хлопушки/бенгалки"""
        return bool(re.search(r'(хлопушк|бенгал)', name, re.I))

Структура файлов

data/connectors/
└── gordeev/
    ├── adapter_products.py   ← Адаптер товаров → PIM
    ├── adapter_pricing.py    ← Адаптер цен → ERP
    ├── import_to_pim.py      ← Использует adapter_products
    └── sync_to_erp.py        ← Использует adapter_pricing

data/connectors/
└── jf_pyro/
    ├── adapter_products.py
    ├── adapter_pricing.py
    ├── import_to_pim.py
    └── sync_to_erp.py

Правила разделения

Вопрос Адаптер товаров Адаптер цен
Извлечь название? ✅ Да ❌ Нет
Извлечь остаток? ✅ Да ❌ Нет
Извлечь базовую цену из прайса? ✅ Да (price_raw) ❌ Нет
Рассчитать себестоимость? ❌ Нет ✅ Да
Рассчитать розничную цену? ❌ Нет ✅ Да
Применить скидки по брендам? ❌ Нет ✅ Да
Знать о маржинальности? ❌ Нет ✅ Да

Преимущества

  1. Разделение ответственности
    - PIM не знает о бизнес-логике цен
    - ERP не знает о форматах прайсов

  2. Переиспользование
    - Один адаптер товаров → много адаптеров цен
    - Разные цены для опта, розницы, маркетплейсов

  3. Тестирование
    - Тестируем маппинг полей отдельно
    - Тестируем формулы цен отдельно

  4. Масштабирование
    - Легко добавить нового поставщика
    - Легко изменить правила ценообразования


Когда применять

Использовать адаптеры:

Не использовать:


Примеры использования

Импорт товаров в PIM

from data.connectors.gordeev import GordeevProductsAdapter

adapter = GordeevProductsAdapter()
df = pd.read_excel('gordeev.xlsx')

for row in df.iterrows():
    product = adapter.extract(row)
    pim.upsert(product)  # Только товары, цена price_raw

Синхронизация цен в ERP

from data.connectors.gordeev import GordeevPricingAdapter

pricing = GordeevPricingAdapter()
products = pim.get_all(supplier='ИП Гордеев')

for product in products:
    prices = pricing.calculate_prices(product)
    erp.update_prices(product['article'], prices)

Стандарт именования

Адаптер Имя файла Класс
Товары adapter_products.py {Supplier}ProductsAdapter
Цены adapter_pricing.py {Supplier}PricingAdapter

Пример:
- GordeevProductsAdapter
- GordeevPricingAdapter
- JFPyroProductsAdapter
- JFPyroPricingAdapter


Связанные стандарты


Версия: 1.0.0
Дата: 2025-12-24
Автор: Claude Sonnet 4.5