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