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