Как вывести остатки, резервы, цены и штрихкоды в списке Номенклатуры УТ 11 без зависаний программы?

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

При работе в программе 1С:Управление торговлей 11 (УТ 11) менеджеры по продажам и закупкам постоянно сталкиваются с необходимостью видеть актуальные остатки товаров, свободные резервы, розничные или оптовые цены, а также штрихкоды непосредственно в форме списка справочника Номенклатура. Стандартный функционал не всегда выводит эти данные в удобном виде, особенно если требуется кастомизация под специфику бизнеса.

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

Почему стандартное решение через "Левое соединение" в запросе динамического списка — это плохая идея?

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

  1. Падение производительности СУБД: Платформа 1С транслирует произвольный запрос динамического списка в сложные SQL-запросы. При прокрутке списка СУБД (PostgreSQL, MS SQL) будет каждый раз заново рассчитывать остатки и срезы цен для большого объема данных, полностью игнорируя постраничный вывод. Это приводит к постоянной загрузке процессора сервера на 100%.
  2. Влияние механизмов RLS (ограничение прав): Если в вашей конфигурации настроены права доступа на уровне записей (например, менеджер видит только свои склады или группы номенклатуры), то RLS-фильтры наложатся на каждую присоединенную таблицу. Скорость работы формы упадет в десятки раз.
  3. Взаимоблокировки (Deadlocks): При интенсивной выписке документов и одновременном открытии «тяжелых» списков номенклатуры в базе данных неизбежно начнут возникать конфликты блокировок.
  4. Проблема дублирования строк: Если вы попытаетесь соединить номенклатуру с регистром ШтрихкодыНоменклатуры, вы получите дублирование позиций на экране. У одного товара может быть несколько штрихкодов (для штуки, коробки, паллеты), и левое соединение выведет одну и ту же номенклатуру столько раз, сколько штрихкодов для нее зарегистрировано.

Выясним, как обойти эти ограничения и реализовать задачу красиво, быстро и правильно.

Решение проблемы отображения нулевых позиций

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

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

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

ЕСТЬNULL(ТоварыНаСкладахОстатки.ВНаличииОстаток, 0) КАК Остаток

Если после этого в списке всё равно отображается пустое поле вместо нуля, это связано с настройками условного оформления или формата поля в свойствах элемента формы. Установите в свойствах колонки формат отображения числа, указав вывод нулевого значения как "0". Также убедитесь, что в настройках формы динамического списка (кнопка "Настройка" -> вкладка "Отбор") не установлена галочка отбора по ненулевым остаткам.

Высокопроизводительный метод: Использование события "ПриПолученииДанныхНаСервере"

Рассмотрим самое современное и архитектурно верное решение, которое используется профессиональными разработчиками в крупных базах данных. Вместо того чтобы перегружать основной SQL-запрос динамического списка тяжелыми соединениями, мы будем получать остатки, резервы, цены и штрихкоды "на лету" — только для тех строк, которые пользователь видит на экране в данный момент (обычно это не более 30-40 позиций при стандартном разрешении монитора).

Разберем этот метод по шагам.

Шаг 1. Создание пустых полей-заглушек в запросе списка

Откроем форму списка справочника Номенклатура. Перейдем в свойства динамического списка Список и откроем его схему компоновки данных или произвольный запрос. Добавим в запрос пустые поля-заглушки, которые не будут нагружать СУБД, но создадут структуру для вывода наших данных:


ВЫБРАТЬ
    СправочникНоменклатура.Ссылка КАК Ссылка,
    СправочникНоменклатура.Код КАК Код,
    СправочникНоменклатура.Наименование КАК Наименование,
    // Поля-заглушки для динамического заполнения на сервере
    0 КАК Остаток,
    0 КАК СвободныйОстаток,
    0 КАК Цена,
    "" КАК Штрихкод
ИЗ
    Справочник.Номенклатура КАК СправочникНоменклатура

Перетащите созданные поля на форму в виде колонок таблицы списка.

Шаг 2. Написание обработчика события

Для элемента формы Список (наша таблица динамического списка) перейдем в палитру свойств, найдем событие ПриПолученииДанныхНаСервере и создадим процедуру-обработчик. Проанализируем программный код, который выполнит сбор необходимых данных в один быстрый микрозапрос:


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

Как этот подход решает все проблемы сразу?

Рассмотрим, почему написанный алгоритм работает безупречно и быстро:

UX-альтернатива: Информационные панели остатков по складам

Если перед вами стоит задача показать остатки товара не в общем количестве по предприятию, а детально в разрезе всех доступных складов компании (как просили в сообщении 16), то вывод множества колонок (по колонке на каждый склад) сделает форму списка перегруженной и нечитаемой. При добавлении нового склада разработчику придется постоянно переписывать код конфигурации.

В этом случае отличной альтернативой является создание вспомогательной информационной панели внизу или сбоку от основного списка номенклатуры (классический интерфейс подбора из УТ 10.3):

  1. Создадим под основной таблицей динамического списка новую небольшую таблицу формы, назовем ее ОстаткиПоСкладам. Источником данных для нее сделаем отдельный легкий динамический список или просто таблицу значений.
  2. В обработчике события основного списка ПриАктивизацииСтроки будем получать ссылку на текущий выделенный товар.
  3. Передадим эту ссылку в параметр запроса динамического списка остатков по складам:

&НаКлиенте
Процедура СписокПриАктивизацииСтроки(Элемент)
    
    ТекущаяСтрока = Элементы.Список.ТекущиеДанные;
    Если ТекущаяСтрока <> Неопределено Тогда
        ПодключитьОбработчикОжидания("ОбновитьОстаткиПоСкладам", 0.1, Истина);
    КонецЕсли;
    
КонецПроцедуры

&НаСервере
Процедура ОбновитьОстаткиПоСкладамНаСервере(Знач ВыбранныйТовар)
    
    // Устанавливаем параметр для подчиненного динамического списка остатков по складам
    СписокОстатковПоСкладам.Параметры.УстановитьЗначениеПараметра("Номенклатура", ВыбранныйТовар);
    
КонецПроцедуры

В подчиненном списке остатков по складам запрос будет предельно простым. Он выберет только те склады, на которых данный товар физически присутствует в наличии:


ВЫБРАТЬ
    ТоварыНаСкладахОстатки.Склад КАК Склад,
    ТоварыНаСкладахОстатки.ВНаличииОстаток КАК Остаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура = &Номенклатура) КАК ТоварыНаСкладахОстатки

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

Используя предложенные методы, вы сможете оптимизировать интерфейс 1С:Управление торговлей 11, сделав его максимально информативным для пользователей и безопасным для стабильности серверной инфраструктуры предприятия.

← На главную