Почему в запросе 1С остатки возвращают NULL и как избежать задвоения строк при списании товара?

Программист 1С v8.3 (Обычные формы) Управленческий учет
← На главную

При разработке механизмов проведения документов в 1С программисты часто сталкиваются с ситуацией, когда запрос к остаткам регистра накопления возвращает неожиданные результаты: значения NULL, задвоенные суммы или некорректные итоги по группировкам. Разберем по шагам, почему возникают эти проблемы, как правильно использовать функцию ЕСТЬNULL в паре с агрегатными функциями и как структурировать запрос, чтобы данные не "размножались" при соединении таблиц.

Разбираемся с NULL в результатах запроса

Рассмотрим ситуацию: вы выполняете ЛЕВОЕ СОЕДИНЕНИЕ табличной части документа с виртуальной таблицей остатков. Если по какой-то позиции номенклатуры на складе нет остатка, то в колонке из регистра придет NULL. Проанализируем, как правильно обрабатывать это значение, чтобы избежать ошибок в вычислениях.

Существует два подхода к использованию функции ЕСТЬNULL в агрегатных запросах, и они работают по-разному:

  1. СУММА(ЕСТЬNULL(Поле, 0)) — здесь система сначала заменяет каждое значение NULL в каждой строке на ноль, а затем складывает их. Этот метод безопасен, но может быть чуть менее производительным на огромных выборках.
  2. ЕСТЬNULL(СУММА(Поле), 0) — этот вариант считается более профессиональным. Сначала SQL-сервер суммирует все имеющиеся значения. Если записей для суммирования не нашлось вовсе (результат соединения пуст), СУММА вернет NULL, который внешняя функция заменит на 0.

Важный нюанс: как было отмечено в обсуждении, конструкция СУММА(ЕСТЬNULL(Поле, 0)) иногда может приводить к ошибке SDBL "Недопустимое преобразование типов" на некоторых версиях платформы (например, 8.3.9), если тип поля не определен однозначно. Поэтому рекомендуется использовать второй вариант.

Почему происходит задвоение остатков?

Выясним причину, по которой количество товара в выборке может увеличиваться в несколько раз. Это происходит, когда в табличной части документа одна и та же номенклатура (или пара Номенклатура + Партия) встречается несколько раз (например, с разной ценой или в разных строках). При левом соединении с остатками к каждой строке документа прицепится полный остаток по этой позиции со склада.

Если в документе две строки по "Товару А" (по 5 штук в каждой), а на складе всего 10 штук, то после простого соединения и суммирования вы получите: 5 шт (строка 1) + 10 шт (остаток) и 5 шт (строка 2) + 10 шт (остаток). Итоговая сумма остатка по группировке превратится в 20, что не соответствует действительности.

Чтобы этого избежать, проанализируем правильный алгоритм:

  1. Сначала помещаем данные табличной части во временную таблицу (ВТ_ДокТч).
  2. Обязательно группируем данные в ВТ_ДокТч по всем измерениям, по которым будем соединяться с регистром (Номенклатура, Партия, Склад).
  3. Только после этого выполняем соединение с таблицей остатков.

Посмотрим на пример правильной подготовки временной таблицы:


|ВЫБРАТЬ
|    Тч.Номенклатура КАК Номенклатура,
|    Тч.Партия КАК Партия,
|    СУММА(Тч.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_ДокТч
|ИЗ
|    Документ.ПеремещениеТоваров.СписокНоменклатуры КАК Тч
|ГДЕ
|    Тч.Ссылка = &Ссылка
|СГРУППИРОВАТЬ ПО
|    Тч.Номенклатура,
|    Тч.Партия
;

Использование итогов и корректный обход выборки

Часто программисты забывают добавить поля в секцию ИТОГИ. Если вы хотите видеть остаток не только в детальных записях, но и в группировках по номенклатуре, это поле должно быть агрегировано.

Рассмотрим структуру запроса с итогами:


|ВЫБРАТЬ
|    ВТ.Номенклатура КАК Номенклатура,
|    ВТ.Партия КАК Партия,
|    ВТ.Количество КАК КоличествоДокумент,
|    ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток
|ИЗ
|    ВТ_ДокТч КАК ВТ
|    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПродуктыНаСкладах.Остатки(&Период, Склад = &Склад) КАК Остатки
|    ПО ВТ.Номенклатура = Остатки.Номенклатура
|    И ВТ.Партия = Остатки.Партия
|ИТОГИ
|    СУММА(КоличествоДокумент),
|    СУММА(КоличествоОстаток)
|ПО
|    Номенклатура

Внимание: При обходе такого результата в коде 1С крайне важно использовать правильные параметры метода Выбрать(). Если вы используете группировки, обход должен выглядеть так:


Результат = Запрос.Выполнить();
// Получаем выборку по верхнему уровню (Номенклатура)
ВыборкаНоменклатура = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаНоменклатура.Следующий() Цикл
    // Здесь мы видим СУММАРНЫЙ остаток по всей номенклатуре (по всем партиям в документе)
    Если ВыборкаНоменклатура.КоличествоДокумент > ВыборкаНоменклатура.КоличествоОстаток Тогда
        Сообщить("Недостаточно товара: " + ВыборкаНоменклатура.Номенклатура);
    КонецЕсли;

    // Для детальных записей по партиям внутри номенклатуры:
    ВыборкаДетальные = ВыборкаНоменклатура.Выбрать();
    Пока ВыборкаДетальные.Следующий() Цикл
        // Обработка конкретной строки/партии
    КонецЦикла;
КонецЦикла;

Списание с конкретной партии и подбор по FIFO

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

Для реализации такой логики обычно применяют следующий подход:

  1. В запросе получают остатки в разрезе всех партий по нужной номенклатуре.
  2. Упорядочивают их так, чтобы первой шла партия из документа (можно использовать конструкцию ВЫБОР КОГДА Партия = &ПартияИзДокумента ТОГДА 0 ИНАЧЕ 1 КОНЕЦ).
  3. В цикле обхода выборки уменьшают требуемое количество (переменная ОсталосьСписать) до тех пор, пока она не станет равной нулю, распределяя списание по доступным строкам остатков.

Важный совет: При получении остатков всегда используйте Граница с моментом времени документа и видом Исключая, если вам нужны остатки "ДО" проведения текущего документа. Если использовать просто Дата, вы рискуете получить данные на начало секунды, что в условиях многопользовательской работы или перепроведения может дать неверный результат.

Таким образом, мы проанализировали основные причины появления пустых значений и дублей в запросах. Главное правило: всегда группируйте исходные данные документа перед тем, как соединять их с виртуальными таблицами регистров накопления, и грамотно управляйте уровнями группировок при обходе результата.

← На главную