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