01 01234567890 01234567890

Складские документы в Bitrix24 Коробка: ownerType hex-кодирование и StoreDocumentTable

Битрикс24 API 30 August 2026
~4 минуты чтения 4 559 символа 7 просмотра

Разбираем структуру StoreDocumentTable, типы складских документов через DOC_TYPE, проведение документов и то, почему hex-кодирование ownerType относится к CRM, а не к складскому модулю

Клиент просит: «Свяжите приход товара на складе со сделкой в CRM, чтобы менеджер видел остатки прямо в карточке». Разработчик открывает модуль catalog, находит класс \Bitrix\Catalog\StoreDocumentTable, ищет там поле OWNER_ID или OWNER_TYPE_ID по аналогии с CRM-сущностями — и не находит. Вместо этого натыкается на упоминания hex-кодирования типа владельца где-то в статьях про Bitrix24 и начинает подозревать, что просто не там смотрит. В этой статье разберём, что на самом деле лежит в StoreDocumentTable, как устроены складские документы (приход, расход, перемещение) через поле DOC_TYPE, как их проводить и — отдельно — откуда берётся механизм ownerType с hex-кодированием и почему он не там, где его обычно ищут разработчики складского учёта Bitrix24 Box.

Материал для разработчиков, которые работают с коробочной версией (Box), а не с облаком — модуль catalog и складской учёт в этом объёме доступны только в Box-редакции с установленным модулем catalog.

Что понадобится перед началом

  • Bitrix24 Box с установленным и активным модулем catalog, включённый складской учёт (в панели администратора — «Магазин» → «Настройки» → «Управление складом»)
  • Доступ к коду: свой модуль или возможность подключать классы через local/php_interface — трогать bitrix/modules/catalog напрямую нельзя, слетит при обновлении
  • PHP 7.4+ (Box на актуальных редакциях спокойно работает и на PHP 8.1–8.2, но проверяйте совместимость версии Bitrix)
  • Понимание D7 ORM — если раньше не работали с DataManager, getList, Result, стоит сначала разобраться с базовым синтаксисом D7, здесь объясняться не будет
  • Доступ к базе данных портала (для отладки — смотреть, что реально записалось в b_catalog_store_docs и b_catalog_store_docs_element)
  • Тестовый портал — операции с проведением документов необратимо меняют остатки на складе, экспериментировать на проде не стоит

Основная часть: StoreDocumentTable, складские документы и ownerType hex-кодирование

Шаг 1. Разбираемся со структурой StoreDocumentTable

Класс \Bitrix\Catalog\StoreDocumentTable наследует Bitrix\Main\Entity\DataManager — это D7 ORM, обёртка над таблицей b_catalog_store_docs. По факту это шапка складского документа, сами позиции (товары, количество, склад-источник и склад-назначение) лежат в связанной таблице b_catalog_store_docs_element, которую описывает StoreDocumentElementTable — в getMap() документа она подключена как OneToMany-связь ELEMENTS.

Из полей документа, которые реально есть в getMap(): ID, TITLE, DOC_TYPE (тип документа), DOC_NUMBER, SITE_ID, CONTRACTOR_ID (ссылка на контрагента через ContractorTable), RESPONSIBLE_ID, CURRENCY, STATUS, WAS_CANCELLED, DATE_STATUS, DATE_DOCUMENT, STATUS_BY, TOTAL, COMMENTARY, служебные поля дат и авторов (DATE_CREATE, CREATED_BY, DATE_MODIFY, MODIFIED_BY).

Отдельный нюанс: STATUS — это BooleanField, не строковый статус с тремя значениями, как иногда пишут в неофициальных источниках. Проведён документ или нет — булево значение. Отмена проведённого документа фиксируется отдельным полем WAS_CANCELLED, тоже булевым. То есть документ может быть: черновиком (STATUS = false), проведённым (STATUS = true) или проведённым и затем отменённым (STATUS = true, WAS_CANCELLED = true) — это не одно поле с тремя состояниями, а комбинация двух флагов, и в фильтрах getList это стоит учитывать сразу, иначе легко получить документы «в отмене» там, где ожидали только активные проведённые.

