При разработке внешних печатных форм в 1С часто возникает необходимость объединять данные из различных табличных частей одного документа или из разных связанных таблиц. Для этого удобно использовать язык запросов 1С с оператором ОБЪЕДИНИТЬ ВСЕ (UNION ALL). Однако, некорректное использование этого оператора может привести к ошибкам преобразования типов данных, например, к сообщению "Преобразование значения к типу Число не может быть выполнено". В этом руководстве мы подробно рассмотрим причины возникновения этой ошибки и предложим эффективные способы ее устранения, а также рассмотрим альтернативные подходы.
Предположим, перед нами стоит задача сформировать печатную форму акта выполненных работ, которая должна содержать информацию как об оказанных услугах (из табличной части Услуги документа РеализацияТоваровУслуг), так и о товарах (например, запчастях или материалах, из табличной части Товары того же документа). При попытке объединить эти данные в одном запросе и вывести их в табличную область печатной формы, возникает упомянутая ошибка.
Изначальный код запроса, который может привести к ошибке, выглядит примерно так:
Запрос = новый Запрос;
Запрос.УстановитьПараметр("Ссылка",СсылкаНаОбъект);
Запрос.Текст = "ВЫБРАТЬ
| РеализацияТоваровУслугТовары.НомерСтроки,
| РеализацияТоваровУслугТовары.Номенклатура КАК ТипПротектора,
| ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Номенклатура.НаименованиеПолное КАК Строка(100)) КАК Услуга,
| РеализацияТоваровУслугТовары.Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг.Услуги КАК РеализацияТоваровУслугТовары
|ГДЕ
| РеализацияТоваровУслугТовары.Ссылка = &Ссылка
|ОБЪЕДИНИТЬ ВСЕ
|ВЫБРАТЬ
| РеализацияТоваровУслугТовары.НомерСтроки,
| РеализацияТоваровУслугТовары.Номенклатура.Наименование КАК Марка,
| ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Номенклатура.РазмерШины КАК Строка(100)) КАК Размер,
| ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Номенклатура.ЗаводскойНомерИДатаИзготовления КАК Строка(100)) КАК ЗавНомер
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
|ГДЕ
| РеализацияТоваровУслугТовары.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
Выборка = Запрос.Выполнить.Выбрать;
ТабДок.Вывести(ОбластьШапкаТаблицы);
ИтогоСумма = 0;
Пока Выборка.Следующий Цикл
ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры,Выборка);
ИтогоСумма = ИтогоСумма + Выборка.Сумма;
ТабДок.Вывести(ОбластьДанные);
КонецЦикла;
В этом коде ошибка возникает при попытке сложить значения Выборка.Сумма. Причина в том, что в первой части запроса колонка Сумма является числом, а во второй части запроса в аналогичной позиции (четвертая колонка) находится ЗавНомер, который является строкой. Оператор ОБЪЕДИНИТЬ ВСЕ требует, чтобы количество колонок, их порядок и совместимость типов были одинаковыми для всех объединяемых частей. Типы колонок результирующей выборки определяются типами колонок в первом подзапросе. Если во втором подзапросе в аналогичной позиции будет колонка несовместимого типа, она будет приведена к типу из первого подзапроса (если это возможно) или вызовет ошибку при попытке дальнейшей обработки, как в нашем случае при сложении.
Наиболее эффективным и правильным решением является приведение колонок в каждом подзапросе оператора ОБЪЕДИНИТЬ ВСЕ к единой структуре и совместимым типам данных. Это означает, что для колонок, которые существуют только в одной из объединяемых частей, мы должны добавить "пустые" или фиктивные колонки соответствующего типа в другую часть.
ОБЪЕДИНИТЬ ВСЕ берутся из первого подзапроса. Поэтому колонки, которые должны быть выведены в печатную форму, должны иметь соответствующие наименования в первом подзапросе.Сумма (число) присутствует в первой части, но отсутствует во второй, во вторую часть нужно добавить фиктивную колонку с числовым значением (например, 0 КАК Сумма). Аналогично, если колонка ЗавНомер (строка) присутствует во второй части, но отсутствует в первой, в первую часть нужно добавить фиктивную колонку со строковым значением (например, "" КАК ЗавНомер).Рассмотрим пример исправленного запроса, который корректно объединяет данные об услугах и товарах:
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТоваровУслугУслуги.НомерСтроки КАК НомерСтроки,
| РеализацияТоваровУслугУслуги.Номенклатура КАК Номенклатура,
| РеализацияТоваровУслугУслуги.Номенклатура.НаименованиеПолное КАК ОписаниеСтроки,
| РеализацияТоваровУслугУслуги.Сумма КАК Сумма,
| "" КАК Марка,
| "" КАК Размер,
| "" КАК ЗавНомер,
| "" КАК ТипЛатки,
| ИСТИНА КАК ЭтоУслуга // Флаг для идентификации типа строки
|ИЗ
| Документ.РеализацияТоваровУслуг.Услуги КАК РеализацияТоваровУслугУслуги
|ГДЕ
| РеализацияТоваровУслугУслуги.Ссылка = &Ссылка
|ОБЪЕДИНИТЬ ВСЕ
|ВЫБРАТЬ
| РеализацияТоваровУслугТовары.НомерСтроки,
| РеализацияТоваровУслугТовары.Номенклатура,
| РеализацияТоваровУслугТовары.Номенклатура.Наименование КАК ОписаниеСтроки,
| 0 КАК Сумма, // Добавляем нулевую сумму для товаров
| РеализацияТоваровУслугТовары.Номенклатура.Наименование,
| ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Номенклатура.РазмерШины КАК Строка(100)),
| ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Номенклатура.ЗаводскойНомерИДатаИзготовления КАК Строка(100)),
| "" КАК ТипЛатки,
| ЛОЖЬ КАК ЭтоУслуга // Флаг для идентификации типа строки
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
|ГДЕ
| РеализацияТоваровУслугТовары.Ссылка = &Ссылка
|ОБЪЕДИНИТЬ ВСЕ
|ВЫБРАТЬ
| РеализацияТоваровУслугЛатки.НомерСтроки,
| РеализацияТоваровУслугЛатки.Номенклатура,
| РеализацияТоваровУслугЛатки.Номенклатура.Наименование КАК ОписаниеСтроки,
| 0 КАК Сумма, // Добавляем нулевую сумму для латок
| "" КАК Марка,
| "" КАК Размер,
| "" КАК ЗавНомер,
| РеализацияТоваровУслугЛатки.Номенклатура.Наименование,
| ЛОЖЬ КАК ЭтоУслуга // Флаг для идентификации типа строки
|ИЗ
| Документ.РеализацияТоваровУслуг.Латки КАК РеализацияТоваровУслугЛатки
|ГДЕ
| РеализацияТоваровУслугЛатки.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
//... дальнейший код обработки выборки
В этом примере мы добавили фиктивные колонки (например, "" КАК Марка, 0 КАК Сумма) в те подзапросы, где соответствующие данные отсутствуют, чтобы все три части запроса возвращали одинаковый набор колонок с совместимыми типами. Это позволяет избежать ошибки преобразования. Также мы добавили колонку ЭтоУслуга для программного определения типа строки в цикле вывода.
Для более удобной работы с результатами запроса и для вычисления итоговых сумм рекомендуется выгружать выборку в таблицу значений и использовать ее методы. Это позволяет избежать ручного накопления суммы в цикле и делает код более читаемым.
// ... (код до запроса) ...
Выборка = Запрос.Выполнить().Выгрузить(); // Выгружаем результат в ТаблицуЗначений
ТабДок.Вывести(ОбластьШапкаТаблицы);
Для Каждого ТекСтрокаДетали Из Выборка Цикл
// Используем флаг для определения, как заполнять область
Если ТекСтрокаДетали.ЭтоУслуга Тогда
ОбластьДанные.Параметры.Услуга = ТекСтрокаДетали.ОписаниеСтроки;
ОбластьДанные.Параметры.Сумма = ТекСтрокаДетали.Сумма;
ОбластьДанные.Параметры.ТипПротектора = ТекСтрокаДетали.Номенклатура.Представление; // или другое поле
ОбластьДанные.Параметры.Марка = "";
ОбластьДанные.Параметры.Размер = "";
ОбластьДанные.Параметры.ЗавНомер = "";
ОбластьДанные.Параметры.ТипЛатки = "";
ИначеЕсли ТекСтрокаДетали.Номенклатура.Тип = Тип("СправочникСсылка.Номенклатура") Тогда // Если это товар
ОбластьДанные.Параметры.Услуга = "";
ОбластьДанные.Параметры.Сумма = ""; // Товары могут не иметь прямого 'суммы' в этой колонке
ОбластьДанные.Параметры.ТипПротектора = "";
ОбластьДанные.Параметры.Марка = ТекСтрокаДетали.Марка;
ОбластьДанные.Параметры.Размер = ТекСтрокаДетали.Размер;
ОбластьДанные.Параметры.ЗавНомер = ТекСтрокаДетали.ЗавНомер;
ОбластьДанные.Параметры.ТипЛатки = "";
Иначе // Возможно, это латка
ОбластьДанные.Параметры.Услуга = "";
ОбластьДанные.Параметры.Сумма = "";
ОбластьДанные.Параметры.ТипПротектора = "";
ОбластьДанные.Параметры.Марка = "";
ОбластьДанные.Параметры.Размер = "";
ОбластьДанные.Параметры.ЗавНомер = "";
ОбластьДанные.Параметры.ТипЛатки = ТекСтрокаДетали.ТипЛатки;
КонецЕсли;
ТабДок.Вывести(ОбластьДанные);
КонецЦикла;
ОбластьПодвал.Параметры.ИтогоСумма = Выборка.Итог("Сумма"); // Используем метод Итог()
ТабДок.Вывести(ОбластьПодвал);
Обратите внимание, что в данном случае колонка ОписаниеСтроки может содержать как полное наименование услуги, так и наименование товара/латки. В макете печатной формы вам потребуется соответствующая логика для вывода информации в нужные поля.
Альтернативным, хотя и менее оптимальным с точки зрения производительности, способом является выполнение нескольких отдельных запросов для каждой табличной части и последующая их обработка. Этот подход может быть проще для понимания новичкам, так как он позволяет избежать сложностей с ОБЪЕДИНИТЬ ВСЕ, но сопряжен с дополнительными обращениями к серверу базы данных.
Пример реализации с несколькими запросами:
// ... (код до запроса) ...
ТабДок.Вывести(ОбластьШапки);
// Запрос для услуг
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Запрос.Текст = "ВЫБРАТЬ
| РеализацияТоваровУслугУслуги.НомерСтроки КАК НомерСтроки,
| РеализацияТоваровУслугУслуги.Номенклатура КАК ТипПротектора,
| РеализацияТоваровУслугУслуги.Номенклатура.НаименованиеПолное КАК Услуга,
| РеализацияТоваровУслугУслуги.Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг.Услуги КАК РеализацияТоваровУслугУслуги
|ГДЕ
| РеализацияТоваровУслугУслуги.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
ВыборкаУслуг = Запрос.Выполнить().Выбрать();
// Запрос для товаров
Запрос1 = Новый Запрос;
Запрос1.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Запрос1.Текст = "ВЫБРАТЬ
| РеализацияТоваровУслугТовары.НомерСтроки КАК НомерСтроки,
| РеализацияТоваровУслугТовары.Номенклатура КАК Марка,
| РеализацияТоваровУслугТовары.Номенклатура.РазмерШины КАК Размер,
| РеализацияТоваровУслугТовары.Номенклатура.ЗаводскойНомерИДатаИзготовления КАК ЗавНомер
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
|ГДЕ
| РеализацияТоваровУслугТовары.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
ВыборкаТоваров = Запрос1.Выполнить().Выбрать();
// Запрос для латок (если есть отдельная табличная часть)
Запрос2 = Новый Запрос;
Запрос2.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Запрос2.Текст = "ВЫБРАТЬ
| РеализацияТоваровУслугЛатки.НомерСтроки КАК НомерСтроки,
| РеализацияТоваровУслугЛатки.Номенклатура КАК ТипЛатки
|ИЗ
| Документ.РеализацияТоваровУслуг.Латки КАК РеализацияТоваровУслугЛатки
|ГДЕ
| РеализацияТоваровУслугЛатки.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
ВыборкаЛаток = Запрос2.Выполнить().Выбрать();
ТабДок.Вывести(ОбластьШапкаТаблицы);
ИтогоСумма = 0;
// Предполагаем, что вывод должен быть по строкам документа, объединяя данные по НомерСтроки
// Это более сложная логика, требующая связывания выборок или предварительной выгрузки в ТаблицыЗначений
// и их объединения в коде.
// Если просто итерировать каждую выборку отдельно, получим сначала все услуги, потом все товары, потом все латки.
// Пример, который привел пользователь, не совсем корректен для вывода в одну строку,
// т.к. он использует вложенные циклы, что приведет к дублированию строк основной табличной части:
// Пока ВыборкаУслуг.Следующий Цикл
// ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры,ВыборкаУслуг);
// ИтогоСумма = ИтогоСумма + ВыборкаУслуг.Сумма;
// Пока ВыборкаТоваров.Следующий Цикл // ЭТО ОШИБКА! Вложенные циклы не для этого!
// ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры,ВыборкаТоваров);
// КонецЦикла;
// ТабДок.Вывести(ОбластьДанные);
// КонецЦикла;
// Правильная обработка нескольких выборок потребует более сложной логики,
// например, выгрузки всех выборок в ТаблицыЗначений и последующего объединения по НомерСтроки
// или вывода каждой выборки в отдельный блок макета, если это позволяет дизайн печатной формы.
// Пример последовательного вывода:
Пока ВыборкаУслуг.Следующий Цикл
ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры, ВыборкаУслуг); // Заполняем параметры из услуг
// Очищаем или заполняем пустые значения для колонок, специфичных для товаров/латок
ОбластьДанные.Параметры.Марка = "";
ОбластьДанные.Параметры.Размер = "";
ОбластьДанные.Параметры.ЗавНомер = "";
ОбластьДанные.Параметры.ТипЛатки = "";
ИтогоСумма = ИтогоСумма + ВыборкаУслуг.Сумма;
ТабДок.Вывести(ОбластьДанные);
КонецЦикла;
Пока ВыборкаТоваров.Следующий Цикл
ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры, ВыборкаТоваров); // Заполняем параметры из товаров
// Очищаем или заполняем пустые значения для колонок, специфичных для услуг/латок
ОбластьДанные.Параметры.Услуга = "";
ОбластьДанные.Параметры.ТипПротектора = "";
ОбластьДанные.Параметры.Сумма = 0; // Для суммирования, если товары не имеют 'Сумма'
ОбластьДанные.Параметры.ТипЛатки = "";
ТабДок.Вывести(ОбластьДанные);
КонецЦикла;
Пока ВыборкаЛаток.Следующий Цикл
ЗаполнитьЗначенияСвойств(ОбластьДанные.Параметры, ВыборкаЛаток); // Заполняем параметры из латок
// Очищаем или заполняем пустые значения для колонок, специфичных для услуг/товаров
ОбластьДанные.Параметры.Услуга = "";
ОбластьДанные.Параметры.ТипПротектора = "";
ОбластьДанные.Параметры.Сумма = 0; // Для суммирования, если латки не имеют 'Сумма'
ОбластьДанные.Параметры.Марка = "";
ОбластьДанные.Параметры.Размер = "";
ОбластьДанные.Параметры.ЗавНомер = "";
ТабДок.Вывести(ОбластьДанные);
КонецЦикла;
ОбластьПодвал.Параметры.ИтогоСумма = ИтогоСумма;
ТабДок.Вывести(ОбластьПодвал);
Недостатки этого подхода:
&Ссылка), а не конкатенацией строк. Это предотвращает SQL-инъекции и улучшает производительность, позволяя СУБД кэшировать планы выполнения запросов.
ТекстЗапроса = "ВЫБРАТЬ ...";
ТекстЗапроса = ТекстЗапроса + "|ОБЪЕДИНИТЬ ВСЕ ...";
Запрос.Текст = ТекстЗапроса;
ВЫРАЗИТЬ(Поле КАК Тип) в запросе. Например, ВЫРАЗИТЬ(РеализацияТоваровУслугТовары.Сумма КАК Число(15,2)).ЗаполнитьЗначенияСвойств(): Метод ЗаполнитьЗначенияСвойств(Область.Параметры, Выборка) автоматически заполняет параметры области макета значениями из выборки, если имена параметров совпадают с именами полей выборки. Это удобно, но требует соответствия имен.[ТипПротектора], [Услуга], [Сумма]).Таким образом, для эффективной и безошибочной работы с печатными формами, требующими объединения данных, мы рекомендуем использовать единый запрос с оператором ОБЪЕДИНИТЬ ВСЕ, тщательно следя за согласованностью количества колонок, их наименований и типов данных во всех подзапросах. Применение фиктивных колонок и явное приведение типов помогут избежать распространенных ошибок и значительно упростят процесс разработки.