Как объединить несколько этапов производства в одну ячейку табличной части в 1С?

Программист 1С v8.3 (Управляемые формы) 1С:ERP Управление предприятием Управленческий учет Промышленность, строительство и АПК
← На главную

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

Суть проблемы и исходный запрос

Представим, что у нас есть запрос к регистру накопления "ЭтапыПроизводства", который возвращает остатки по номенклатуре и заказу, включая отдельный реквизит "Этап". Если для одной и той же номенклатуры по одному заказу есть несколько различных этапов, запрос вернет несколько отдельных строк. Наша цель — представить эти данные таким образом, чтобы для каждой уникальной комбинации "ЗаказНаПроизводство" + "Номенклатура" + "Характеристика" все связанные "Этапы" были собраны в одну ячейку в итоговой табличной части.

Давайте проанализируем исходный запрос, предоставленный вами:


Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
    | ЗаказыОстатки.ЗаказНаПроизводство КАК ЗаказНаПроизводство,
    | ЗаказыОстатки.Номенклатура КАК Номенклатура,
    | ЗаказыОстатки.Характеристика КАК Характеристика,
    | СУММА(ЗаказыОстатки.КоличествоОстаток) КАК Количество,
    | ЗаказыОстатки.Этап КАК Этап,
    | ЗаказыОстатки.Номенклатура.ТипНоменклатуры КАК ТипНоменклатуры,
    | ЗаказыОстатки.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
    | ЗаказыОстатки.Спецификация КАК Спецификация,
    | ЗаказыОстатки.ПодразделениеЗавершающегоЭтапа КАК ПодразделениеЗавершающегоЭтапа,
    | ЗаказыОстатки.ЕдиницаИзмерения КАК ЕдиницаИзмерения1
    |ИЗ
    | (ВЫБРАТЬ
    | ЭтапыПроизводства.Заказ КАК ЗаказНаПроизводство,
    | ЭтапыПроизводства.Номенклатура КАК Номенклатура,
    | ЭтапыПроизводства.Характеристика КАК Характеристика,
    | СУММА(ЭтапыПроизводства.КоличествоПлан - ЭтапыПроизводства.КоличествоФакт) КАК КоличествоОстаток,
    | ЭтапыПроизводства.Этап КАК Этап,
    | ЭтапыПроизводства.Номенклатура.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
    | ЭтапыПроизводства.Спецификация КАК Спецификация,
    | ЭтапыПроизводства.СтруктурнаяЕдиница КАК ПодразделениеЗавершающегоЭтапа
    | ИЗ
    | РегистрНакопления.ЭтапыПроизводства КАК ЭтапыПроизводства
    | ГДЕ
    | ЭтапыПроизводства.Заказ = &Ссылка
    |
    | СГРУППИРОВАТЬ ПО
    | ЭтапыПроизводства.Номенклатура,
    | ЭтапыПроизводства.Заказ,
    | ЭтапыПроизводства.Характеристика,
    | ЭтапыПроизводства.Этап,
    | ЭтапыПроизводства.Спецификация,
    | ЭтапыПроизводства.СтруктурнаяЕдиница,
    | ЭтапыПроизводства.Номенклатура.ЕдиницаИзмерения) КАК ЗаказыОстатки
    |
    |СГРУППИРОВАТЬ ПО
    | ЗаказыОстатки.ЗаказНаПроизводство,
    | ЗаказыОстатки.Номенклатура,
    | ЗаказыОстатки.Характеристика,
    | ЗаказыОстатки.Этап,
    | ЗаказыОстатки.Номенклатура.ТипНоменклатуры,
    | ЗаказыОстатки.ЕдиницаИзмерения,
    | ЗаказыОстатки.Спецификация,
    | ЗаказыОстатки.ПодразделениеЗавершающегоЭтапа,
    | ЗаказыОстатки.ЕдиницаИзмерения
    |
    |ИМЕЮЩИЕ
    | СУММА(ЗаказыОстатки.КоличествоОстаток) > 0";
Запрос.Текст = Запрос.Текст;
Запрос.УстановитьПараметр("Ссылка", ДанныеЗаполнения);
МассивРезультатов = Запрос.Выполнить().Выгрузить();

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

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

В отличие от некоторых систем управления базами данных (СУБД), которые предоставляют специальные функции, такие как `STRING_AGG` или `GROUP_CONCAT`, для конкатенации строковых значений внутри группировки, язык запросов 1С не имеет прямой и универсальной агрегатной функции для объединения строк. Функции вроде `МАКСИМУМ()` или `МНИМУМ()` для строковых полей просто выберут одно строковое значение, но не объединят все. Поэтому для решения нашей задачи мы будем использовать программный подход после получения данных из запроса.

Решение: Программное объединение данных после выполнения запроса

Наиболее гибким и распространенным способом решения этой задачи в 1С является программная обработка результата запроса. Мы будем итерировать по всем строкам, полученным из запроса, и динамически формировать строки нашей табличной части, объединяя значения поля "Этап".

1. Подготовка табличной части и реквизита "Этап"

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

  1. Тип "Строка": Если вы хотите, чтобы все этапы были представлены как одна текстовая строка (например, "Этап 1, Этап 2, Этап 3"), то реквизит должен быть типа Строка. Это наиболее простой вариант для отображения.
  2. Тип "СписокЗначений": Если вам нужно не только отобразить этапы, но и иметь возможность программно работать с каждым этапом отдельно из ячейки (например, открывать карточку этапа по двойному клику), то реквизит должен быть типа СписокЗначений. В этом случае каждый этап будет элементом списка.

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

2. Пошаговый алгоритм программного объединения

