Как сформировать сложный отчет с фиксированными разделами и динамическим соединением временных таблиц?

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

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

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

Проблема: «Куча» временных таблиц и пустые значения

Рассмотрим ситуацию, описанную в задаче. У нас есть необходимость сформировать таблицу, где:

  1. Всегда присутствуют определенные разделы (например, «Списанные товары», «Проданные товары», «Раздел-1» и т.д.).
  2. Данные по этим разделам собираются в разные временные таблицы (ВТ).
  3. Если данных нет, раздел все равно должен выводиться с нулевыми показателями (План = 0, Факт = 0).

При попытке написать такой запрос вручную мы сталкиваемся с необходимостью делать множество левых соединений (LEFT JOIN) к таблице-шаблону и прописывать огромные конструкции ВЫБОР КОГДА или ЕСТЬNULL для каждого поля. Если разделов становится много, запрос становится нечитаемым и сложным в поддержке.

Этап 1: Создание «Скелета» отчета (Таблица шаблона)

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

Автор вопроса использовал подход с ОБЪЕДИНИТЬ ВСЕ. Это рабочий метод, но если разделов много, удобнее сформировать таблицу значений (ТЗ) программно или использовать справочник. Рассмотрим вариант с программным формированием ТЗ и помещением её в запрос.

Создадим таблицу, которая будет содержать структуру:


// Создаем таблицу значений для структуры отчета
ТЗ_Разделы = Новый ТаблицаЗначений;
ТЗ_Разделы.Колонки.Добавить("ИмяРаздела", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(100)));
ТЗ_Разделы.Колонки.Добавить("КлючСвязи", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(50))); // Например, код раздела
ТЗ_Разделы.Колонки.Добавить("Порядок", Новый ОписаниеТипов("Число"));

// Заполняем фиксированными данными
НовСтр = ТЗ_Разделы.Добавить();
НовСтр.ИмяРаздела = "Списанные товары";
НовСтр.КлючСвязи = "WriteOff";
НовСтр.Порядок = 1;

НовСтр = ТЗ_Разделы.Добавить();
НовСтр.ИмяРаздела = "Проданные товары";
НовСтр.КлючСвязи = "Sale";
НовСтр.Порядок = 2;
// ... и так далее

Теперь поместим эту таблицу в МенеджерВременныхТаблиц. Обратите внимание: при передаче ТЗ в запрос в качестве параметра, крайне желательно типизировать колонки при выборе во временную таблицу.


Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ТЗ.ИмяРаздела КАК Раздел,
    |   ТЗ.КлючСвязи КАК КлючСвязи,
    |   ТЗ.Порядок КАК Порядок,
    |   0 КАК План,
    |   0 КАК Факт
    |ПОМЕСТИТЬ ВТ_Разделы
    |ИЗ
    |   &ТЗ_Разделы КАК ТЗ
    |ИНДЕКСИРОВАТЬ ПО
    |   КлючСвязи";

Запрос.УстановитьПараметр("ТЗ_Разделы", ТЗ_Разделы);
Запрос.Выполнить();

Этап 2: Получение данных из сложных источников (ЗУП и функции)

Часто данные нельзя получить простым запросом к базе данных (как упоминалось в теме про расчет отпусков в ЗУП). В этом случае мы используем возможности языка 1С для получения готовых Таблиц Значений и последующей их загрузки в тот же МенеджерВременныхТаблиц.

Предположим, у нас есть функция ПолучитьДанныеПоОтпускам(), возвращающая ТЗ. Нам нужно включить эти данные в наш общий запрос.


// Получаем данные сложным алгоритмом
ТЗ_Отпуска = ЗарплатныйМодуль.ПолучитьДанныеПоОтпускам(Период);

// Загружаем их в тот же менеджер временных таблиц
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Т.Сотрудник,
    |   Т.СуммаФакта,
    |   ""Vacation"" КАК КлючСвязи // Ключ для связи с разделом
    |ПОМЕСТИТЬ ВТ_ДанныеОтпуска
    |ИЗ
    |   &ТЗ_Отпуска КАК Т
    |ИНДЕКСИРОВАТЬ ПО
    |   КлючСвязи";
    
Запрос.УстановитьПараметр("ТЗ_Отпуска", ТЗ_Отпуска);
Запрос.Выполнить(); 

Таким образом, мы накопили в менеджере множество таблиц: ВТ_Разделы (скелет), ВТ_Списания, ВТ_Продажи, ВТ_ДанныеОтпуска и т.д.

