Не нашли ответы на свои вопросы в наших публикациях? Задайте вопрос в службу техподдержки!
Синхронизация заказов и остатков
1. Архитектура: два контура
Площадка (Ozon / WB / YM …) │ ▼ Плагин заказов → Controller::syncExtToStore → OrderSync::runSync │ ▼ Заказ Sale → doFinalAction(true) → save │ ▼ Bitrix Catalog: резерв / списание QUANTITY, QUANTITY_RESERVED или AMOUNT склада (складской учёт) │ ├─► YandexLiveStocks::touchCachedCounts ← только снимок YM │ ├─► OnProductUpdate / OnStoreProduct* │ │ │ ▼ │ очередь AUTO_GENERATE ← снимок в БД модуля, не API площадки │ └─► (отдельно, по расписанию) профиль экспорта → шаг EXPORT_STOCKS → API Ozon / WB / YM / …
Ключевой разрыв: шаг «заказ сохранён» не вызывает stepExportStocks у профилей экспорта.
Важные файлы
| Роль | Путь |
|---|---|
| Оркестратор заказов | lib/orders/controller.php |
| Создание/обновление заказа | lib/orders/ordersync.php |
| Агенты заказов | lib/orders/periodsync.php |
| Cron заказов | lib/orders/exporter.php, cli/export/export.php |
| События Sale → площадка | lib/orders/eventhandler.php |
| Внешние URL, очередь каталога | lib/export/eventhandler.php |
| Снимок / live YM | lib/export/yandexlivestocks.php |
| Очередь AUTO_GENERATE | lib/export/exporter.php (addToQueue, processQueue) |
| Трекер изменений остатка | lib/export/productchangetracker.php |
| Складской учёт (хелпер) | lib/helper.php → isCatalogUseStoreControl() |
| UI синхронизации заказов | admin/orders/include/tabs/sync.php |
2. Контур заказов: какие варианты есть
Общий путь после получения заказа с площадки:
Controller::syncExtToStore → поиск заказа в магазине → OrderSync::runSync (или только статус, если MODULE_ACTION = change_status).
Пропускаются неактивный профиль, заказы вне start_date / end_date, статусы из «не использовать». Есть MySQL named lock acrit_ord_*.
2.1. Агент Bitrix (один период)
- Где:
PeriodSync::set/PeriodSync::run, типPlugin::ADD_SYNC_TYPE_SINGLE(по умолчанию). - Когда: в профиле
SYNC[add_active]=Yи задан период. - Плюсы: просто; не нужен crontab.
- Минусы: зависит от хитов сайта; окно lookback легко задать слишком узко и пропустить заказы.
2.2. Два агента (минуты + часы)
- Где:
getAddSyncType() = ADD_SYNC_TYPE_DUAL— Ozon, WB, YM/YM2, Kaspi и ряд других. - Плюсы: свежие заказы часто; «хвост» догоняется длинным окном.
- Минусы: два прогона; риск гонок (частично снимает lock).
2.3. Cron (CLI)
- Где:
cli/export/export.php→ для модуля заказов вызываетсяPeriodSync::run(та же логика, что у агента). - Плюсы: стабильнее агента на нагруженном сайте.
- Минусы: нужен crontab; это всё ещё опрос API, не real-time.
2.4. Ручной запуск
- Где: вкладка «Синхронизация»,
admin/orders/include/actions/man_sync_run.php. - Плюсы: период (день / неделя / всё), опция только новых, контроль глазами.
- Минусы: не автоматика; шаги по ~100 секунд.
2.5. Внешний запрос / уведомления (push)
Роутер: EventHandlerExport::OnProlog. Нужны опция модуля allow_external_request = Y и в профиле EXTERNAL_REQUEST + URL.
| Плагин | Механизм | Комментарий |
|---|---|---|
| Яндекс.Маркет 2 | API-уведомления (notificationType, PING, проверка IP) | Заказ почти сразу; опрос остаётся запасным каналом |
| Яндекс.Маркет (старый) | cart / order-accept / status / cancel | Устаревшая схема |
| Ozon, WB и большинство остальных | нет push заказов | Только опрос агентом/cron |
Плюсы push: низкая задержка.
Минусы: настройка URL, белый список, не универсально для всех площадок.
2.6. Обратный контур (магазин → площадка)
События OnSaleOrderBeforeSaved / OnSaleOrderSaved → Acrit\Core\Orders\EventHandler → onBitrixStatusChanged / onBitrixTrackingChanged.
Это статусы и трек, не загрузка заказа и не выгрузка остатков.
3. Что происходит с остатком, когда создаётся заказ
Acrit не вызывает CCatalogProduct::Update и не пишет StoreProduct, чтобы уменьшить остаток.
Что делает код (OrderSync::runSync):
- Собирает корзину Sale (
updateOrderProducts). - При новом заказе или смене состава:
$order->refreshData(); $order->doFinalAction(true);затем save. - При необходимости выставляет
STORE_IDотгрузки. Если Sale отвергает склад (ограничения доставки), заказ всё равно создаётся, склад может дописаться отложенным SQL. - После успешного save:
YandexLiveStocks::touchCachedCounts($elementIds)— только таблицы снимков YM.
Списание / резерв (поведение Sale + Catalog при включённом количественном учёте и/или складском учёте).
Товары, которые модуль заказов сам создаёт в каталоге (Products::createCatalogProduct): QUANTITY = 0, QUANTITY_TRACE = N, CAN_BUY_ZERO = Y. Такие позиции не резервируются как обычная карточка.
Если количественный учёт выключен или «можно купить при нуле» (заказ появится, каталог не изменится, пушить на другие площадки будет нечего).
4. Контур остатков (экспорт)
Выгрузка остатков не привязана к заказу. Это шаги профиля экспорта.
4.1. Полный / остаточный прогон профиля
- Cron, агент экспорта, ручной запуск в админке.
- Флаги вида
EXPORT_STOCKS, у Ozon ещёEXPORT_STOCKS_ONLY/ режим «остатки и цены». - Склады площадки мапятся на поля каталога:
CATALOG_QUANTITY,QUANTITY_AVAILABLE,CATALOG_STORE_AMOUNT_{id}и варианты с резервом.
Плюсы: единый пайплайн, пачки, склады, обнуление «лишних» SKU.
Минусы: задержка до следующего cron; шаг leftover опасен при двух профилях на один склад.
4.2. AUTO_GENERATE по событию каталога
OnProductAdd/OnProductUpdate→Exporter::addToQueue.OnStoreProductAdd/OnStoreProductUpdate→ то же +ProductChangeTracker::trackStoreStock.- На
OnEpilog/ редиректе (processQueue: пересчёт строк в БД модуля для профилей сAUTO_GENERATE=Y).
Плюсы: снимок в модуле свежий сразу после изменения каталога.
Минусы: площадка об этом не узнает, пока не выполнится шаг API остатков.
4.3. «Быстрые остатки» Яндекс.Маркета (LIVE_STOCKS)
- По умолчанию включено.
- Ответы
/stocksи/cartсчитают остаток в момент запроса:
QUANTITY − |QUANTITY_RESERVED| или значение из маппинга профиля (в том числе склад).
- После заказа
touchCachedCountsобновляет COUNT в таблицахacrit_yandex_marketplace_stocks/acrit_yandex_market_api_stocks.
Плюсы: Маркет не ждёт cron; меньше оверселла после резерва.
Минусы: работает только пока Маркет сам дергает URL. У Ozon и WB такого канала нет.
4.4. Сброс «хвостов» (leftover / RESET_MARKET_STOCKS)
- Обнуляет на площадке SKU, которых нет в текущей сессии выгрузки.
- Ключ keep:
SKU|идентификатор склада. Склад0вместо реального id обнуляет чужие позиции.
Этот шаг нельзя запускать «на один товар после заказа».
5. Складской учёт: выключен и включён
Опция каталога: catalog / default_use_store_control (Helper::isCatalogUseStoreControl()).
В логике заказов Acrit этой галки нет: поведение задаёт Sale после doFinalAction.
В экспорте поля складов показываются, если склады заведены (проверка isCatalogUseStoreControl в выборе полей закомментирована).
| Складской учёт выкл. | Складской учёт вкл. | |
|---|---|---|
| Где остаток | b_catalog_product.QUANTITY, QUANTITY_RESERVED | b_catalog_store_product.AMOUNT по складам |
| Заказ Acrit | Резерв на уровне товара, если QUANTITY_TRACE не «нет» | Нужен верный STORE_ID отгрузки |
| Поля экспорта | CATALOG_QUANTITY, QUANTITY_AVAILABLE | CATALOG_STORE_AMOUNT_{id}, *_RESERVED, *_AVAILABLE — явно мапить на склад МП |
| Риск «сразу на все МП» | Один остаток на все выгрузки (проще | Профиль A: склад 1 → Ozon, профиль B: склад 2 → WB). Пуш общего QUANTITY даёт оверселл или ноль не там |
| YM live без маппинга | getCatalogAvailable = товарный доступный | Если мапинг не на склад (снова товарный QUANTITY), это уже ошибка учёта |
| Отгрузка без склада | Обычно не критично | Sale может отвергнуть STORE_ID; резерв мог пройти не с того склада |
6. Обмен с 1С (ERP)
В acrit.core нет клиента CommerceML, sale.export.1c, catalog.import.1c. Упоминания «1С» в коде почти всегда значат 1С-Битрикс (CMS).
Остатки и заказы с ERP идут штатным обменом Bitrix. Acrit видит только результат в каталоге и в заказах Sale.
| Схема клиента | Если пушить остатки сразу после заказа Ozon | Как согласовать |
|---|---|---|
| 1С (хозяин остатков), Bitrix витрина | Резерв Sale уменьшит Bitrix, через цикл обмена 1С перезапишет QUANTITY «как в 1С» без этого заказа → на МП уйдёт старый остаток, оверселл | Сначала заказ в 1С, 1С считает склад, импорт в Bitrix, уже этот остаток пушить на МП. «Сразу» = после импорта 1С |
| Bitrix (хозяин, 1С забирает заказы | Резерв Sale) источник правды. Пуш по каталогу корректен | Можно пушить после doFinalAction. Не включать leftover на один SKU |
| Оба пишут QUANTITY | Гонка: заказ Ozon, импорт 1С, выгрузка Acrit | Очередь с паузой и дедупом; не пушить в окне обмена 1С |
| Склады 1С + складской учёт | 1С пишет AMOUNT, Sale резервирует по STORE_ID | Явный маппинг: склад 1С ↔ склад Bitrix ↔ склад Ozon/WB в каждом профиле экспорта |
7. Известные ловушки (из кода и практики)
- Ключ leftover
SKU|склад. Склад0вместо id склада обнуляет SKU текущей сессии. - Два профиля экспорта на один склад площадки: шаг сброса хвостов одного профиля затирает остатки другого.
- Сообщение «остатки успешно выгружены» нельзя вешать на пачки обнуления.
- Ozon: при настроенных складах общий
stockможет давать ошибку API (нужныstock_{warehouseId}). - WB FBO-приоритет может обнулять FBS, если на FBO ещё есть количество.
- YM
LIVE_STOCKSвыкл. → в/stocksуходит устаревший снимок → оверселл после резерва. touchCachedCountsне пушит Ozon/WB.AUTO_GENERATE ≠выгрузка на API.- Пустая корзина /
cartblock=Y— состав заказа не обновляется. - Внешние запросы не работают в ajax/cli/админке (
OnPrologих пропускает).
8. Что сделать для режима «заказ на Ozon → остаток на всех интеграциях»
Честно «сразу на все МП одной галкой» нельзя:
- Ozon и WB принимают только исходящий API, с лимитами;
- полный шаг leftover нельзя вызывать на один SKU;
- 1С может перезаписать каталог следом.
8.1. Рекомендуемый контур
Триггер (не «заказ Ozon»), а изменение остатка в каталоге
(заказ МП, импорт 1С, ручной заказ, правка в админке (одно событие)).
Уже ловятся: OnProductUpdate, OnStoreProduct*. Сейчас из них только AUTO_GENERATE. Нужно добавить:
- Запись в очередь:
product_id+store_id(или только товар, если склады выкл.) + время. - Склейка 5–15 секунд (пачка под лимиты API; при 1С-хозяине (дольше или «после конца обмена»)).
- Для каждого профиля экспорта с включёнными остатками: только пачка stocks API этих SKU, без leftover/reset.
- Маппинг:
QUANTITY_AVAILABLEилиCATALOG_STORE_AMOUNT_{id}→warehouseIdплощадки. - Блокировка: не пересекаться с полным прогоном того же профиля.
Плюсы: одна механика на заказ Ozon и на импорт 1С.
Минусы: новая очередь, лимиты, маппинг складов, настройка «кто хозяин остатков».
8.2. Хук только после OrderSync (недостаточно
Сработает для заказа МП, не увидит импорт 1С и ручное списание). Для клиента, где 1С хозяин остатков, пуш до пересчёта 1С вреден. Имеет смысл как дополнительный touch (как сейчас YM), не как единственный канал.
8.3. Чего не делать
| Идея | Почему нет |
|---|---|
| Полный прогон всех профилей на каждый заказ | Минуты, лимиты API, обнуление чужих SKU |
| Считать LIVE_STOCKS универсальным | Ozon и WB сами остаток не спрашивают |
Списывать остаток через CCatalogProduct::Update | Двойное списание с Sale, поломка складов и обмена 1С |
Всегда слать общий QUANTITY при включённых складах | Оверселл на одном складе, ноль на другом |
8.4. Этапы внедрения
- Контракт профиля экспорта: отдельная галка «оперативные остатки»; только профили с
EXPORT_STOCKS=Yи настроенным складом МП; leftover не вызывать. - Триггер: существующие события каталога → очередь SKU+склад.
- Склейка: агент 5–15 с или ограниченная обработка на OnEpilog; при обмене 1С (ждать конец импорта).
- Пуш: тот же формат пачки, что
stepExportStocks, без reset. - 1С: явная настройка клиента. Хозяин 1С (пуш после импорта). Хозяин Bitrix (пуш после резерва Sale).
- Склады: вкл. — ключ
product|store_id, в профиль только если этот store замаплен. Выкл. — ключ товара, поле доступного количества.
9. Итог
| Вопрос | Ответ по коду |
|---|---|
| Заказ с Ozon сразу меняет остаток на WB? | Нет |
| Кто меняет остаток в Bitrix? | Sale/Catalog после doFinalAction, не Acrit напрямую |
| Когда остаток уйдёт на Ozon/WB? | Следующий прогон профиля экспорта с остатками |
| Где «сразу» уже есть? | Только YM live /stocks и /cart |
| Учитывается ли галка складского учёта в заказах Acrit? | Нет, смотрит ядро Bitrix |
| Есть ли обмен с 1С ERP в модуле? | Нет |
| Как сделать нужный режим? | Очередь по событию каталога → пачка stocks без leftover; правило «хозяин: Bitrix или 1С»; маппинг складов |
Сейчас «сразу» есть только у Яндекса, потому что он сам спрашивает. Для цепочки Ozon → WB / другие склады Ozon / Avito нужен исходящий пуш по событию каталога, без полного прогона и без leftover, с явным правилом относительно 1С и складов Bitrix. Иначе либо задержка cron, либо оверселл, либо обнуление чужих остатков.
Краткий вывод
Сейчас нет режима «заказ с Ozon → остаток сразу на все интеграции».
Заказы и выгрузка остатков (два независимых контура). Общая точка (каталог 1С-Битрикс). Дальше каждый контур работает по своему расписанию.
- Заказ с площадки создаёт заказ Sale. Модуль сам не уменьшает
QUANTITY. - Резерв и списание делает ядро Bitrix (количественный учёт, складской учёт).
- Выгрузка остатков на Ozon, WB и другие площадки идёт прогоном профиля экспорта (cron / агент / вручную).
- Исключение: Яндекс.Маркет с «быстрыми остатками» (Маркет сам спрашивает
/stocksи/cart), ответ считается из каталога в момент запроса. - Обмена с 1С (ERP) в этом репозитории нет: это штатный обмен Bitrix. Acrit читает то, что уже лежит в каталоге.
Назад в раздел
