При разработке отчетов в платформе 1С:Предприятие 7.7 нередко возникает задача сгруппировать данные по папке верхнего уровня — например, получить бренд или категорию товара, если справочник номенклатуры имеет трехуровневую или четырехуровневую иерархию. Разработчики часто пытаются написать конструкцию следующего вида:
|Бренд = Регистр.ОстаткиТМЦ.Номенклатура.Родитель.Родитель;
|Группировка Бренд без групп;
Однако на практике встроенный транслятор запросов 1С 7.7 («чёрный запрос») возвращает в поле Бренд значение непосредственного родителя первого уровня вложенности (Номенклатура.Родитель), полностью игнорируя вторую точку. Разберем причины такого поведения платформы и рассмотрим рабочие способы получения нужного результата.
Проанализируем работу встроенного механизма запросов 1С 7.7. Платформа без проблем обрабатывает цепочки через несколько точек, если они обращаются к разным объектам метаданных (например: Регистр.ОстаткиТМЦ.Номенклатура.БазоваяЕдиница.ОКЕИ). В этом случае транслятор последовательно соединяет физические таблицы между собой.
Ограничение возникает именно при попытке самосоединения (self-join). Реквизит Родитель (в базе данных — поле PARENTID) ссылается на ту же самую таблицу справочника. Встроенный процессор запросов платформы не умеет создавать второй псевдоним (алиас) для таблицы, чтобы связать её саму с собой повторно. В итоге платформа отсекает второе обращение и возвращает значение поля PARENTID первого уровня.
Для решения этой проблемы рассмотрим несколько проверенных подходов.
В объектной модели 1С 7.7 обращение по цепочке Элемент.Родитель.Родитель работает штатно и без ограничений. Поэтому самый распространенный и простой вариант — получить в запросе детальные данные по номенклатуре или ее непосредственному родителю, выгрузить результат в объект ТаблицаЗначений, вычислить нужный уровень в цикле и свернуть таблицу.
Разберем по шагам реализацию этого метода:
ТаблицаЗначений с помощью метода Выгрузить().Бренд.Свернуть().Посмотрим на пример кода:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Период с ВыбКонПериода по ВыбКонПериода;
|Номенклатура = Регистр.ОстаткиТМЦ.Номенклатура;
|Количество = Регистр.ОстаткиТМЦ.Количество;
|Функция КонОст = КонОст(Количество);
|Группировка Номенклатура без групп;
|"//}}ЗАПРОС
;
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
ТЗ = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ);
ТЗ.НоваяКолонка("Бренд", "Справочник.Номенклатура");
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
Родитель1 = ТЗ.Номенклатура.Родитель;
Если Родитель1.Выбран() = 1 Тогда
Родитель2 = Родитель1.Родитель;
Если Родитель2.Выбран() = 1 Тогда
ТЗ.Бренд = Родитель2;
Иначе
ТЗ.Бренд = Родитель1;
КонецЕсли;
КонецЕсли;
КонецЦикла;
ТЗ.Свернуть("Бренд", "КонОст");
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
// Вывод данных в печатную форму
КонецЦикла;
КонецЕсли;
Если нет желания выгружать данные в промежуточные структуры, используем встроенные возможности иерархических итогов запроса. Для этого уберем директиву без групп и выполним обход с проверкой уровня вложенности.
Разберем логику:
|Группировка Номенклатура;. Платформа автоматически строит иерархическое дерево папок.Запрос.Группировка("Номенклатура") проверяем, является ли текущая запись группой с помощью функции Запрос.ЭтоГруппа().Запрос.Номенклатура.Уровень() определяем позицию в иерархии и перехватываем итоги только на нужном уровне (например, втором), пропуская детализацию ниже.Для конфигураций, работающих под управлением Microsoft SQL Server с установленными внешними компонентами прямого доступа (например, 1С++), ограничение встроенного транслятора обходится классическим соединением таблиц базы данных. В SQL-запросе мы можем связать таблицу справочника номенклатуры саму с собой необходимое количество раз через псевдонимы.
Посмотрим на пример запроса:
SELECT
Дедушка.ID [Бренд $Справочник.Номенклатура],
SUM(Остатки.КоличествоОстаток) AS [Количество]
FROM
$РегистрОстатки.ОстаткиТМЦ(:ВыбДата~,,
,
(Номенклатура),
(Количество)) AS Остатки
INNER JOIN $Справочник.Номенклатура AS Номенклатура ON Остатки.Номенклатура = Номенклатура.ID
LEFT JOIN $Справочник.Номенклатура AS Родитель ON Номенклатура.PARENTID = Родитель.ID
LEFT JOIN $Справочник.Номенклатура AS Дедушка ON Родитель.PARENTID = Дедушка.ID
GROUP BY
Дедушка.ID
Обратите внимание: соединение родительских таблиц обязательно выполняем через LEFT JOIN, а не через INNER JOIN. Если глубина структуры каталога неоднородна и часть позиций лежит на первом или втором уровне, INNER JOIN отсечет такие позиции и исказит итоговые остатки.
Если отчетность в разрезе брендов или категорий строится регулярно на больших объемах данных, постоянный рекурсивный поиск родителей замедляет систему. В таком случае оптимальным архитектурным решением будет создание явного реквизита Бренд в справочнике Номенклатура.
Чтобы исключить ошибки пользователей при вводе данных, заполнение реквизита автоматизируют: в модуле справочника в предопределенной процедуре ПриЗаписи() программно определяют родителя нужного уровня и сохраняют его в реквизит элемента.