Шаг 2. Смотрим на типы документов через DOC_TYPE

Поле DOC_TYPE определяет, что документ делает с остатками. По REST API (catalog.document.getFields) и по структуре модуля подтверждаются пять типов:

  • A — приход товара на склад
  • S — оприходование (используется, в частности, при инвентаризации, когда фактический остаток отличается от учётного)
  • M — перемещение товара между складами
  • R — возврат товара на склад
  • D — списание (расход) товара со склада

Это не строковые константы, которые вы придумываете сами — они завязаны на логику модуля: например, при типе M документ обязан ссылаться сразу на два склада (STORE_FROM и STORE_TO в элементах), а для A и D заполняется только один. Пользовательские поля для документов конкретного типа в Bitrix24 создаются через userfieldconfig.add с entityId вида CAT_STORE_DOCUMENT_A, CAT_STORE_DOCUMENT_D и так далее — то есть тип документа зашит даже в идентификатор энтити для UF.

<?php
// Создаём документ прихода товара на склад через D7 ORM
use Bitrix\Catalog\StoreDocumentTable;

$result = StoreDocumentTable::add([
    'DOC_TYPE'      => StoreDocumentTable::TYPE_ARRIVAL, // тип 'A' — приход
    'TITLE'         => 'Приход от поставщика №487',
    'DOC_NUMBER'    => '487',
    'RESPONSIBLE_ID' => 1,
    'CURRENCY'      => 'RUB',
    'COMMENTARY'    => 'Поставка по накладной от 20.08.2026',
]);

if ($result->isSuccess()) {
    $docId = $result->getId();
} else {
    // Частая причина ошибки — не заполнен CURRENCY или невалидный DOC_TYPE
    $errors = $result->getErrorMessages();
}

Частая ошибка: указывают DOC_TYPE строкой 'A' вручную вместо константы StoreDocumentTable::TYPE_ARRIVAL. Работать будет одинаково, но при обновлении платформы буквенные коды типов — по опыту работы с другими enum-полями Bitrix — иногда получают дополнительные значения, и хардкод строки труднее сопровождать, чем ссылку на константу класса.

Шаг 3. Добавляем позиции документа

Сам по себе документ без элементов ничего не делает с остатками. Позиции добавляются через StoreDocumentElementTable, каждая строка — это товар, количество и склад (для прихода — только STORE_TO, для расхода — только STORE_FROM, для перемещения — оба поля).

<?php
use Bitrix\Catalog\StoreDocumentElementTable;

// Добавляем позицию — 10 штук товара с ID=3211 приходуем на склад ID=2
$elementResult = StoreDocumentElementTable::add([
    'DOC_ID'          => $docId,
    'STORE_TO'        => 2,
    'ELEMENT_ID'      => 3211,
    'AMOUNT'          => 10,
    'PURCHASING_PRICE' => 1200,
]);

if (!$elementResult->isSuccess()) {
    // Частая причина: ELEMENT_ID указывает на товар, у которого не включён складской учёт
    $errors = $elementResult->getErrorMessages();
}

Частая ошибка на этом шаге: пытаются добавить позицию до того, как товар с ELEMENT_ID включён в складской учёт (галка «Вести учёт по складам» в карточке товара выключена). Позиция добавится в таблицу без ошибки на уровне ORM, но при проведении документ не изменит фактический остаток — и разработчик долго ищет проблему не там.

Шаг 4. Проводим документ

Проведение (в терминологии модуля — conduct) — это операция, которая фиксирует изменение остатков: до проведения документ существует только как запись, после — реально меняет количество товара на складе. На уровне REST API это метод catalog.document.conduct (и обратная операция catalog.document.cancel).

