В практике разработки на платформе 1С часто возникает задача построения отчетов со сложной, заранее определенной структурой. Например, необходимо вывести список всех статей бюджета или разделов учета, даже если по ним не было движений (сумм) в выбранном периоде. Ситуация усложняется, когда данные для разных разделов собираются из разнородных источников: регистров бухгалтерии, сложных расчетов зарплаты (ЗУП) или внешних систем.
В этой статье мы подробно разберем, как решить проблему громоздкого кода при соединении множества временных таблиц, как корректно использовать механизм «скелета» отчета и как программно формировать текст запроса, чтобы избежать ручного написания сотен строк кода.
Рассмотрим ситуацию, описанную в задаче. У нас есть необходимость сформировать таблицу, где:
При попытке написать такой запрос вручную мы сталкиваемся с необходимостью делать множество левых соединений (LEFT JOIN) к таблице-шаблону и прописывать огромные конструкции ВЫБОР КОГДА или ЕСТЬNULL для каждого поля. Если разделов становится много, запрос становится нечитаемым и сложным в поддержке.
Чтобы в отчете всегда отображались все необходимые строки, первым делом мы должны создать таблицу-основу. Это делается через создание временной таблицы, содержащей все возможные варианты разделов.
Автор вопроса использовал подход с ОБЪЕДИНИТЬ ВСЕ. Это рабочий метод, но если разделов много, удобнее сформировать таблицу значений (ТЗ) программно или использовать справочник. Рассмотрим вариант с программным формированием ТЗ и помещением её в запрос.
Создадим таблицу, которая будет содержать структуру:
// Создаем таблицу значений для структуры отчета
ТЗ_Разделы = Новый ТаблицаЗначений;
ТЗ_Разделы.Колонки.Добавить("ИмяРаздела", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(100)));
ТЗ_Разделы.Колонки.Добавить("КлючСвязи", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(50))); // Например, код раздела
ТЗ_Разделы.Колонки.Добавить("Порядок", Новый ОписаниеТипов("Число"));
// Заполняем фиксированными данными
НовСтр = ТЗ_Разделы.Добавить();
НовСтр.ИмяРаздела = "Списанные товары";
НовСтр.КлючСвязи = "WriteOff";
НовСтр.Порядок = 1;
НовСтр = ТЗ_Разделы.Добавить();
НовСтр.ИмяРаздела = "Проданные товары";
НовСтр.КлючСвязи = "Sale";
НовСтр.Порядок = 2;
// ... и так далее
Теперь поместим эту таблицу в МенеджерВременныхТаблиц. Обратите внимание: при передаче ТЗ в запрос в качестве параметра, крайне желательно типизировать колонки при выборе во временную таблицу.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.ИмяРаздела КАК Раздел,
| ТЗ.КлючСвязи КАК КлючСвязи,
| ТЗ.Порядок КАК Порядок,
| 0 КАК План,
| 0 КАК Факт
|ПОМЕСТИТЬ ВТ_Разделы
|ИЗ
| &ТЗ_Разделы КАК ТЗ
|ИНДЕКСИРОВАТЬ ПО
| КлючСвязи";
Запрос.УстановитьПараметр("ТЗ_Разделы", ТЗ_Разделы);
Запрос.Выполнить();
Часто данные нельзя получить простым запросом к базе данных (как упоминалось в теме про расчет отпусков в ЗУП). В этом случае мы используем возможности языка 1С для получения готовых Таблиц Значений и последующей их загрузки в тот же МенеджерВременныхТаблиц.
Предположим, у нас есть функция ПолучитьДанныеПоОтпускам(), возвращающая ТЗ. Нам нужно включить эти данные в наш общий запрос.
// Получаем данные сложным алгоритмом
ТЗ_Отпуска = ЗарплатныйМодуль.ПолучитьДанныеПоОтпускам(Период);
// Загружаем их в тот же менеджер временных таблиц
Запрос.Текст =
"ВЫБРАТЬ
| Т.Сотрудник,
| Т.СуммаФакта,
| ""Vacation"" КАК КлючСвязи // Ключ для связи с разделом
|ПОМЕСТИТЬ ВТ_ДанныеОтпуска
|ИЗ
| &ТЗ_Отпуска КАК Т
|ИНДЕКСИРОВАТЬ ПО
| КлючСвязи";
Запрос.УстановитьПараметр("ТЗ_Отпуска", ТЗ_Отпуска);
Запрос.Выполнить();
Таким образом, мы накопили в менеджере множество таблиц: ВТ_Разделы (скелет), ВТ_Списания, ВТ_Продажи, ВТ_ДанныеОтпуска и т.д.
Здесь мы подходим к главному вопросу темы: как соединить всё это, не создавая «лапшу» из кода? Если у вас 10-20 разделов, писать руками ЛЕВОЕ СОЕДИНЕНИЕ для каждого утомительно. Решение — программное формирование текста запроса.
Мы можем сформировать список (массив или таблицу) с именами временных таблиц данных и их ключами связи, а затем в цикле собрать итоговый текст запроса.
Рассмотрим алгоритм действий:
ЛЕВОЕ СОЕДИНЕНИЕ.ЕСТЬNULL для суммирования показателей.
// Опишем структуру наших данных для сборки
МассивТаблицДанных = Новый Массив;
// Структура: Имя таблицы, Ключ связи, Имя поля факта в таблице
МассивТаблицДанных.Добавить(Новый Структура("ИмяВТ, ПолеФакта", "ВТ_Списания", "СуммаСписания"));
МассивТаблицДанных.Добавить(Новый Структура("ИмяВТ, ПолеФакта", "ВТ_Продажи", "СуммаПродажи"));
МассивТаблицДанных.Добавить(Новый Структура("ИмяВТ, ПолеФакта", "ВТ_ДанныеОтпуска", "СуммаФакта"));
// Начало формирования текста
ТекстПолей = "ВТ_Разделы.Раздел, ВТ_Разделы.Порядок ";
ТекстСоединений = " ИЗ ВТ_Разделы КАК ВТ_Разделы ";
ТекстСуммыФакта = "";
Для Каждого Элем Из МассивТаблицДанных Цикл
// Формируем блок суммирования с проверкой на NULL
// ЕСТЬNULL(ВТ_Списания.СуммаСписания, 0) + ЕСТЬNULL(...)
Если ЗначениеЗаполнено(ТекстСуммыФакта) Тогда
ТекстСуммыФакта = ТекстСуммыФакта + " + ";
КонецЕсли;
ТекстСуммыФакта = ТекстСуммыФакта + " ЕСТЬNULL(" + Элем.ИмяВТ + "." + Элем.ПолеФакта + ", 0)";
// Формируем блок соединений
// ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Списания КАК ВТ_Списания ПО ВТ_Разделы.КлючСвязи = ВТ_Списания.КлючСвязи
ТекстСоединений = ТекстСоединений +
" ЛЕВОЕ СОЕДИНЕНИЕ " + Элем.ИмяВТ + " КАК " + Элем.ИмяВТ +
" ПО ВТ_Разделы.КлючСвязи = " + Элем.ИмяВТ + ".КлючСвязи ";
КонецЦикла;
// Собираем итоговый запрос
ИтоговыйТекст = "ВЫБРАТЬ " +
ТекстПолей + ", " +
"(" + ТекстСуммыФакта + ") КАК Факт " +
ТекстСоединений +
" УПОРЯДОЧИТЬ ПО ВТ_Разделы.Порядок";
Запрос.Текст = ИтоговыйТекст;
Результат = Запрос.Выполнить();
Важные моменты:
ЕСТЬNULL (ISNULL) обязательно при левом соединении, так как если данных в присоединенной таблице нет, запрос вернет NULL, и любая арифметическая операция с NULL даст в итоге NULL. Нам же нужны нули.КлючСвязи на этапе создания ВТ).В теме также поднимался вопрос: как выполнить запрос к другой базе (например, из БП в ЗУП) от имени текущего пользователя, если пароли совпадают.
Здесь важно понимать архитектуру безопасности 1С. Просто так передать сессию пользователя («пробросить» текущего пользователя) через HTTP-сервис нельзя, даже если логины и пароли совпадают. У вас есть два пути:
Authorization HTTP-запроса. Однако хранить пароли пользователей в открытом виде или кешировать их — грубое нарушение безопасности.Для создания гибких отчетов, объединяющих данные из регистров, внешних функций и других баз, используйте следующую стратегию:
ЕСТЬNULL для корректной математики при отсутствии данных.