Как вывести среднюю цену номенклатуры в форме списка справочника?

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

Одной из частых задач при разработке в среде 1С является вывод дополнительных расчетных данных непосредственно в форму списка. В нашем случае мы рассмотрим, как отобразить среднюю цену закупки товара, используя данные из регистров накопления. Эта задача имеет несколько вариантов решения: от простых запросов в динамическом списке до архитектурно сложных, но производительных систем с использованием регламентных заданий.

Вариант 1. Прямой запрос в динамическом списке (Левое соединение)

Рассмотрим самый очевидный способ, который часто выбирают начинающие разработчики. Мы можем изменить основной запрос динамического списка, добавив в него соединение с виртуальной таблицей оборотов регистра накопления. Важно понимать, что расчет средней цены должен базироваться на формуле Сумма / Количество, а не на усреднении самих цен из документов, так как это даст математически неверный результат.

Проанализируем пример запроса, который можно вставить в поле "Настройка списка -> Запрос":


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

Разберем, почему мы используем вложенный запрос. Если соединять справочник напрямую с таблицей оборотов без предварительной группировки, система может выдать ошибку "Обнаружено дублирование ключевых значений". Это происходит потому, что в регистре накопления может быть множество записей по одной и той же номенклатуре за выбранный период.

Вариант 2. Оптимизация через вспомогательный Регистр Сведений (Денормализация)

Как справедливо отмечено в обсуждении, использование тяжелых запросов с виртуальными таблицами внутри динамического списка — это "ящик Пандоры". На небольших базах это работает, но с ростом количества данных форма списка начнет открываться медленно, а прокрутка станет прерывистой. Выясним причину: динамический список выполняет запрос к базе данных при каждой отрисовке строк на экране.

Для решения проблемы производительности рассмотрим метод денормализации. Мы создадим отдельный Регистр Сведений (например, СредниеЦеныНоменклатуры), который будет хранить уже готовое число.

Алгоритм действий будет следующим:

  1. Создаем непериодический Регистр Сведений с измерением Номенклатура и ресурсом Цена.
  2. Создаем регламентное задание, которое будет запускаться (например, раз в час или ночью) и пересчитывать среднюю цену для всех товаров, записывая результат в наш регистр.
  3. В динамическом списке справочника выполняем максимально простое ЛЕВОЕ СОЕДИНЕНИЕ со справочником.

Посмотрим, как упростится запрос динамического списка в этом случае:


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

Такой подход позволяет пользователю выполнять сортировку и поиск по колонке цены, что практически невозможно реализовать эффективно при использовании виртуальных таблиц в первом варианте.

Вариант 3. Использование события ПриПолученииДанныхНаСервере

Если нам нужна актуальность "здесь и сейчас", но мы не хотим нагружать базу данных запросами по всей номенклатуре, проанализируем ситуацию с использованием программного вывода данных. В управляемых формах 1С 8.3 существует событие таблицы ПриПолученииДанныхНаСервере.

Этот метод позволяет вычислить значения только для тех строк, которые в данный момент видны пользователю на экране (обычно это 30–50 позиций). Рассмотрим пример кода в модуле формы:


&НаСервере
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
    
    // Собираем массив ссылок номенклатуры, которые отображаются на экране
    МассивНоменклатуры = Строки.ПолучитьКлючи();
    
    // Выполняем один запрос для получения цен только по этим ссылкам
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ЗакупкиОбороты.Номенклатура КАК Номенклатура,
        |   СУММА(ЗакупкиОбороты.СуммаОборот) / СУММА(ЗакупкиОбороты.КоличествоОборот) КАК СредняяЦена
        |ИЗ
        |   РегистрНакопления.Закупки.Обороты(, , , Номенклатура В (&МассивНоменклатуры)) КАК ЗакупкиОбороты
        |СГРУППИРОВАТЬ ПО
        |   ЗакупкиОбороты.Номенклатура";
    
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    Результат = Запрос.Выполнить().Выбрать();
    
    Пока Результат.Следующий() Цикл
        // Заполняем расчетные данные в оформление строки
        ДанныеСтроки = Строки[Результат.Номенклатура];
        ДанныеСтроки.Данные["СредняяЦена"] = Результат.СредняяЦена;
    КонецЦикла;
    
КонецПроцедуры

Преимущество данного метода: форма работает молниеносно даже на огромных справочниках. Недостаток: по этой колонке нельзя будет сделать быстрый отбор или сортировку стандартными средствами платформы, так как данные не хранятся в базе, а вычисляются "на лету" только для отображения.

Важные замечания по структуре данных

Разберем типичную ошибку начинающих, упомянутую в сообщении №42. Часто разработчики пытаются создать в Регистре Накопления ресурс Цена. Это в корне неверно. Цена — это производная величина, характеристика конкретной сделки. Ресурсами регистра оборотов должны быть Количество и Сумма. Только их можно суммировать и усреднять. Если вы храните цену как ресурс, то при сложении двух покупок (по 100 рублей каждая) система покажет "цену" 200, что является математическим абсурдом.

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

Подведем итог: для небольших баз и быстрой реализации подойдет Вариант 1. Для профессиональных тиражных решений и крупных информационных систем единственно верным путем будет Вариант 2 с использованием регламентного пересчета данных в отдельный регистр сведений.

← На главную