На уровне самого модуля catalog в коробке историческим способом проведения документа остаётся процедурный класс CCatalogDocs (файл bitrix/modules/catalog/general/store_docs.php) — тот же класс, через который построен интерфейс складского учёта в административной панели. D7 ORM (StoreDocumentTable) описывает структуру данных и валидацию, но сам метод проведения документа как часть публичного API StoreDocumentTable в актуальной документации не задокументирован. Если ищете StoreDocumentTable::conduct() и не находите — такого метода нет, логика проведения вынесена за пределы ORM-класса.

<?php
// Проведение документа через процедурный API модуля catalog.
// В актуальных редакциях Box эта операция также доступна через REST-метод
// catalog.document.conduct — используйте его, если работаете из внешнего приложения,
// а не изнутри модуля.
CModule::IncludeModule('catalog');

$conductResult = CCatalogDocs::ConductDocument($docId, $USER->GetID());

if ($conductResult) {
    // Остатки на складе обновлены, STATUS документа станет true
} else {
    global $APPLICATION;
    $error = $APPLICATION->GetException();
}

Здесь стоит честно оговориться: сигнатуру CCatalogDocs::ConductDocument() я привожу по её распространённому употреблению в модуле и по косвенным упоминаниям в связанных источниках, но не нашёл её в актуальной официальной документации dev.1c-bitrix.ru как задокументированный публичный метод — если работаете из кастомного модуля, а не из административного интерфейса, надёжнее ориентироваться на REST-метод catalog.document.conduct, который официально описан и стабилен между обновлениями.

Шаг 5. Разбираемся с ownerType и hex-кодированием

Здесь и начинается путаница, из-за которой многие разработчики теряют время. В CRM-модуле Bitrix24 действительно есть механизм с hex-кодированием типа сущности: для CRM-сущностей с фиксированным типом (сделка, лид, контакт) используются однобуквенные обозначения (D — сделка, L — лид, C — контакт, CO — компания), а для смарт-процессов (SPA, entityTypeId от 128 и выше) буквенного алфавита не хватает, и Bitrix кодирует числовой entityTypeId в шестнадцатеричный вид с префиксом T — например, тип 128 (0x80) превращается в префикс T80. Это задокументированный подход именно для универсальных идентификаторов CRM-сущностей.

Проблема в том, что при прямой проверке структуры StoreDocumentTable и StoreDocumentElementTable (полный список полей getMap() для обеих таблиц) в них нет ни поля OWNER_ID, ни OWNER_TYPE_ID, ни каких-либо признаков hex-кодирования — ни как отдельного поля, ни в самом коде класса. То есть на уровне модуля catalog складской документ сам по себе не хранит привязку к CRM-сущности через ownerType. Это не значит, что связку сделать нельзя — значит, что готового поля для неё нет, и это нужно строить самостоятельно.

На практике связь склада с CRM-сущностью в интеграциях Bitrix24 Box делают одним из трёх способов:

  1. Через CONTRACTOR_ID — если контрагент документа привязан к компании/контакту CRM, это уже частичная связь, но только на уровне «кто поставщик», а не «какая сделка породила документ».
  2. Через пользовательское поле (userfieldconfig.add с entityId = CAT_STORE_DOCUMENT_A и аналогичными для других типов) — туда можно записать ownerType/ownerId вручную, в том числе применить тот же hex-подход, что и в CRM, если типов владельцев много и однобуквенных кодов не хватает. Но это будет решение интегратора, а не встроенный механизм модуля.
  3. Через отдельную связующую таблицу в своём модуле — DOC_ID + ENTITY_TYPE_ID + ENTITY_ID, где ENTITY_TYPE_ID можно кодировать так же, как это делает CRM для SPA (hex от entityTypeId), чтобы формат совпадал с уже принятым в портале.
<?php
// Пример: записываем связь складского документа со сделкой CRM
// через пользовательское поле, используя тот же принцип, что и в CRM —
// буква для типовых сущностей, hex-префикс для смарт-процессов (SPA)

