Перейти к содержимому

Складские документы - приход, списание, перемещение, резерв

Меняем остатки товаров правильно: складским документом, а не записью в таблицу. Разбираем создание черновика, проведение, типы документов и правильное снятие резерва.

Механика

Остатки по складам живут отдельно от общего количества товара в каталоге. При включённом складском учёте общее количество складывается из остатков по складам и правку руками теряет.

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

Типы складских документов различаются обязательным набором полей в строке. Приход и оприходование указывают склад назначения, списание и снятие резерва - склад источника, перемещение - оба склада сразу.

У приходного документа есть свои обязательные поля в шапке. Ему нужны поставщик и валюта, потому что в строке хранится закупочная цена позиции.

Ошибка в отдельной строке никак не отменяет создание всего документа целиком. Метод вернёт идентификатор, а позиция может не сохраниться, поэтому строки после создания перечитывают и сверяют.

Проведение складского документа молча не срабатывает ровно в двух известных случаях. Складской учёт выключен или магазин работает в режиме внешнего каталога учётной системы.

Свободного остатка отдельным полем в этой таблице остатков просто не существует. Таблица хранит общий остаток и резерв, а свободное количество считают вычитанием одного из другого.

Резерв снимают отдельным складским документом, а не правкой числа в таблице остатков. Прямая запись расходится с заказами, и склад с заказами начинают жить разными жизнями.

У товара с вариантами складские остатки хранятся у каждого конкретного предложения. Запись на родительскую карточку выглядит успешной, но на витрине магазина не появляется.

Ошибочно проведённый документ откатывают отменой самого проведения, а не удалением его записи. Удалять его как обычный черновик нельзя: проведённые движения уже учтены в остатках.

Документы дают историю движений товара, которой прямая запись не даёт никогда. По ним видно, кто и когда изменил остаток, и любое расхождение с заказами разбирается по записям, а не по памяти сотрудников склада.

Шаги

  1. Убедиться, что складской учёт включён и текущий режим каталога это позволяет.
  2. Завести поставщика заранее, если создаётся приходный документ от него.
  3. Создать черновик документа нужного типа с правильным набором полей в строках.
  4. Перечитать строки созданного документа и сверить их с исходным списком позиций.
  5. Провести готовый документ и проверить полученный результат по остаткам нужного склада.
  6. Ошибочный документ отменять обратным действием, а не удалением его записи.

Код

Создаём приход от поставщика:

$documentId = \CCatalogDocs::add([
'DOC_TYPE' => \Bitrix\Catalog\StoreDocumentTable::TYPE_ARRIVAL,
'SITE_ID' => 's1',
'TITLE' => 'Поступление товара',
'CURRENCY' => 'RUB', // нужна из-за закупочной цены
'CONTRACTOR_ID' => $contractorId, // поставщик, у прихода обязателен
'RESPONSIBLE_ID' => $userId,
'ELEMENT' => [
['ELEMENT_ID' => $productId, 'STORE_TO' => $storeId,
'AMOUNT' => 10, 'PURCHASING_PRICE' => 900],
],
]);

Создание проверяет только шапку документа: его тип, сайт и ответственного сотрудника. Остатки на этом шаге не меняются, документ остаётся черновиком до отдельного действия.

Проверяем, что все строки сохранились:

$rows = \Bitrix\Catalog\StoreDocumentElementTable::getList([
'select' => ['ELEMENT_ID', 'STORE_TO', 'AMOUNT'],
'filter' => ['=DOC_ID' => $documentId],
])->fetchAll();
// строк меньше, чем передавали - часть позиций молча не прошла проверку

Ошибка одной позиции не влияет на результат создания документа. Сверка строк - единственный способ узнать о потере, и делать её надо до проведения, а не после жалобы кладовщика.

Проводим документ:

if (!\CCatalogDocs::conductDocument($documentId, $userId)) {
global $APPLICATION;
$e = $APPLICATION->GetException();
throw new \RuntimeException($e ? $e->GetString() : 'документ не проведён');
}
// именно этот вызов меняет остатки на складах

Проведение возвращает признак успеха, а причину отказа кладёт в объект ошибки приложения. Тихий отказ почти всегда означает выключенный складской учёт или режим внешнего каталога.

Списываем и перемещаем товар:

// списание: указывают склад-источник
$deductId = \CCatalogDocs::add([
'DOC_TYPE' => \Bitrix\Catalog\StoreDocumentTable::TYPE_DEDUCT,
'SITE_ID' => 's1', 'RESPONSIBLE_ID' => $userId,
'ELEMENT' => [['ELEMENT_ID' => $productId, 'STORE_FROM' => $storeId, 'AMOUNT' => 1]],
]);
// перемещение требует обоих складов: STORE_FROM и STORE_TO в одной строке

Набор полей строки зависит от типа документа и молча прощает лишние. Строка списания со складом назначения вместо источника создаётся без ошибки, но при проведении ведёт себя не так, как ожидает автор.