После того как запрос выполнен и его результат выгружен (например, в МассивРезультатов или ТаблицуЗначений), мы переходим к программной обработке:

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

  7. Добавление этапа или создание новой строки:
    • Если соответствующая строка в целевой табличной части найдена: Мы берем текущий "Этап" из выборки запроса и добавляем его к реквизиту "Этап" найденной строки.
    • Если строка не найдена: Мы создаем новую строку в целевой табличной части и заполняем ее всеми полями из выборки запроса, включая первый "Этап".

3. Пример кода для программного объединения этапов

Рассмотрим конкретный пример кода, который реализует описанный алгоритм. Предположим, что целевая табличная часть называется Продукция и в ней есть реквизит ЭтапыПроизводстваСтрока типа Строка, куда мы хотим объединить этапы.


Продукция.Очистить(); // Очищаем целевую табличную часть перед заполнением

// Вспомогательная структура для быстрого поиска уже добавленных строк.
// Ключ: Строка с уникальной комбинацией полей (Заказ, Номенклатура, Характеристика, Спецификация и т.д.)
// Значение: Ссылка на строку в табличной части Продукция
СоответствиеКлючейИСтрок = Новый Соответствие();

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

    // Проверяем, есть ли уже строка с таким ключом в нашей табличной части
    СуществующаяСтрока = СоответствиеКлючейИСтрок.Получить(КлючСтроки);

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

        // Заполняем поле с этапами первым этапом
        НоваяСтрока.ЭтапыПроизводстваСтрока = Строка(ВыборкаПродукция.Этап); // Предполагаем, что реквизит называется ЭтапыПроизводстваСтрока

        // Добавляем новую строку в соответствие для быстрого поиска в будущем
        СоответствиеКлючейИСтрок.Вставить(КлючСтроки, НоваяСтрока);
    Иначе
        // Если строка уже есть, добавляем текущий этап к существующей строке
        // Используем Символы.ПС для переноса строки, чтобы этапы были каждый на новой строке
        СуществующаяСтрока.ЭтапыПроизводстваСтрока = СуществующаяСтрока.ЭтапыПроизводстваСтрока + Символы.ПС + Строка(ВыборкаПродукция.Этап);
    КонецЕсли;

    // Если у вас есть связанная табличная часть, как в вашем коде (ВыполненныеЭтапы),
    // и ее логика предполагает отдельное хранение этапов для каждой строки,
    // то этот код должен быть отдельным и не касаться агрегации:
    // НС = ВыполненныеЭтапы.Добавить();
    // НС.КлючСвязи = НоваяСтрока.КлючСвязи; // Если НоваяСтрока.КлючСвязи - это уникальный идентификатор строки Продукции
    // НС.Этап = ВыборкаПродукция.Этап;
КонецЦикла;

// Дополнительные действия, которые были в вашем коде
ЗаполнитьСкладыИРезервы();
Показать();

Пояснения к коду:

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

4. Вариант с типом "СписокЗначений"

Если вы выбрали для реквизита "Этап" тип СписокЗначений, код будет немного отличаться в части обработки поля "Этап":


// ... (начало кода такое же)

    Если СуществующаяСтрока = Неопределено Тогда
        НоваяСтрока = ЭтотОбъект.Продукция.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаПродукция);

        // Создаем новый СписокЗначений и добавляем в него первый этап
        НоваяСтрока.ЭтапыПроизводстваСписок = Новый СписокЗначений(); // Предполагаем, что реквизит называется ЭтапыПроизводстваСписок
        НоваяСтрока.ЭтапыПроизводстваСписок.Добавить(ВыборкаПродукция.Этап, ВыборкаПродукция.Этап.Наименование); // Используем сам Этап как значение и его Наименование как представление

        СоответствиеКлючейИСтрок.Вставить(КлючСтроки, НоваяСтрока);
    Иначе
        // Добавляем текущий этап в существующий СписокЗначений
        СуществующаяСтрока.ЭтапыПроизводстваСписок.Добавить(ВыборкаПродукция.Этап, ВыборкаПродукция.Этап.Наименование);
    КонецЕсли;

// ... (конец кода такой же)

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

Особенности отображения многострочного текста в управляемых формах

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

  1. В свойствах элемента формы типа "Поле формы" (для вашей табличной части) для колонки "ЭтапыПроизводстваСтрока" установите свойство "Многострочный режим" в значение Истина.
  2. Возможно, потребуется увеличить "Высоту строки" табличного поля или установить "Автоматическая высота строки", чтобы все строки этапов были видны.

Альтернатива: Использование метода ТаблицыЗначений.Свернуть()

Вы упомянули метод МассивРезультатов.Свернуть(). Этот метод применим к объекту ТаблицаЗначений. Его можно использовать для группировки данных и агрегации, но он имеет свои ограничения для строковых полей:

Метод Свернуть() позволяет указать поля группировки и поля для суммирования. Для числовых полей он выполнит стандартную агрегацию (сумма, количество, максимум, минимум). Однако для строковых полей он не предоставляет прямого механизма конкатенации. Если вы укажете строковое поле в качестве поля для "суммирования", Свернуть() просто выберет одно из значений (обычно первое или последнее в рамках группы, в зависимости от внутренней реализации), а не объединит их.

Поэтому, если вы решите использовать Свернуть(), вам придется применить двухшаговый подход:

  1. Сначала выполнить Свернуть() по всем полям, кроме "Этап", чтобы получить уникальные строки.
  2. Затем пройтись по *оригинальному* результату запроса (или отдельной таблице значений, которая его содержит) и для каждой строки из свернутой таблицы найти все соответствующие этапы в исходных данных и программно объединить их, как мы делали в основном решении.

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

Важные аспекты и рекомендации

Заключение

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

← На главную