// $entityTypeId получен, например, из CCrmOwnerType::Deal (числовой ID = 2)
// или из ID динамического типа смарт-процесса
$ownerTypeCode = ($entityTypeId >= 128)
    // диапазон 128-191 в CRM зарезервирован под смарт-процессы (SPA),
    // для них используется префикс 'T' + hex-код числового ID
    ? 'T' . strtoupper(dechex($entityTypeId))
    // для типовых сущностей (сделка, лид, компания) буквенный код
    // берётся из справочника типов, который поддерживаете сами или
    // получаете через \CCrmOwnerType::ResolveName()
    : $ownerTypeLetterMap[$entityTypeId] ?? 'U'; // 'U' — неизвестный тип

\CUserTypeEntity::SetUserFields('CAT_STORE_DOCUMENT_A', $docId, [
    'UF_OWNER_TYPE' => $ownerTypeCode,
    'UF_OWNER_ID'   => $dealId,
]);

Обратите внимание: этот код не воспроизводит недокументированный внутренний API, а вручную применяет к складскому документу тот же принцип кодирования, что реально задокументирован для CRM (\CCrmOwnerType, диапазон ID 128–191 для смарт-процессов, префикс T + hex) — это решение на стороне интегратора, а не встроенная функциональность StoreDocumentTable.

Частые ошибки и как их избежать

Ищут метод conduct() прямо в классе StoreDocumentTable и не находят. Проведение документа — это отдельная операция, вынесенная в процедурный класс CCatalogDocs или доступная через REST. D7 ORM здесь отвечает только за структуру и CRUD, но не за бизнес-логику проведения.

Путают STATUS с трёхзначным статусом документа. STATUS — булево поле (проведён/не проведён), отмена проведения — отдельный флаг WAS_CANCELLED. Фильтр ['STATUS' => 'C'] в стиле старого API не сработает на D7-таблице.

Ожидают hex-кодирование ownerType прямо в StoreDocumentTable. Такого поля в таблице нет — механизм принадлежит CRM-модулю (в первую очередь смарт-процессам), а не catalog. Связку с CRM-сущностью нужно строить самостоятельно — через UF-поле или отдельную таблицу.

Добавляют элементы документа для товара без включённого складского учёта. Запись создаётся без ошибки, но при проведении остаток не меняется — товар просто не участвует в складской логике.

Из практики А2

В одном из проектов с розничной сетью (10+ магазинов) заказчик хотел видеть в карточке сделки, с какого склада фактически отгружен товар — стандартной связки между StoreDocumentTable и сделкой в CRM не было, ровно по причине, описанной выше: поля-владельца в таблице документа просто нет. Решили не городить хранимую процедуру или триггер MySQL (первая идея заказчика — «пусть база сама проставляет»), а завести отдельную таблицу-связку crm_store_doc_link (DOC_ID, ENTITY_TYPE_ID, ENTITY_ID) и заполнять её в обработчике события OnAfterAddStoreDocument. Отдельно пришлось учитывать, что часть заказов у клиента шла не через сделки, а через смарт-процесс «Заявка на отгрузку» — там entityTypeId был динамическим (за пределами стандартных типов), и без hex-кодирования по правилам CrmOwnerType формат ENTITY_TYPE_ID в связке разъехался бы с остальной CRM-логикой портала уже на втором смарт-процессе.

Итог

Теперь понятно, что реально лежит в StoreDocumentTable и StoreDocumentElementTable, как работают типы документов через DOC_TYPE, чем STATUS отличается от WAS_CANCELLED и почему hex-кодирование ownerType, которое встречается в статьях про Bitrix24, относится к CRM-модулю, а не к складскому учёту напрямую — и как эту связку достроить самому, если она нужна в интеграции. Если на каком-то шаге застряли или структура вашего проекта требует другой связки со смарт-процессами — опишите ситуацию в комментарии или напишите нам напрямую.

Азамат

Азамат

Основатель «А Квадрат»

Работаю в web-деве с 2014 года. Прошел путь от фрилансера до фрилансера-руководителя))

Обновление сделок через REST API Bitrix24: поля, статусы, массовые операции
Следующая статья 28.08.2026