При разработке отчетов и печатных форм в среде «1С:Предприятие» программисты часто сталкиваются с задачей компактного представления данных. Классический пример: вывод информации об отгрузках за день. Если выводить каждый заказ в отдельной строке, результирующий документ получается слишком длинным и неудобным для анализа. Гораздо более читаемым является вариант, когда для каждого контрагента выводится одна строка с общими итогами (вес, объем, сумма), а все номера его заказов перечисляются в одной ячейке через запятую.
В этой статье мы подробно разберем, как решить эту задачу различными способами: от исправления классического процедурного кода обхода выборки до использования современных декларативных механизмов Системы компоновки данных (СКД) и оптимизации производительности BSL-кода.
Рассмотрим по шагам все возможные варианты реализации этой задачи, проанализируем потенциальные ошибки и выберем наиболее эффективные подходы.
---
Рассмотрим ситуацию, когда печать реализуется через классический алгоритм ручного вывода секций макета. Основная сложность здесь заключается в том, чтобы правильно рассчитать индексы строк табличного документа для последующего объединения ячеек по группировкам (например, по дате отгрузки), избежав при этом появления пустых строк и смещения областей.
Выясним причину ошибок в первоначальном коде автора темы. В исходном алгоритме переменная НомерСтрокиКонтрагент инициализировалась один раз в самом начале процедуры со значением 6. При обходе детальных записей в циклах это значение некорректно накапливалось, что приводило к неверному расчету координат метода Объединить() и визуальному искажению таблицы (появлению пустых областей). Чтобы исправить это, сброс счетчика строк контрагента нужно выполнять строго при начале обработки новой группировки дат.
Ниже представлен полностью рабочий, отлаженный код процедуры вывода данных, в котором исправлены ошибки расчета координат и реализован вывод номеров заказов в одну строчку через запятую:
ТабДок = ЭлементыФормы.ПолеТабличногоДокумента1;
МакетПечать = ПолучитьМакет("Печать_Макет");
ТекущийПериод = НачалоДня(ТекущаяДата);
Горизонт = КонецМесяца(ГоризонтПланирования);
_Период = НачалоМесяца(ТекущийПериод);
_КонПериод = КонецДня(Горизонт);
Шапка = МакетПечать.ПолучитьОбласть("Шапка");
Шапка.Параметры.ДатаНач = _Период;
Шапка.Параметры.ДатаКон = _КонПериод;
ТабДок.Вывести(Шапка);
ЗапросЗаказы = Новый Запрос;
ЗапросЗаказы.Текст =
"ВЫБРАТЬ
| ПС_ЗаказыДляОтгрузкиЗаказы.Вес КАК Вес,
| ПС_ЗаказыДляОтгрузкиЗаказы.Объем КАК Объем,
| ПС_ЗаказыДляОтгрузкиЗаказы.Сумма КАК Сумма,
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.Контрагент КАК Контрагент,
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ДеньОтгрузки КАК Дата,
| ПС_ЗаказыДляОтгрузкиЗаказы.Заказ.Номер КАК Заказ
|ИЗ
| Документ.ПС_ЗаказыДляОтгрузки.Заказы КАК ПС_ЗаказыДляОтгрузкиЗаказы
|ГДЕ
| НЕ ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ПометкаУдаления
|
|СГРУППИРОВАТЬ ПО
| ПС_ЗаказыДляОтгрузкиЗаказы.Вес,
| ПС_ЗаказыДляОтгрузкиЗаказы.Объем,
| ПС_ЗаказыДляОтгрузкиЗаказы.Сумма,
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.Контрагент,
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ДеньОтгрузки,
| ПС_ЗаказыДляОтгрузкиЗаказы.Заказ.Номер
|
|УПОРЯДОЧИТЬ ПО
| Дата,
| Контрагент,
| Заказ
|ИТОГИ
| Сумма(Вес),
| Сумма(Объем),
| Сумма(Сумма)
|ПО
| Дата,
| Контрагент";
Заказы = ЗапросЗаказы.Выполнить();
Выборка = Заказы.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
НомерСтрокиДата = 6; // Начальный номер строки в табличном документе для первой даты
ОбластьСтрока = МакетПечать.ПолучитьОбласть("Таблица");
Пока _Период <= _КонПериод Цикл
ЕстьЗаказыЗаДень = Ложь;
Пока Выборка.НайтиСледующий(_Период, "Дата") Цикл
ЕстьЗаказыЗаДень = Истина;
ВыборкаКонтрагенты = Выборка.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
НомерСтрокиКонтрагент = 0; // Сбрасываем счетчик выведенных строк для текущей даты
Пока ВыборкаКонтрагенты.Следующий() Цикл
// Заполняем параметры области (Контрагент, Вес, Объем, Сумма) данными группировки
ОбластьСтрока.Параметры.Заполнить(ВыборкаКонтрагенты);
ЗаказыСписок = "";
ВыборкаЗаказы = ВыборкаКонтрагенты.Выбрать();
// Собираем заказы текущего контрагента в строку через запятую
Пока ВыборкаЗаказы.Следующий() Цикл
ЗаказыСписок = ЗаказыСписок + ?(ЗаказыСписок = "", "", ", ") + ВыборкаЗаказы.Заказ;
КонецЦикла;
ОбластьСтрока.Параметры.Заказ = ЗаказыСписок;
ТабДок.Вывести(ОбластьСтрока);
НомерСтрокиКонтрагент = НомерСтрокиКонтрагент + 1;
КонецЦикла;
// Объединяем ячейки даты по вертикали для выведенных строк контрагентов
Если НомерСтрокиКонтрагент > 0 Тогда
ТабДок.Область(НомерСтрокиДата, 1, НомерСтрокиДата + НомерСтрокиКонтрагент - 1, 1).Объединить();
НомерСтрокиДата = НомерСтрокиДата + НомерСтрокиКонтрагент;
КонецЕсли;
КонецЦикла;
Если Не ЕстьЗаказыЗаДень Тогда
// Если за день заказов не было, выводим пустую строку с указанием даты
ОбластьСтрока.Параметры.Заполнить(Новый Структура("Дата, Контрагент, Заказ, Вес, Объем, Сумма", _Период, NULL, NULL, NULL, NULL, NULL));
ТабДок.Вывести(ОбластьСтрока);
НомерСтрокиДата = НомерСтрокиДата + 1;
КонецЕсли;
_Период = КонецДня(_Период) + 1;
КонецЦикла;
ТабДок.Показать();
---
Рассмотрим подробнее важный нюанс производительности. В предыдущем примере кода склеивание строк происходит в цикле с помощью оператора +:
ЗаказыСписок = ЗаказыСписок + ?(ЗаказыСписок = "", "", ", ") + ВыборкаЗаказы.Заказ;
В платформе «1С:Предприятие 8» тип данных Строка является неизменяемым. Это значит, что при каждом сложении строк в памяти выделяется новый сегмент, куда копируется старая строка и добавляется новая часть, а старый сегмент помечается для очистки сборщиком мусора. Если у контрагента десятки или сотни заказов, такой подход приводит к существенному замедлению работы алгоритма и избыточному потреблению оперативной памяти.
Начиная с версии платформы 8.3.6, разработчикам доступна высокопроизводительная встроенная функция СтрСоединить, которая объединяет массив строк в одну строку с использованием указанного разделителя на уровне платформенного кода (написанного на C++), что работает в десятки раз быстрее.
Давайте посмотрим на пример оптимизированного фрагмента кода:
// Создаем массив для накопления номеров заказов
МассивЗаказов = Новый Массив;
ВыборкаЗаказы = ВыборкаКонтрагенты.Выбрать();
Пока ВыборкаЗаказы.Следующий() Цикл
// Исключаем пустые значения номеров и приводим к строковому типу
Если ЗначениеЗаполнено(ВыборкаЗаказы.Заказ) Тогда
МассивЗаказов.Добавить(Строка(ВыборкаЗаказы.Заказ));
КонецЕсли;
КонецЦикла;
// Быстро и элегантно объединяем элементы массива через запятую и пробел
ОбластьСтрока.Параметры.Заказ = СтрСоединить(МассивЗаказов, ", ");
ТабДок.Вывести(ОбластьСтрока);
Преимущества данного подхода:
?(ЗаказыСписок = "", ... )).---
Если разработка ведется в конфигурации, где включена совместимость с версиями платформы ниже 8.3.6 (где функция СтрСоединить недоступна), хорошим тоном будет использование стандартных функций БСП. Это избавит вас от написания собственных вспомогательных функций обработки строк.
Для объединения элементов массива в строку в БСП существует общий модуль СтроковыеФункцииКлиентСервер. Реализация выглядит следующим образом:
МассивЗаказов = Новый Массив;
ВыборкаЗаказы = ВыборкаКонтрагенты.Выбрать();
Пока ВыборкаЗаказы.Следующий() Цикл
МассивЗаказов.Добавить(Строка(ВыборкаЗаказы.Заказ));
КонецЦикла;
// Используем стандартный метод БСП
ОбластьСтрока.Parameters.Заказ = СтроковыеФункцииКлиентСервер.СтрокаИзМассиваПодстрок(МассивЗаказов, ", ");
ТабДок.Вывести(ОбластьСтрока);
---
Если перед вами стоит задача разработать полноценный отчет, то писать ручной процедурный код обхода выборок вообще не требуется. Вся верстка табличного документа, расчет итогов по группировкам и объединение строк могут быть выполнены платформой автоматически в Системе компоновки данных (СКД).
Для агрегации строк в одну ячейку СКД предоставляет встроенную функцию СоединитьСтроки().
Разберем по шагам, как настроить это в схеме компоновки:
СГРУППИРОВАТЬ ПО и ИТОГИ. СКД сама выполнит группировку данных.Вес, Объем и Сумма добавьте стандартное выражение агрегации: Сумма(Вес), Сумма(Объем), Сумма(Сумма).Заказ в колонке «Выражение» укажите: СоединитьСтроки(ВычислитьВыражениеТаблица("Заказ")) или более простую конструкцию: СоединитьСтроки(Заказ, ", ").Дата (День отгрузки).Контрагент.Заказ, Вес, Объем и Сумма.При формировании отчета СКД автоматически рассчитает суммы веса и объема для каждого контрагента за выбранную дату, соберет все детальные номера заказов в массив, склеит их через запятую и выведет в одну красивую строку, самостоятельно объединив ячейки дат по вертикали.
---
Проанализируем техническую сложность процедурного метода 1. Расчет индексов строк для объединения «на лету» в циклах обхода выборки очень чувствителен к любым изменениям макета. Если в будущем в макет добавится новая строка или изменится состав группировок, разработчику придется полностью переписывать математику сложения индексов НомерСтрокиДата и НомерСтрокиКонтрагент.
Чтобы сделать код максимально устойчивым к изменениям, рекомендуется использовать методику пост-обработки готового документа. Суть метода проста: мы выводим данные в табличный документ обычным плоским списком, записывая дату в каждую строчку (без объединения). А после того как таблица полностью сформирована, специальная процедура пробегается по первой колонке снизу вверх и объединяет ячейки с одинаковыми датами.
Давайте посмотрим на пример реализации такого алгоритма:
Процедура ОбъединитьПовторяющиесяДатыВТабличномДокументе(ТабДок, НомерПервойСтрокиДанных)
ВсегоСтрок = ТабДок.ВысотаТаблицы;
Если ВсегоСтрок <= НомерПервойСтрокиДанных Тогда
Возврат;
КонецЕсли;
// Начинаем обход снизу вверх, чтобы не сбивались индексы при объединении
ТекущаяСтрока = ВсегоСтрок;
Пока ТекущаяСтрока > НомерПервойСтрокиДанных Цикл
ЗначениеНижнее = ТабДок.Область(ТекущаяСтрока, 1).Текст;
СтрокаНачалаОбъединения = ТекущаяСтрока;
// Ищем одинаковые идущие подряд значения выше по колонке
Пока СтрокаНачалаОбъединения > НомерПервойСтрокиДанных
И ТабДок.Область(СтрокаНачалаОбъединения - 1, 1).Текст = ЗначениеНижнее Цикл
СтрокаНачалаОбъединения = СтрокаНачалаОбъединения - 1;
КонецЦикла;
// Если нашли одинаковые строки, объединяем их в один блок
Если СтрокаНачалаОбъединения < ТекущаяСтрока Тогда
ТабДок.Область(СтрокаНачалаОбъединения, 1, ТекущаяСтрока, 1).Объединить();
ТекущаяСтрока = СтрокаНачалаОбъединения;
КонецЕсли;
ТекущаяСтрока = ТекущаяСтрока - 1;
КонецЦикла;
КонецПроцедуры
Этот вспомогательный метод полностью изолирует логику визуального оформления таблицы от бизнес-логики выборки данных. Процедура формирования печатной формы становится чистой, легко читаемой и избавленной от сложных формул сложения номеров строк.
---
Подводя итог нашему анализу, дадим несколько практических советов по выбору метода реализации:
СоединитьСтроки() в ресурсах компоновки данных решает задачу группировки и вывода через запятую декларативно, без написания единой строчки кода.