Считаем свободный остаток:

$row = \Bitrix\Catalog\StoreProductTable::getList([
'select' => ['AMOUNT', 'QUANTITY_RESERVED', 'STORE_TITLE' => 'STORE.TITLE'],
'filter' => ['=PRODUCT_ID' => $productId, '=STORE_ID' => $storeId],
])->fetch();
$free = max(0, (float)$row['AMOUNT'] - (float)$row['QUANTITY_RESERVED']);
// отдельного поля со свободным остатком в таблице нет

Витрина показывает остаток, а продать можно только свободное количество. Разница между ними и есть резерв под уже оформленные, но ещё не отгруженные заказы.

Снимаем резерв и отменяем ошибочный документ:

$undoId = \CCatalogDocs::add([
'DOC_TYPE' => \Bitrix\Catalog\StoreDocumentTable::TYPE_UNDO_RESERVE,
'SITE_ID' => 's1', 'RESPONSIBLE_ID' => $userId,
'ELEMENT' => [['ELEMENT_ID' => $productId, 'STORE_FROM' => $storeId, 'AMOUNT' => 1]],
]);
\CCatalogDocs::cancellationDocument($wrongDocumentId, $userId); // откат проведения

Снятие резерва - такой же документ, как приход или списание. Правка поля резерва напрямую оставляет заказы с их резервами, а склад - со своими числами, и сойтись им уже негде.

Читаем остатки товара с вариантами:

$offers = \CCatalogSKU::getOffersList($productId); // предложения родителя
$ids = array_keys($offers[$productId] ?? []);
$rows = \Bitrix\Catalog\StoreProductTable::getList([
'select' => ['PRODUCT_ID', 'STORE_ID', 'AMOUNT'],
'filter' => ['@PRODUCT_ID' => $ids], // остатки лежат у предложений
])->fetchAll();

Складские остатки товара с вариантами хранятся у конкретных предложений. Запрос по идентификатору родительской карточки вернёт пусто, и это выглядит как потеря данных, хотя данные на месте.

Ограничения

Складской учёт в магазине включают осознанно и заранее, до первых заказов. После включения общее количество товара считается из остатков по складам, и прежняя ручная правка количества перестаёт работать.

Проведённый документ уже не черновик и убирается совсем иначе. Его отменяют, а не удаляют, иначе движения по остаткам остаются без объяснения.

Прямая запись в таблицу остатков допустима только при самой первой загрузке данных. На работающем магазине она разъезжается с резервами по заказам в первый же день.

Режим внешнего каталога учётной системы отключает складские документы полностью. Там остатками распоряжается учётная система, а сайт только принимает готовые числа при очередном обмене.

Складские документы не заменяют полноценный товарный учёт. Партии, себестоимость и взаиморасчёты с поставщиками остаются за учётной системой, а сайту достаётся текущее количество на складах и резерв под заказы.

Типичные проблемы

Документ создан, а остатки прежние.

Создание записи даёт только черновик будущего документа. Остатки меняет отдельное действие - проведение, и без него документ ни на что не влияет.

Метод вернул идентификатор, но позиций в документе меньше.

Ошибка отдельной строки не отменяет создание всего документа. Строки после создания документа перечитывают и сверяют с исходным списком позиций.

Проведение возвращает отказ без объяснения.

Складской учёт выключен или магазин работает в режиме внешнего каталога учётной системы. Текст причины отказа лежит в объекте ошибки приложения.

В таблице остатков не находится свободное количество.

Такого поля нет: есть общий остаток и резерв. Свободное количество считают вычитанием резерва из складского остатка.

У товара с вариантами остатков не видно.

Складские остатки хранятся у торговых предложений, а не у родительской карточки товара. Читать и писать такие остатки нужно только по идентификаторам самих предложений.

Склад и заказы разошлись в числах.

Остатки или резерв правили прямой записью в таблицу мимо документов. Все движения на работающем магазине идут документами, иначе сверить их уже нечем.

Частые вопросы

Чем оприходование отличается от прихода?

Приход - поступление от поставщика с закупочной ценой и контрагентом. Оприходование правит остаток без поставщика: им пользуются после инвентаризации, когда фактическое количество разошлось с учётным.

Как снять резерв с заказа?

Отдельным документом снятия резерва на нужный склад и количество. Правка поля резерва в таблице оставит заказ с его резервом и разведёт числа окончательно.

Можно ли удалить проведённый документ?

Нет, сначала отменяют проведение, и только потом документ становится черновиком. Удаление проведённого документа оставило бы движения по остаткам без причины.

Почему остаток на витрине больше, чем можно купить?

Витрина показывает складской остаток, а покупка ограничена свободным количеством. Разница - это резерв под оформленные, но не отгруженные заказы.

Нужны ли документы, если остатки приходят из 1С?

Нет, при обмене остатками числа приходят готовыми из учётной системы. Документы нужны магазинам, которые ведут склад на стороне сайта.

Смежное

Первоисточник