+7 499 938 8452 пн.-пт. 10:00 – 17:00

Не нашли ответы на свои вопросы в наших публикациях? Задайте вопрос в службу техподдержки!


Варианты синхронизации заказов и остатков

Синхронизация заказов и остатков

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.phpisCatalogUseStoreControl()
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 / OnSaleOrderSavedAcrit\Core\Orders\EventHandleronBitrixStatusChanged / onBitrixTrackingChanged.

Это статусы и трек, не загрузка заказа и не выгрузка остатков.


3. Что происходит с остатком, когда создаётся заказ

Acrit не вызывает CCatalogProduct::Update и не пишет StoreProduct, чтобы уменьшить остаток.

Что делает код (OrderSync::runSync):

  1. Собирает корзину Sale (updateOrderProducts).
  2. При новом заказе или смене состава: $order->refreshData(); $order->doFinalAction(true); затем save.
  3. При необходимости выставляет STORE_ID отгрузки. Если Sale отвергает склад (ограничения доставки), заказ всё равно создаётся, склад может дописаться отложенным SQL.
  4. После успешного 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 / OnProductUpdateExporter::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. Известные ловушки (из кода и практики)

  1. Ключ leftover SKU|склад. Склад 0 вместо id склада обнуляет SKU текущей сессии.
  2. Два профиля экспорта на один склад площадки: шаг сброса хвостов одного профиля затирает остатки другого.
  3. Сообщение «остатки успешно выгружены» нельзя вешать на пачки обнуления.
  4. Ozon: при настроенных складах общий stock может давать ошибку API — нужны stock_{warehouseId}.
  5. WB FBO-приоритет может обнулять FBS, если на FBO ещё есть количество.
  6. YM LIVE_STOCKS выкл. → в /stocks уходит устаревший снимок → оверселл после резерва.
  7. touchCachedCounts не пушит Ozon/WB.
  8. AUTO_GENERATE ≠ выгрузка на API.
  9. Пустая корзина / cartblock=Y — состав заказа не обновляется.
  10. Внешние запросы не работают в ajax/cli/админке (OnProlog их пропускает).

8. Что сделать для режима «заказ на Ozon → остаток на всех интеграциях»

Честно «сразу на все МП одной галкой» нельзя:

  • Ozon и WB принимают только исходящий API, с лимитами;
  • полный шаг leftover нельзя вызывать на один SKU;
  • 1С может перезаписать каталог следом.

8.1. Рекомендуемый контур

Триггер — не «заказ Ozon», а изменение остатка в каталоге
(заказ МП, импорт 1С, ручной заказ, правка в админке — одно событие).

Уже ловятся: OnProductUpdate, OnStoreProduct*. Сейчас из них только AUTO_GENERATE. Нужно добавить:

  1. Запись в очередь: product_id + store_id (или только товар, если склады выкл.) + время.
  2. Склейка 5–15 секунд (пачка под лимиты API; при 1С-хозяине — дольше или «после конца обмена»).
  3. Для каждого профиля экспорта с включёнными остатками: только пачка stocks API этих SKU, без leftover/reset.
  4. Маппинг: QUANTITY_AVAILABLE или CATALOG_STORE_AMOUNT_{id}warehouseId площадки.
  5. Блокировка: не пересекаться с полным прогоном того же профиля.

Плюсы: одна механика на заказ 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. Этапы внедрения

  1. Контракт профиля экспорта: отдельная галка «оперативные остатки»; только профили с EXPORT_STOCKS=Y и настроенным складом МП; leftover не вызывать.
  2. Триггер: существующие события каталога → очередь SKU+склад.
  3. Склейка: агент 5–15 с или ограниченная обработка на OnEpilog; при обмене 1С — ждать конец импорта.
  4. Пуш: тот же формат пачки, что stepExportStocks, без reset.
  5. 1С: явная настройка клиента. Хозяин 1С — пуш после импорта. Хозяин Bitrix — пуш после резерва Sale.
  6. Склады: вкл. — ключ 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 читает то, что уже лежит в каталоге.



Назад в раздел



Часто задаваемые вопросы по модулям экспорта

Видео плейлист по настройке модулей экспорта