В работе с платформой 1С:Предприятие 7.7 разработчики часто сталкиваются с существенными ограничениями встроенного языка запросов. Одной из таких задач является необходимость вывести итоги по товарам, но с условием: если у товара заполнен реквизит Аналог, то все количественные и суммовые показатели должны быть отнесены именно к аналогу, а не к исходному товару. В современных версиях платформы (8.x) это легко решается конструкцией ВЫБОР КОГДА... (CASE WHEN), однако в "семерке" штатный конструктор запросов не позволяет гибко манипулировать полями группировки в момент выполнения.
В данной статье мы разберем, почему стандартный подход "через запятую" не работает, и рассмотрим эффективные способы решения этой задачи с использованием программной обработки результатов запроса.
Иногда программисты пытаются использовать перечисление полей в описании переменных запроса, надеясь на автоматическую подстановку значений. Проанализируем типичную ошибку при попытке составить запрос:
|ТоварДляОтчета = Справочник.Товары.ТекущийЭлемент, Справочник.Товары.Аналог;
|Группировка ТоварДляОтчета;
Важный момент: такая конструкция в 1С 7.7 не объединяет значения в одну логическую сущность для целей группировки с суммированием итогов. Запрос будет работать либо по первому полю, либо по условию "ИЛИ" для фильтрации, но он не сможет "схлопнуть" записи разного происхождения в одну строку итогов. Для решения нашей задачи нам придется прибегнуть к постобработке данных.
Это самый надежный и универсальный метод для 1С 7.7, если объем данных невелик. Суть метода заключается в том, чтобы получить из базы все необходимые данные (и Товар, и Аналог), а затем программно "перебросить" показатели с основного товара на аналог в объекте ТаблицаЗначений.
Рассмотрим алгоритм реализации по шагам:
Выгрузить().Свернуть() для окончательного подсчета итогов.Посмотрим на пример реализации этого кода:
// Описываем запрос
ТекстЗапроса = "
|Товар = Справочник.Товары.ТекущийЭлемент;
|Аналог = Справочник.Товары.Аналог;
|Кол = Справочник.Товары.Количество;
|Функция ВсегоКол = Сумма(Кол);
|Группировка Товар без групп;
|";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
ТЗ = СоздатьОбъект("ТаблицаЗначений");
// Выгружаем результат запроса в ТЗ
Запрос.Выгрузить(ТЗ, 0, 0);
// Подменяем товар на аналог там, где это необходимо
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
Если ПустоеЗначение(ТЗ.Аналог) = 0 Тогда
ТЗ.Товар = ТЗ.Аналог;
КонецЕсли;
КонецЦикла;
// Схлопываем строки по товару (теперь там и товары, и их аналоги)
ТЗ.Свернуть("Товар", "ВсегоКол");
// Теперь ТЗ содержит корректные итоги, которые можно выводить в отчет
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
// Вывод данных в таблицу отчета
КонецЦикла;
КонецЕсли;
Если нам нужно строить отчет в процессе обхода запроса, можно применить метод с использованием вспомогательной функции и вложенных условий. Этот способ позволяет избежать полного копирования данных в ТЗ, если мы работаем с очень сложными иерархиями, хотя он требует более аккуратного кода.
Разберем пример, где мы анализируем наличие аналога непосредственно при обходе выборки:
Процедура ДобавитьВТЗ(ТЗ_Итогов, ТоварДляЗаписи, ЗначениеСуммы)
СтрокаНомер = 0;
Если ТЗ_Итогов.НайтиЗначение(ТоварДляЗаписи, СтрокаНомер, "Товар") = 1 Тогда
ТЗ_Итогов.ПолучитьСтрокуПоНомеру(СтрокаНомер);
ТЗ_Итогов.Сумма = ТЗ_Итогов.Сумма + ЗначениеСуммы;
Иначе
ТЗ_Итогов.НоваяСтрока();
ТЗ_Итогов.Товар = ТоварДляЗаписи;
ТЗ_Итогов.Сумма = ЗначениеСуммы;
КонецЕсли;
КонецПроцедуры
// В теле основной программы
// ... выполнение запроса с группировкой по Товару
Пока Запрос.Группировка("Товар") = 1 Цикл
ТекТовар = Запрос.Товар;
// Если у товара есть аналог, берем его
Если ТекТовар.Аналог.Выбран() = 1 Тогда
ЦелевойОбъект = ТекТовар.Аналог;
Иначе
ЦелевойОбъект = ТекТовар;
КонецЕсли;
// Аккумулируем данные в свою таблицу или сразу выводим
ДобавитьВТЗ(ПриемникТЗ, ЦелевойОбъект, Запрос.Количество);
КонецЦикла;
Этот метод позволяет гибко управлять логикой подмены прямо "на лету", однако стоит помнить, что метод НайтиЗначение в большой таблице значений может работать медленнее, чем метод Свернуть.
При составлении условий внутри запроса (секция Когда), следует быть осторожными. В 1С 7.7 (особенно в SQL-версии) проверка ПустоеЗначение(Реквизит) = 1 внутри текста запроса иногда может отрабатывать некорректно из-за особенностей хранения пустых ссылок в базе данных SQL. Если вам необходимо фильтровать данные в самом запросе, лучше использовать сравнение с заранее полученным пустым значением справочника или проверять через Выбран() = 0 при обходе результата.
Если данных очень много и отчет работает медленно, программисты часто используют внешние компоненты, такие как 1C++. Прямой SQL-запрос позволяет выполнить подмену на уровне сервера базы данных, что в десятки раз быстрее штатных средств. В SQL это выглядело бы примерно так:
SELECT
CASE WHEN $Спр.Аналог = ' 0 ' THEN $Спр.ID ELSE $Спр.Аналог END AS [ТоварИлиАналог $Справочник.Товары],
SUM($Док.Количество) as [Кол]
FROM ...
GROUP BY CASE WHEN $Спр.Аналог = ' 0 ' THEN $Спр.ID ELSE $Спр.Аналог END
Подведем итог: для стандартных задач в 1С 7.7 оптимальным выбором будет Способ №1 (выгрузка в ТЗ и метод Свернуть). Он прост в отладке, не требует внешних компонент и гарантирует правильный расчет итогов даже в тех случаях, когда несколько разных товаров ссылаются на один и тот же аналог.