Перемещение товаров — это одна из базовых складских операций, которая требует особого внимания при разработке. Нам необходимо не просто изменить цифры в регистре, но и гарантировать, что товар действительно есть на складе-отправителе, а также обеспечить целостность данных при одновременной работе нескольких пользователей. В этой статье мы подробно разберем, как создать механизм перемещения "с нуля", используя возможности встроенного языка и конструктора запросов.
Прежде чем приступать к написанию кода в модуле документа, проанализируем структуру наших объектов. Для реализации задачи нам понадобятся:
Склад, Номенклатура. Ресурс: Количество.СкладОтправитель, СкладПолучатель.Номенклатура, Количество.Важно убедиться, что в свойствах документа на закладке "Движения" отмечен наш регистр ТоварыНаСкладах, а стратегия удаления движений установлена в значение "Удалять автоматически при перепроведении".
Рассмотрим логику проведения. Нам нужно забрать товар с одного склада и положить на другой. В терминах регистров накопления это означает формирование двух видов движений: Расход для склада-отправителя и Приход для склада-получателя. Проанализируем, как это реализовать максимально эффективно с помощью одного запроса.
Откроем модуль документа ПеремещениеТоваров и создадим процедуру ОбработкаПроведения. Мы будем использовать пакетный запрос или объединение, чтобы получить данные для движений. Рассмотрим пример кода, который подготавливает данные:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Очищаем движения перед записью (если не стоит автоудаление)
Движения.ТоварыНаСкладах.Записывать = Истина;
Движения.ТоварыНаСкладах.Очистить();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Номенклатура КАК Номенклатура,
| СУММА(ТЧ.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| Документ.ПеремещениеТоваров.СписокНоменклатуры КАК ТЧ
|ГДЕ
| ТЧ.Ссылка = &Ссылка
|ГРУППИРОВАТЬ ПО
| ТЧ.Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Товары.Номенклатура КАК Номенклатура,
| ВТ_Товары.Количество КАК Количество,
| &СкладОтправитель КАК Склад,
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход) КАК ВидДвижения
|ИЗ
| ВТ_Товары КАК ВТ_Товары
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| ВТ_Товары.Номенклатура,
| ВТ_Товары.Количество,
| &СкладПолучатель,
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
|ИЗ
| ВТ_Товары КАК ВТ_Товары";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("СкладОтправитель", СкладОтправитель);
Запрос.УстановитьПараметр("СкладПолучатель", СкладПолучатель);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Движение = Движения.ТоварыНаСкладах.Добавить();
Движение.ВидДвижения = Выборка.ВидДвижения;
Движение.Период = Дата;
Движение.Номенклатура = Выборка.Номенклатура;
Движение.Склад = Выборка.Склад;
Движение.Количество = Выборка.Количество;
КонецЦикла;
// Записываем движения, чтобы они отразились в базе перед проверкой остатков
Движения.ТоварыНаСкладах.Записать();
// Далее следует блок контроля остатков
ВыполнитьКонтрольОстатков(Отказ);
КонецПроцедуры
Обратите внимание, что мы сгруппировали товары в табличной части. Это необходимо на случай, если пользователь ввел одну и ту же номенклатуру в разные строки — для регистра важен общий итог по позиции.
Проанализируем ситуацию: почему мы записываем движения до того, как проверили наличие товара? Это и есть суть "Новой методики" проведения. Мы сначала фиксируем намерение (записываем расход), а затем проверяем, не ушел ли остаток по регистру в "минус". Если ушел — отменяем транзакцию установкой Отказ = Истина.
Выясним причину популярности этого метода: он требует меньше обращений к базе данных и более устойчив к блокировкам в высоконагруженных системах. Разберем, как должен выглядеть запрос для проверки остатков после записи движений:
Процедура ВыполнитьКонтрольОстатков(Отказ)
ЗапросОстатков = Новый Запрос;
ЗапросОстатков.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&МоментКонтроля,
| Склад = &СкладОтправитель И Номенклатура В (
| ВЫБРАТЬ Номенклатура ИЗ Документ.ПеремещениеТоваров.СписокНоменклатуры ГДЕ Ссылка = &Ссылка
| )
| ) КАК ТоварыНаСкладахОстатки
|ГДЕ
| ТоварыНаСкладахОстатки.КоличествоОстаток < 0";
ЗапросОстатков.УстановитьПараметр("СкладОтправитель", СкладОтправитель);
ЗапросОстатков.УстановитьПараметр("Ссылка", Ссылка);
// Важный момент: если проведение оперативное, проверяем на текущий момент (пустая граница),
// если задним числом - на момент времени документа.
ГраницаКонтроля = Новый Граница(МоментВремени(), ВидГраницы.Включая);
ЗапросОстатков.УстановитьПараметр("МоментКонтроля", ГраницаКонтроля);
Результат = ЗапросОстатков.Выполнить();
Если Не Результат.Пустой() Тогда
Отказ = Истина;
ВыборкаОшибок = Результат.Выбрать();
Пока ВыборкаОшибок.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара " + ВыборкаОшибок.Номенклатура +
". Дефицит: " + Строка(-ВыборкаОшибок.Остаток);
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
КонецПроцедуры
В многопользовательской среде может возникнуть проблема: два кладовщика одновременно проводят перемещение одного и того же товара, которого на складе всего 1 штука. Без блокировок оба запроса к остаткам могут вернуть "1 шт.", оба документа проведутся, и остаток станет "-1".
Чтобы этого избежать, проанализируем механизм БлокировкаДанных. Нам нужно заблокировать остатки по складу-отправителю и списку номенклатуры перед выполнением запроса.
Рассмотрим шаги настройки блокировки:
БлокировкаДанных.ТоварыНаСкладах.Исключительный.Пример кода блокировки:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Склад", СкладОтправитель);
ЭлементБлокировки.ИсточникДанных = СписокНоменклатуры;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
Эту вставку необходимо поместить в самое начало процедуры ОбработкаПроведения до того, как начнут считываться какие-либо данные из базы.
Проанализируем основные "грабли", на которые наступают начинающие разработчики, судя по обсуждениям на форумах:
&Склад) передается не ссылка на справочник, а строка или другой объект. Всегда проверяйте типы данных в отладчике.ПередЗаписью при отмене проведения.МоментВремени() важно помнить про ВидГраницы.Включая. Если использовать просто момент времени без границы, запрос может не увидеть только что записанные движения самого документа.Мы рассмотрели полный цикл реализации перемещения товаров. Подытожим: правильный алгоритм включает в себя установку управляемых блокировок, формирование движений по обоим складам в одном цикле, запись этих движений и последующий контроль отрицательных остатков по складу-отправителю. Использование пакетных запросов и временных таблиц позволит вашему коду работать быстро даже при большом объеме номенклатуры в документе.