Как вывести все значения из табличной части в одну ячейку печатной формы?

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

При разработке печатных форм и отчетов в 1С часто возникает задача: вывести данные основного документа в одну строку, а все связанные значения из его табличной части (например, номера экземпляров, сертификатов или соисполнителей) объединить в строку и поместить в одну-единственную ячейку. При неправильном подходе к написанию запроса и циклов обхода разработчики сталкиваются с дублированием строк, медленной работой алгоритмов или «кашей» из данных, когда значения одного документа перетекают в другой. Рассмотрим, почему возникают эти ошибки и как реализовать задачу правильно, красиво и быстро.

Выясняем причины ошибок: декартово произведение и неверная область видимости

Проанализируем типичные ошибки, которые часто совершают начинающие разработчики. Первая ошибка кроется в конструировании текста запроса. Использование конструкции выбора из двух несвязанных таблиц (например, ВТ и ВТ1) без явного указания условий связи в секции ПО приводит к так называемому перекрестному соединению (cross join / декартово произведение). В результате система пытается сопоставить каждую строку первой таблицы с каждой строкой второй. Это порождает лавинообразный рост дублирующихся строк.

Вторая распространенная ошибка — неверная область видимости переменной-накопителя в коде обхода выборки. Если объявить строковую переменную для сбора номеров (например, Текст3 = "") перед началом внешнего цикла по документам, а во внутреннем цикле выполнять конкатенацию, то значения предыдущих документов будут постоянно накапливаться и выводиться в последующих строках печатной формы.

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

Решение 1: Запрос с левым соединением и обход по группировкам (классический метод)

Для правильного отбора данных нам необходимо использовать Левое соединение (Left Join) между таблицей документов и табличной частью их экземпляров. Чтобы система корректно сгруппировала данные, мы задействуем секцию ИТОГИ ПО. Это позволит нам построить иерархический обход результатов запроса.

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


