Три года назад я впервые подключил 1С к маркетплейсу. Казалось: ну что там сложного? API отдаёт заказы, в базе есть номенклатура, нужно просто связать одно с другим. Найти товар, проверить остатки, создать документ. Делов на пару дней.
С тех пор я собрал коллекцию историй, каждая из которых начинается одинаково: "У нас что-то не то отгружается". И почти каждая сводится к одному и тому же: мы неправильно определяем, какой именно товар имеется в виду.
Эта статья — про конкретный случай с Wildberries, который обошёлся в несколько десятков ошибочных отгрузок и одну неделю разбирательств. И про более широкую проблему: почему привычки из розничного учёта ломаются, когда вы выходите на маркетплейс.
Один артикул — два товара
Торговая сеть спортивной одежды. Несколько сотен SKU: кроссовки, футболки, худи, аксессуары. Продажи идут через собственные точки и через Wildberries. Задача интеграции стандартная: API маркетплейса отдаёт список новых заказов, для каждого нужно найти номенклатуру в базе 1С, подобрать склад с остатками, сформировать документ перемещения на точку выдачи.
Всё работало несколько месяцев без единой жалобы. А потом со склада пришло сообщение: клиент заказал футболку размера L/XL, а в документе перемещения стоит S/M. Товар тот же — тот же бренд, та же модель, тот же цвет. Размер другой.
Первая мысль: кладовщик ошибся. Проверяю заказ в личном кабинете WB — размер L/XL. Проверяю данные из API — размер L/XL. Проверяю документ в 1С — номенклатура "Футболка Nike Dri-FIT DX0006 S/M". Ошибка не на складе. Ошибка в коде.
Как работал код сопоставления
Открываю модуль интеграции. Логика поиска номенклатуры простая до прозрачности:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.АртикулПоставщика = &Артикул";
Запрос.УстановитьПараметр("Артикул", ДанныеЗаказа.supplierArticle);
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
НайденнаяНоменклатура = Результат.Номенклатура;
КонецЕсли;
Видите проблему? Запрос ищет по артикулу поставщика и берёт первую попавшуюся запись. Если артикул уникален — всё отлично. Если нет — лотерея.
А в спортивной одежде артикул — это модель, не единица хранения. Футболка Nike Dri-FIT с артикулом DX0006 существует в справочнике как две разные номенклатуры: DX0006 S/M и DX0006 L/XL. Два товара. Один артикул. Запрос возвращает первый попавшийся — какой окажется выше в индексе таблицы, тот и попадёт в документ.
Иногда правильный. Иногда нет. Классический случай: баг, который не воспроизводится стабильно, потому что зависит от порядка записей в базе данных.
Почему проблема не проявлялась раньше
Обувь. У кроссовок каждый размер — это отдельный артикул. Полуботинки Nike Revolution 7 размера 42 имеют артикул FB2207-001, а размер 43 — FB2207-002. Аксессуары тоже: один артикул — один товар. Система работала корректно для 80% ассортимента.
Проблема вылезла только когда на Wildberries выставили одежду с размерным рядом. Это произошло тихо: менеджер загрузил карточки, заказы пошли, документы создавались. Никто не заметил разницу, пока клиент не пожаловался. А до того — сколько отгрузок было с неправильным размером? Мы проверили задним числом. 23 документа за две недели. В 9 из них размер не совпадал.
Решение: штрихкод вместо артикула
В API Wildberries у каждого заказа есть поле skus — массив штрихкодов. Штрихкод (баркод, EAN) уникален для конкретной комбинации модель + размер + цвет. Это именно тот уровень детализации, который нужен для однозначного сопоставления. И этот же штрихкод хранится в карточке номенклатуры в 1С — в реквизите или в регистре сведений "Штрихкоды".
Поле skus было в ответе API с самого начала. Код интеграции его просто игнорировал.
Исправленная логика работает в два шага:
- Приоритет — штрихкод. Берём массив
skusиз заказа и ищем номенклатуру, у которой штрихкод совпадает с любым значением из этого массива. Если нашли — готово, однозначное соответствие. - Фолбэк — артикул. Если массив
skusпуст (бывает в старых заказах) или штрихкод не найден в базе, тогда ищем поsupplierArticle. Но теперь с оговоркой: если запрос вернул больше одной записи, документ не создаётся, а в лог пишется предупреждение для ручного разбора.
// Шаг 1: поиск по штрихкоду
НайденнаяНоменклатура = Неопределено;
Если ДанныеЗаказа.skus.Количество() > 0 Тогда
Для Каждого Штрихкод Из ДанныеЗаказа.skus Цикл
НайденнаяНоменклатура = НайтиНоменклатуруПоШтрихкоду(Штрихкод);
Если ЗначениеЗаполнено(НайденнаяНоменклатура) Тогда
Прервать;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Шаг 2: фолбэк на артикул
Если НЕ ЗначениеЗаполнено(НайденнаяНоменклатура) Тогда
Результат = НайтиНоменклатуруПоАртикулу(ДанныеЗаказа.supplierArticle);
Если Результат.Количество = 1 Тогда
НайденнаяНоменклатура = Результат.Номенклатура;
Иначе
ЗаписатьВЛог("Неоднозначное сопоставление: артикул "
+ ДанныеЗаказа.supplierArticle
+ ", найдено записей: " + Результат.Количество);
КонецЕсли;
КонецЕсли;
Одно поле. Один уровень приоритета. Разница — клиент получает свой размер или чужой.
После изменения мы прогнали все заказы за последний месяц через новую логику в тестовом режиме. Расхождения нашлись только в тех 9 документах, которые уже знали. Новых ошибок — ноль. Правка заняла полдня, включая тестирование.
Что вообще такое "артикул" на маркетплейсе
Вот в чём корень проблемы: слово "артикул" значит разные вещи для разных участников процесса.
- Для поставщика артикул — это внутренний код модели. Он может быть уникальным (обувь), а может и нет (одежда с размерным рядом).
- Для Wildberries
supplierArticle— это то, что поставщик написал при создании карточки. Маркетплейс его не валидирует на уникальность. Может быть что угодно, вплоть до "футболка синяя". - Для 1С артикул — обычно реквизит справочника "Номенклатура". Уникальность зависит от настроек конкретной базы.
- Для OZON артикул (
offer_id) — это уже ключ конкретного SKU, уникальный в рамках продавца. Совсем другая семантика.
Когда разработчик интеграции пишет НайтиПоАртикулу(supplierArticle), он неявно предполагает, что все четыре определения совпадают. В нашем случае первые три разошлись, и система стала работать "иногда".
Универсальное правило: если маркетплейс даёт штрихкод — используйте штрихкод. Если даёт свой внутренний ID — храните его отдельно и сопоставляйте по нему. Артикул поставщика — последний рубеж обороны, а не первый.
Другие грабли из практики интеграций с маркетплейсами
За три года работы с маркетплейсами я накопил достаточно историй, чтобы заполнить отдельный блог. Вот несколько, которые запомнились.
OZON FBO и партионный учёт: когда FIFO играет против вас
Крупный поставщик продуктов питания, склад FBO на OZON. Возврат товара. По логике OZON, при возврате товар возвращается на остатки. В 1С нужно оприходовать возврат и привязать его к правильной партии — для учёта сроков годности и себестоимости.
Проблема: API возврата не говорит, из какой именно партии был товар. Разработчик интеграции решил вопрос "по логике FIFO": берём самую старую партию с остатками, к ней и привязываем возврат. Звучит разумно.
На практике получилось иначе. Когда в один день пришло 14 возвратов одного и того же SKU, система привязала все 14 к одной партии — самой старой. В результате на этой партии образовался отрицательный виртуальный остаток (возвратов больше, чем было продано из этой партии), а на других партиях остатки зависли. Бухгалтерия потратила два дня на ручной разбор.
Правильное решение оказалось проще: не привязывать возвраты FBO к конкретной партии вообще, а создавать их как новую партию поступления с текущей датой. Себестоимость брать из средневзвешенной. Это не идеально с точки зрения учёта, но это единственный способ, который не ломается при масштабировании.
Тестирование без реальных заказов
Отдельная боль — как тестировать интеграцию, если реальных заказов ещё нет или их нельзя трогать.
У Wildberries нет полноценного sandbox. Есть тестовые методы для некоторых эндпоинтов, но полную цепочку "заказ-сборка-отгрузка" в тестовом режиме прогнать нельзя. У OZON sandbox формально существует, но данные в нём устаревшие и поведение отличается от продуктива.
Что работает на практике:
- Записывать реальные ответы API в файлы и прогонять обработку по ним в тестовой базе 1С. Грубо, но надёжно. Я храню набор из 15-20 типовых ответов: простой заказ, заказ с несколькими позициями, заказ с размерным рядом, отмена, частичная отмена, возврат.
- Мок-сервер на Python. Простейший Flask-сервис, который отдаёт JSON из файлов. В 1С меняем URL API на localhost:5000. 30 строк кода — и можно тестировать любые сценарии, включая таймауты и ошибки HTTP 429.
- Параллельный запуск "только чтение". Подключаем интеграцию к реальному API, но документы не проводим, а записываем в отдельный регистр сведений. Сравниваем с тем, что менеджеры создают вручную. Через неделю параллельной работы видно все расхождения.
Третий способ — самый ценный. Именно он помог обнаружить баг с артикулами: в параллельном режиме расхождения по размерам были видны сразу, но к тому моменту интеграцию уже запустили в боевом режиме без этого этапа.
Маппинг данных: одно поле, пять интерпретаций
Каждый маркетплейс называет одни и те же сущности по-разному. Вот что значит "цена" в разных контекстах:
- Wildberries: цена до скидки (
price), скидка продавца (discount), итоговая цена рассчитывается маркетплейсом. Комиссия и логистика вычитаются позже, в отчёте о продажах. - OZON: цена "до" (
old_price), цена "после" (price), маркетинговая цена (marketing_price), минимальная цена (min_price). Четыре поля вместо одного, и каждое влияет на видимость товара. - Яндекс.Маркет: базовая цена, цена со скидкой, цена для программы лояльности. Плюс отдельная логика "привязки к рекомендованной цене".
Когда пишешь универсальный модуль обмена для нескольких маркетплейсов, соблазн велик: сделать одно поле "Цена" и маппить его на все площадки. Не делайте так. Через месяц окажется, что менеджер выставил цену 5000 на WB, имея в виду цену до скидки, а модуль отправил 5000 как финальную цену на OZON, потому что маппинг одинаковый. Потеря маржи — 15% на каждой продаже, пока заметили.
Каждый маркетплейс — это отдельная ценовая модель. Хранить нужно все поля отдельно и конвертировать при выгрузке.
Чему меня научили маркетплейсы
Маркетплейсы — это отдельный мир со своей логикой. Они не ложатся на привычную схему розничного учёта "поступление-продажа-списание". У них своё понимание остатков (FBO vs FBS, транзитные склады, виртуальные резервы). Своя модель ценообразования. Своя система идентификации товаров.
Несколько выводов, которые я сделал за эти годы:
- Читайте документацию API целиком, а не только нужный эндпоинт. Поле
skusбыло описано в документации Wildberries. Я его видел, когда читал описание метода. Но разработчик, который писал первую версию интеграции, прочитал только те поля, которые были нужны для минимально работающей версии.supplierArticleвыглядел достаточным. И был достаточным — до определённого момента. - Не доверяйте полю, если не проверили его уникальность. Простой запрос
ВЫБРАТЬ РАЗЛИЧНЫЕ АртикулПоставщика ИЗ Номенклатура СГРУППИРОВАТЬ ПО АртикулПоставщика ИМЕЮЩИЕ КОЛИЧЕСТВО(*) > 1показал бы проблему за 5 секунд до того, как она стала проблемой. - Фолбэк лучше, чем отказ. Новый код не ломает работу для товаров, где штрихкода нет в базе. Он просто возвращается к старой логике с дополнительной проверкой. Жёсткий подход "нет штрихкода — нет документа" правильный в теории, но парализует работу на практике.
- Параллельный режим перед боевым. Неделя работы в режиме "считаем, но не проводим" стоит дешевле одного дня ручного разбора ошибок.
Самое ироничное в этой истории: баг с артикулами — не баг. Это корректная работа кода в соответствии с его логикой. Код делал ровно то, что было написано: искал по артикулу, брал первый результат. Проблема была не в коде, а в предположении, на котором код был построен. Артикул уникален. Нет, не всегда.
В ритейле артикул — это "что". Штрихкод — это "какой именно". Пока разницу не замечаешь, система работает. Просто иногда наугад.


