Когда учёт ведётся в облаке, подключение сайта выглядит проще, чем с 1С: не нужен сервер обмена, не нужны папки с правами на запись, не нужен протокол с этапами. Но простота обманчива ровно в одном месте — в структуре данных. Разберём, как на самом деле устроена синхронизация, если делать обмен сайта с МойСклад не через готовый модуль, а понимая, что происходит.
Чем это отличается от CommerceML
Главное отличие — направление инициативы. В обмене с 1С инициатором выступает учётная система: она сама стучится на сайт, авторизуется, загружает файлы и ждёт ответа. С облачным учётом наоборот: сайт сам ходит за данными по HTTP, когда ему удобно, и получает JSON вместо XML.
Базовый адрес выглядит так: https://api.moysklad.ru/api/remap/1.2, дальше путь до нужной сущности. Никаких файлов выгрузки не существует вообще — есть набор ресурсов, каждый из которых отдаёт страницу данных. Это удобнее в отладке: любой запрос можно повторить руками и увидеть ровно то, что видит сайт.
Плата за удобство — нет готового механизма на стороне платформы. Если на 1С-Битрикс обмен по CommerceML работает из коробки, то синхронизацию с облачным учётом почти всегда пишут под проект. Что выбрать в принципе, мы разбирали в отдельной статье, здесь речь о том, как это работает после того, как выбор сделан.
Токен и права: с чего начинается подключение
Авторизация бывает двух видов: логин с паролем в заголовке Basic или токен в заголовке Authorization: Bearer. Для сайта правильный вариант только второй, и причины не в красоте.
- Токен выдаётся отдельному служебному пользователю — его права видно и можно сузить до нужных разделов
- Смена пароля сотрудника не роняет обмен, потому что обмен к этому сотруднику не привязан
- Токен отзывается в один клик, если его засветили в репозитории или в переписке
- В журнале действий видно, что изменил именно сайт, а не человек — при разборе спорных ситуаций это решает
- Служебному пользователю не нужны права на удаление: сайт никогда ничего не удаляет в учёте
Отдельно про хранение: токен живёт в переменных окружения, а не в коде и не в таблице настроек, доступной из админки. Это не паранойя — токен даёт полный доступ к учёту в пределах прав пользователя, включая цены закупки и контрагентов.
Товар, модификация, комплект, услуга — четыре разные сущности
Здесь спотыкаются чаще всего, потому что в каталоге сайта это обычно одна таблица. В учёте — четыре независимых справочника с разными полями.
- Товар — карточка номенклатуры без вариантов. Путь <code>entity/product</code>
- Модификация — конкретный вариант товара: размер, цвет, фасовка. Путь <code>entity/variant</code>, у каждой есть ссылка на родительский товар и набор характеристик
- Комплект — набор, который продаётся как единица, но собирается из других позиций. Путь <code>entity/bundle</code>, остаток у него считается по компонентам
- Услуга — позиция без остатка вообще. Путь <code>entity/service</code>
Если тянуть только товары, каталог приедет без вариантов: у магазина одежды вместо тридцати позиций будет десять, и ни у одной не будет размеров. Если тянуть только модификации — потеряются товары без вариантов. Поэтому в каталоге сайта нужно заранее решить, что считается карточкой, а что торговым предложением, и держать это соответствие.
Товар и модификация в учёте — объекты с разными идентификаторами. Карточка на сайте может соответствовать товару, а цена и остаток при этом живут на модификациях. Пока это не разведено в структуре базы сайта, обмен будет приносить непредсказуемые цифры.
Ассортимент: одна ручка вместо четырёх
Чтобы не собирать каталог из четырёх запросов, есть сводный ресурс entity/assortment. Он отдаёт всё, что можно продать, одним списком, и у каждой записи в поле meta.type указано, что это за сущность: product, variant, bundle или service.
Это ресурс только для чтения — создать через него ничего нельзя. Но для первичной заливки каталога и для регулярной сверки он удобнее всего: один проход, одна логика разбора, в ответе уже есть и цены, и признак архивности. Писать изменения назад, если это вообще нужно, приходится в конкретную сущность по её собственному адресу.
Остатки лежат не в карточке товара
Вторая классическая ошибка. В карточке товара остатка нет — он считается по документам и отдаётся отчётами: report/stock/all для сводных цифр и report/stock/bystore с разбивкой по складам.
В отчёте не одно число, а несколько, и путать их нельзя:
- stock — физический остаток, то, что лежит на складе
- reserve — зарезервировано под заказы покупателей
- inTransit — ожидается от поставщика
- quantity — доступное к продаже: физический остаток минус резерв плюс ожидание
На витрину обычно уходит quantity, иногда — stock минус резерв без учёта ожидаемого. Выбор не технический: если показывать ожидаемое, сайт будет продавать то, что ещё не приехало. Решение принимает тот, кто отвечает за товародвижение, и фиксируется оно до начала работ, а не после первой жалобы. Почему цифры в учёте и на сайте расходятся даже при исправном обмене, мы подробно разбирали здесь — для облачного учёта список причин почти тот же.
Полезная деталь: у отчёта по остаткам есть фильтр режима, который позволяет получать только положительные остатки. На каталоге в десятки тысяч позиций это сокращает ответ в разы и экономит те самые лимиты запросов, про которые ниже.
Цены: массив, типы и копейки
Цена в учёте — не одно число. В карточке лежит массив цен продажи, у каждой свой тип: розничная, оптовая, дилерская, акционная. Отдельно хранятся закупочная цена и минимальная цена.
- Типы цен настраиваются в учёте и имеют свои идентификаторы — сайт должен знать, какой из них его, и брать цену по типу, а не по порядковому номеру в массиве
- Все суммы приходят в копейках: 1490 рублей в ответе выглядит как 149000. Делить на сто нужно один раз и в одном месте кода
- Закупочную цену на сайт не отдают никогда — её даже не стоит тянуть в запросе, если служебному пользователю можно эти права не давать
- Если на сайте есть личный кабинет с дилерскими ценами, тип цены выбирается по контрагенту, и это отдельная логика, а не поле в карточке
- Скидки, рассчитанные в учёте, и акции, настроенные на сайте, складываются непредсказуемо — источник правды по скидкам должен быть один
Страницы, лимиты и код 429
Ответ всегда постраничный. Количество записей задаётся параметром limit, смещение — offset, в метаданных ответа приходит общее количество. Максимальный размер страницы ограничен, и при использовании expand для подтягивания связанных объектов ограничение становится жёстче — это нормально, но код обхода страниц должен это учитывать, а не предполагать, что всё приедет за один раз.
Важнее другое: на частоту обращений есть лимит. Он считается в запросах за короткий интервал и ограничивает число одновременных соединений. При превышении приходит код 429 и в заголовках — сколько ждать до следующей попытки. Конкретные цифры лежат в документации и со временем меняются, поэтому полагаться надо не на них, а на правильное поведение клиента.
- Обработка 429 с повторной попыткой и нарастающей паузой — обязательна, без неё обмен будет падать на больших каталогах случайным образом
- Параллельных запросов немного и больше не станет: обходить каталог лучше последовательно, но большими страницами
- Обмен не должен запускаться из веб-запроса пользователя: только из очереди или по расписанию, иначе страница отвалится по таймауту на середине
- Два одновременных обмена одного и того же — худший сценарий. Нужна блокировка, которая не даёт запуститься второму экземпляру
- Любой ответ с кодом 5xx на стороне сервиса — это повод повторить попытку позже, а не повод считать товар отсутствующим
Последний пункт звучит очевидно, но именно на нём теряют каталоги: обмен не смог получить данные, получил пустой ответ, добросовестно записал нулевые остатки, и магазин на несколько часов остался без товаров в наличии. Правильная логика — пустой или ошибочный ответ не меняет ничего.
Что тянуть целиком, а что по изменениям
Полная выгрузка каталога нужна редко. У сущностей есть поле времени последнего изменения, и по нему работает фильтр: запросить только то, что менялось после определённого момента. Это превращает ночной обход на двадцать минут в минутный запрос раз в четверть часа.
- Остатки и цены — часто, по изменениям, с запасом по времени на случай расхождения часов
- Описания, характеристики, картинки — редко, полным проходом, ночью
- Полная сверка раз в сутки всё равно нужна: она ловит то, что по какой-то причине не приехало инкрементально
- Время последнего успешного обмена нужно хранить и показывать в админке — это первый вопрос при любом разборе
- Товары, пропавшие из учёта или помеченные архивными, скрываются с витрины, но страница остаётся: удалять нельзя, она уже в индексе
Заказы в обратную сторону
Заказ покупателя создаётся как документ entity/customerorder, и у него обязательные связи: организация, от лица которой продаём, контрагент-покупатель и склад, с которого отгружаем. Позиции заказа ссылаются на конкретную сущность ассортимента — товар, модификацию или комплект — и содержат количество и цену в копейках.
- Покупателя надо во что-то превратить: искать существующего контрагента по телефону или почте либо создавать нового. Решение принимает тот, кто ведёт учёт, иначе справочник контрагентов за год превращается в свалку дублей
- У документа можно задать внешний идентификатор — тот же номер, что на сайте. Без него сверка заказов превращается в сопоставление по дате и сумме
- Повторная отправка одного и того же заказа при сбое не должна создавать второй документ: проверка по внешнему идентификатору перед созданием обязательна
- Доставка, сборка, скидка — это или отдельные позиции в документе, или поля документа. Как именно, зависит от того, как потом считают выручку
- Статусы заказа на сайте и статусы документа в учёте — разные справочники. Нужна таблица соответствия, и в ней должно быть указано, чей статус главнее при расхождении
Отдельно про оплату: факт оплаты на сайте и приходный документ в учёте — не одно и то же. Если нужна связка с онлайн-кассой и эквайрингом, это проектируется сразу, потому что дописывать её к работающему обмену дороже.
Вебхуки: когда помогают, а когда мешают
Вместо того чтобы спрашивать учёт об изменениях, можно попросить его сообщать самому. Подписка настраивается на событие создания, изменения или удаления сущности, и на указанный адрес приходит уведомление.
- Обработчик должен отвечать мгновенно и ставить задачу в очередь, а не обрабатывать на месте: медленный ответ приводит к повторным попыткам
- Уведомление может прийти дважды — обработчик обязан быть идемпотентным
- Уведомление может не прийти вовсе: сеть, перезапуск, ошибка. Поэтому вебхуки дополняют регулярную сверку, а не заменяют её
- Массовая правка цен в учёте породит лавину уведомлений — очередь должна их склеивать, а не обрабатывать каждое
- Адрес обработчика должен быть доступен снаружи по HTTPS, что не всегда так на тестовых контурах
Практический вывод: вебхуки хороши для заказов и срочных изменений остатков по ходовым позициям. Весь каталог на них строить не стоит — получится система, которая работает, пока не пропустит одно уведомление, и после этого расходится молча.
Что ломается на практике
- Сопоставление потеряно: товары на сайте привязаны по артикулу, артикул в учёте изменили — каталог задублировался. Привязка должна быть по идентификатору сущности, он не меняется никогда
- Остаток берётся с товара, а варианты не учтены: витрина показывает сумму по всем размерам
- Цена не делится на сто или делится дважды — ловится на первой же карточке, если проверять глазами, а не только логами
- Обмен запускается по расписанию, но предыдущий ещё идёт: два процесса пишут в одну таблицу
- Кеш каталога не сбрасывается после обмена: в базе данные свежие, на витрине вчерашние
- Картинки тянутся при каждом обмене заново — обмен работает часами вместо минут. Скачивать надо только изменившиеся
- Нет уведомления о сбое: обмен не работает три дня, узнают от покупателя
- Товар скрыт в учёте как архивный, а на сайте остался в продаже, потому что обработан только признак остатка
Порядок внедрения, который себя оправдывает
- Зафиксировать источник правды по каждому полю: цена, остаток, название, описание, картинки — что приезжает из учёта, а что ведётся на сайте
- Решить, что на сайте является карточкой, а что торговым предложением, и как это соответствует товарам и модификациям
- Выбрать показатель остатка и склады, с которых продаёт сайт
- Выбрать тип цены и описать, что происходит со скидками
- Сделать односторонний обмен каталога и проверить его на десятке контрольных позиций руками
- Добавить обмен заказами с проверкой на повторную отправку
- Настроить расписание, блокировку одновременного запуска и уведомления о сбоях
- Записать в журнал время каждого обмена и вывести его в админку
Первые четыре пункта — не разработка, а решения владельца бизнеса. Их отсутствие и есть основная причина, по которой интеграции переделывают: код работает ровно так, как его попросили, а попросили исходя из догадок.
Мы делаем синхронизацию с МойСклад под конкретный каталог, с расписанием, журналом и уведомлениями о сбоях, а не как разовый скрипт. Если каталог только проектируется, интеграцию логичнее заложить в проект сразу — смотрите разработку интернет-магазина. Если в учёте уже лежат остатки, которые должны одновременно уезжать и на сайт, и на площадки, это соседняя задача — выгрузка на маркетплейсы. Обмен, который уже работает и требует присмотра, ведётся в рамках поддержки.
Как проверить, что повторный обмен не создаёт дубли
В плане приёмки нужен не только первый успешный запуск. Проверьте повтор того же события и восстановление после сбоя: интеграция должна находить уже созданную запись по согласованному идентификатору. Отдельно определите, какая система отвечает за каждое поле.
- Повторно передайте один контрольный товар: на сайте должна обновиться существующая карточка, если именно так согласована схема.
- Создайте тестовый заказ с пометкой, проверьте передачу в учёт и повторную обработку того же заказа.
- Сравните свободный остаток и резерв в одном и том же складе и в один момент времени.
- Зафиксируйте, что происходит при временной недоступности системы: где видна ошибка и кто получает уведомление.
Точные форматы и ограничения проверяйте по документации JSON API МойСклад для используемой версии. В Funny Jobs применена двусторонняя связь с магазином. Для оценки вашего обмена нужны CMS, схема данных и пример товара или заказа.