Запрос6 = Новый Запрос;
Запрос6.Текст = 
    "ВЫБРАТЬ
    |    АрхивныйДокумент.Ссылка КАК Ссылка,
    |    АрхивныйДокумент.Дата КАК Дата,
    |    АрхивныйДокумент.ДецНомер КАК ДецНомер,
    |    АрхивныйДокумент.Наименование КАК Наименование,
    |    АрхивныйДокумент.КолвоПодл КАК КолвоПодл,
    |    АрхивныйДокумент.КолЛистов КАК КолЛистов,
    |    ЕСТЬNULL(АрхивныйДокументЭкземпляры.Номер, """") КАК НомерЭкземпляра
    |ИЗ
    |    Документ.АрхивныйДокумент КАК АрхивныйДокумент
    |    ЛЕВОЕ СОЕДИНЕНИЕ Документ.АрхивныйДокумент.Экземпляры КАК АрхивныйДокументЭкземпляры
    |        ПО АрхивныйДокументЭкземпляры.Ссылка = АрхивныйДокумент.Ссылка
    |ГДЕ
    |    АрхивныйДокумент.ДецНомер В(&ТаблицаДецНомера)
    |ИТОГИ ПО
    |    Ссылка";

Запрос6.УстановитьПараметр("ТаблицаДецНомера", ТаблицаДецНомера);
ВыборкаДокумента = Запрос6.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Ссылка");

к = 0;
Пока ВыборкаДокумента.Следующий() Цикл
    к = к + 1;
    
    // Заполняем общие параметры документа один раз (на уровне шапки)
    ОбластьЗначенияТабл.Параметры.НомерПП = к;
    ОбластьЗначенияТабл.Параметры.ДецНомер = ВыборкаДокумента.ДецНомер;
    ОбластьЗначенияТабл.Параметры.ДатаРег = Формат(ВыборкаДокумента.Дата, "ДЛФ=Д");
    ОбластьЗначенияТабл.Параметры.НаимДок = ВыборкаДокумента.Наименование;
    ОбластьЗначенияТабл.Параметры.КолЭкз = ВыборкаДокумента.КолвоПодл;
    ОбластьЗначенияТабл.Параметры.ЛистыЭкз = ВыборкаДокумента.КолЛистов;
    ОбластьЗначенияТабл.Параметры.ВсегоЛистов = ЧислоПрописью((ВыборкаДокумента.КолЛистов * ВыборкаДокумента.КолвоПодл), "Л=ru_RU; НП=Ложь", ",,,,,,,,0");
    
    // Инициализируем (очищаем) строку накопителя строго внутри внешнего цикла
    ТекстНомера = "";
    
    // Получаем детальные записи (номера экземпляров) для текущего документа
    ВыборкаЭкземпляров = ВыборкаДокумента.Выбрать();
    Пока ВыборкаЭкземпляров.Следующий() Цикл
        Если ЗначениеЗаполнено(ВыборкаЭкземпляров.НомерЭкземпляра) Тогда
            ТекстНомера = ТекстНомера + Строка(ВыборкаЭкземпляров.НомерЭкземпляра) + "; ";
        КонецЕсли;
    КонецЦикла;
    
    // Удаляем лишний символ разделителя "; " на конце строки, если она не пустая
    Если ЗначениеЗаполнено(ТекстНомера) Тогда
        ТекстНомера = Лев(ТекстНомера, СтрДлина(ТекстНомера) - 2);
    КонецЕсли;
    
    ОбластьЗначенияТабл.Параметры.НомерЭкз = ТекстНомера;
    
    // Выводим готовую и заполненную область макета
    ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;

Решение 2: Использование метода ЗаполнитьЗначенияСвойств

Для того чтобы сократить количество рутинного кода и сделать его чтение более приятным, воспользуемся системным методом ЗаполнитьЗначенияСвойств(). Чтобы этот прием сработал идеально, нам достаточно переименовать псевдонимы полей в запросе так, чтобы они полностью соответствовали именам параметров в макете табличного документа.

Например, если в макете параметр называется ДатаРег, то в запросе мы пишем: АрхивныйДокумент.Дата КАК ДатаРег. Посмотрим на пример лаконичного заполнения:


Пока ВыборкаДокумента.Следующий() Цикл
    // Заполняем все совпадающие параметры автоматически одной строкой
    ЗаполнитьЗначенияСвойств(ОбластьЗначенияТабл.Параметры, ВыборкаДокумента);
    
    // Дозаполняем расчетные или специфические поля вручную
    к = к + 1;
    ОбластьЗначенияТабл.Параметры.НомерПП = к;
    ОбластьЗначенияТабл.Параметры.ВсегоЛистов = ЧислоПрописью((ВыборкаДокумента.КолЛистов * ВыборкаДокумента.КолвоПодл), "Л=ru_RU; НП=Ложь", ",,,,,,,,0");
    
    // Сборка номеров экземпляров
    ТекстНомера = "";
    ВыборкаЭкземпляров = ВыборкаДокумента.Выбрать();
    Пока ВыборкаЭкземпляров.Следующий() Цикл
        Если ЗначениеЗаполнено(ВыборкаЭкземпляров.НомерЭкземпляра) Тогда
            ТекстНомера = ТекстНомера + Строка(ВыборкаЭкземпляров.НомерЭкземпляра) + "; ";
        КонецЕсли;
    КонецЦикла;
    
    ОбластьЗначенияТабл.Параметры.НомерЭкз = СокрЛП(ТекстНомера);
    ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;

Решение 3: Отказ от вложенного цикла с помощью СтрСоединить

Конкатенация строк через оператор "+" в циклах является неоптимальной операцией на больших объемах данных, так как в памяти каждый раз создается новый строковый объект. Начиная с версии платформы 8.3.6, в 1С доступен встроенный высокопроизводительный метод СтрСоединить(). Мы можем выгрузить подчиненные номера во временный массив или таблицу значений и объединить их одной строкой без написания вложенного цикла Пока.

Разберем пример реализации, где данные подчиненной таблицы предварительно помещаются в соответствие или обрабатываются методом поиска строк:


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

ЗапросЭкземпляры.УстановитьПараметр("ТаблицаДецНомера", ТаблицаДецНомера);
ТЗЭкземпляры = ЗапросЭкземпляры.Выполнить().Выгрузить();

// Теперь при обходе основного списка документов собираем номера с помощью СтрСоединить
Пока ВыборкаДокумента.Следующий() Цикл
    ЗаполнитьЗначенияСвойств(ОбластьЗначенияТабл.Параметры, ВыборкаДокумента);
    
    // Получаем массив номеров для конкретного документа с помощью отбора в ТЗ
    ПараметрыОтбора = Новый Структура("Ссылка", ВыборкаДокумента.Ссылка);
    НайденныеСтроки = ТЗЭкземпляры.НайтиСтроки(ПараметрыОтбора);
    
    МассивНомеров = Новый Массив;
    Для Каждого СтрокаТЗ Из НайденныеСтроки Цикл
        МассивНомеров.Добавить(Строка(СтрокаТЗ.Номер));
    КонецЦикла;
    
    // Объединяем массив в одну строку с разделителем за одно действие
    ОбластьЗначенияТабл.Параметры.НомерЭкз = СтрСоединить(МассивНомеров, "; ");
    
    ТабДок.Вывести(ОбластьЗначенияТабл);
КонецЦикла;

Решение 4: Реализация в отчетах СКД без программирования

Если ваша задача — вывести эти данные в отчет, то писать программный код обхода выборок и макетов вручную вообще не нужно. В Системе компоновки данных (СКД) есть мощная агрегатная функция СоединитьСтроки(), которая выполняет всю эту работу автоматически на уровне платформы.

Разберем по шагам, как настроить вывод значений табличной части в одну ячейку через СКД:

  1. В схеме компоновки данных добавьте набор данных - запрос, связывающий основную таблицу документов и табличную часть через ЛЕВОЕ СОЕДИНЕНИЕ.
  2. Перейдите на вкладку Ресурсы.
  3. Выберите поле Номер (номер экземпляра) и перенесите его в список ресурсов.
  4. В поле Выражение напишите функцию объединения: СоединитьСтроки(ВычислитьВыражениеТаблицаЗначений("Номер"), "; ") или более простую формулу СоединитьСтроки(Номер, "; ").
  5. Настройте группировку в отчете по полям документа (Ссылка, Дата, ДецНомер и т.д.). Поле ресурсов Номер автоматически выведется в итоговой строке группировки как одна строка с красивым разделителем.

Использование СКД не только экономит время разработчика, но и гарантирует максимальное быстродействие отчета, так как механизмы агрегации строк оптимизированы на уровне платформенного C++ кода.

← На главную