Этап 3: Динамическая генерация текста запроса

Здесь мы подходим к главному вопросу темы: как соединить всё это, не создавая «лапшу» из кода? Если у вас 10-20 разделов, писать руками ЛЕВОЕ СОЕДИНЕНИЕ для каждого утомительно. Решение — программное формирование текста запроса.

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

Рассмотрим алгоритм действий:

  1. Формируем начальный текст запроса: выбор из таблицы разделов.
  2. Запускаем цикл по списку таблиц с данными.
  3. В цикле добавляем строку с ЛЕВОЕ СОЕДИНЕНИЕ.
  4. В списке полей выборки добавляем конструкцию ЕСТЬNULL для суммирования показателей.

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

// Начало формирования текста
ТекстПолей = "ВТ_Разделы.Раздел, ВТ_Разделы.Порядок ";
ТекстСоединений = " ИЗ ВТ_Разделы КАК ВТ_Разделы ";
ТекстСуммыФакта = "";

Для Каждого Элем Из МассивТаблицДанных Цикл
    
    // Формируем блок суммирования с проверкой на NULL
    // ЕСТЬNULL(ВТ_Списания.СуммаСписания, 0) + ЕСТЬNULL(...)
    Если ЗначениеЗаполнено(ТекстСуммыФакта) Тогда
        ТекстСуммыФакта = ТекстСуммыФакта + " + ";
    КонецЕсли;
    
    ТекстСуммыФакта = ТекстСуммыФакта + " ЕСТЬNULL(" + Элем.ИмяВТ + "." + Элем.ПолеФакта + ", 0)";
    
    // Формируем блок соединений
    // ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Списания КАК ВТ_Списания ПО ВТ_Разделы.КлючСвязи = ВТ_Списания.КлючСвязи
    ТекстСоединений = ТекстСоединений + 
        " ЛЕВОЕ СОЕДИНЕНИЕ " + Элем.ИмяВТ + " КАК " + Элем.ИмяВТ + 
        " ПО ВТ_Разделы.КлючСвязи = " + Элем.ИмяВТ + ".КлючСвязи ";
        
КонецЦикла;

// Собираем итоговый запрос
ИтоговыйТекст = "ВЫБРАТЬ " + 
                ТекстПолей + ", " + 
                "(" + ТекстСуммыФакта + ") КАК Факт " + 
                ТекстСоединений + 
                " УПОРЯДОЧИТЬ ПО ВТ_Разделы.Порядок";

Запрос.Текст = ИтоговыйТекст;
Результат = Запрос.Выполнить();

Важные моменты:

Этап 4: HTTP-запросы и авторизация (Кросс-базовые отчеты)

В теме также поднимался вопрос: как выполнить запрос к другой базе (например, из БП в ЗУП) от имени текущего пользователя, если пароли совпадают.

Здесь важно понимать архитектуру безопасности 1С. Просто так передать сессию пользователя («пробросить» текущего пользователя) через HTTP-сервис нельзя, даже если логины и пароли совпадают. У вас есть два пути:

  1. Технический пользователь (Рекомендуемый). В базе-источнике (ЗУП) создается пользователь с ограниченными правами (только чтение нужных данных) и сложным паролем. В базе-приемнике (БП) эти данные прописываются в константах или защищенном хранилище. Запрос выполняется всегда под этим пользователем. Фильтрация данных «кто запустил отчет» передается как параметр JSON в теле запроса.
  2. Сквозная авторизация (OpenID/OS). Если обе базы находятся в одном домене или используют провайдер OpenID, возможна настройка, при которой авторизация происходит прозрачно. Но это требует серьезного администрирования серверной части.
  3. Basic Auth (Передача пароля). Вы можете запросить у пользователя пароль при формировании отчета и передать его в заголовке Authorization HTTP-запроса. Однако хранить пароли пользователей в открытом виде или кешировать их — грубое нарушение безопасности.

Резюме

Для создания гибких отчетов, объединяющих данные из регистров, внешних функций и других баз, используйте следующую стратегию:

  1. Сформируйте таблицу-скелет (структуру разделов) и поместите её во временную таблицу.
  2. Сложные данные получайте функциями, выгружайте в ТЗ и помещайте в отдельные временные таблицы с индексацией по ключам связи.
  3. Текст итогового запроса собирайте программно в цикле, используя строковые переменные. Это позволит легко добавлять новые разделы без переписывания всего запроса.
  4. Используйте ЕСТЬNULL для корректной математики при отсутствии данных.
← На главную