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