Клиент просит: «Свяжите приход товара на складе со сделкой в 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 делают одним из трёх способов:
- Через
CONTRACTOR_ID— если контрагент документа привязан к компании/контакту CRM, это уже частичная связь, но только на уровне «кто поставщик», а не «какая сделка породила документ». - Через пользовательское поле (
userfieldconfig.addсentityId = CAT_STORE_DOCUMENT_Aи аналогичными для других типов) — туда можно записатьownerType/ownerIdвручную, в том числе применить тот же hex-подход, что и в CRM, если типов владельцев много и однобуквенных кодов не хватает. Но это будет решение интегратора, а не встроенный механизм модуля. - Через отдельную связующую таблицу в своём модуле —
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-модулю, а не к складскому учёту напрямую — и как эту связку достроить самому, если она нужна в интеграции. Если на каком-то шаге застряли или структура вашего проекта требует другой связки со смарт-процессами — опишите ситуацию в комментарии или напишите нам напрямую.