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