Хочу поделиться своим опытом работы с деревом значений. Оно во многом напоминает таблицу значений, но в отличие от неё кроме строк имеет группировки, которые можно обходить иерархически на манер дерева справочника. Но что больше всего мне понравилось, так это возможность вычислять итоги группировок с учетом иерархии на манер группировок запроса.
Этим и займёмся. Еще немного подкрасим строки дерева в зависимости от групприовки, выделим группы номенклатуры жирным, поставим запрет на изменение колонок, которые нам не интересны. Также нарисуем макет и выведем содержимое дерева на печать.
Для примера я взял демо-базу «Примеры ИТС» с диска ИТС.
В Конфигураторе создадим новый внешний отчет, назовём его ПримерИспользованияДереваЗначений.
Создадим форму отчета, нажав на лупу в поле Основная форма внешнего отчета. В конструкторе форм ничего менять не будем, нажмем Готово. Откроется новое окно с новой формой. На форму поместим табличное поле. Сделать это можно несколькими способами и каждый может использовать тот, какой ему удобно. Я буду описывать только один из них, конечный результат будет тот же.
Чтобы добавить на форму новое табличное поле, вызовем пункт Форма главного меню, в нём пункт Вставить элемент управления. Появится новый мастер, в котором сразу зададим осмысленный идентификатор этого поля как ТЗВыборка, т.к. в это поле поместим выборку результата запроса. Сменим Тип значения с таблицы значений на ДеревоЗначений.
После поместим это поле на форму отчета и перейдем в модуль формы.
Там уже есть процедура КнопкаСформироватьНажатие(Кнопка), которая сгенерирована конструктором формы и является обработчиком нажатия кнопки Сформировать. Вместо строки // Вставить содержимое обработчика впишем свой обработчик.
Результат нашего запроса будет иметь три колонки – Склад, Номенклатура и Количество, мы программно добавим еще одну колонку – «Факт. остаток», данные которой и будем просчитывать. Обратите внимание, чтобы сформировать иерархическое дерево, запрос обязательно должен иметь раздел ИТОГИ, который и определяет иерархию. Если в запросе объявить общие итоги и такой запрос выгрузить в дерево значений, то в первой строке будет пустая группировка, внутри которой будет продублирована вся остальная выборка. Пока нам это не нужно.
Процедура КнопкаСформироватьНажатие(Кнопка)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| УчетНоменклатурыОстатки.Склад КАК Склад,
| УчетНоменклатурыОстатки.Номенклатура КАК Номенклатура,
| УчетНоменклатурыОстатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.УчетНоменклатуры.Остатки КАК УчетНоменклатурыОстатки
|ИТОГИ
| СУММА(Количество)
|ПО
| Склад,
| Номенклатура ИЕРАРХИЯ";
// Выполним запрос и поместим его результат в табличное поле на форме.
ЭлементыФормы.ТЗВыборка.Значение = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
// Для определения типа добавляемой колонки создадим описание типа Число 12.2
МассивТипов = Новый Массив;
МассивТипов.Добавить(Тип("Число"));
КЧ = Новый КвалификаторыЧисла(12, 2);
ОписаниеЧисло = Новый ОписаниеТипов(МассивТипов, КЧ);
// Добавляем новую колонку описанного типа
КолонкиВыборки.Добавить("ФактОстаток", ОписаниеЧисло, "Факт. остаток");
// Для того, чтобы были видны колонки, их надо создать
ЭлементыФормы.ТЗВыборка.СоздатьКолонки();
КонецПроцедуры
Сохраним и сформируем этот отчет. Можно поиграть с формой и поменять значения колонок:
Данные менять можно, но наша цель – пересчитывать итоги по группировкам после изменения значений в колонке ФактОстаток.
Чтобы не было группировок по каждой строке номенклатуры, в тексте запроса нужно изменить тип иерархии для номенклатуры как ТОЛЬКО ИЕРАРХИЯ.
Вернемся к нашему дереву значений. В конфигураторе откроем диалог формы, вызовем свойства поля ТЗВыборка и перейдём в раздел Обработчики. Т.к. никаких проверок на вводимые данные делать не будем, нас интересует событие ПриОкончанииРедактирования. Нажмём лупу этого обработчика и попадём в новую процедуру обработчика. Впишем свой текст процедуры.
Процедура ТЗВыборкаПриОкончанииРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
// Пересчитаем итоги группировок в дереве
СчитатьСуммыДЗ(ТЗВыборка.Строки);
КонецПроцедуры
Ниже опишем процедуру СчитатьСуммыДЗ. Она в цикле перебирает таблицу и если текущая строка группировка, рассчитывается итог подчиненных строк. Группровка имеет несколько подчиненных ей строк, количество которых можно узнать методом Количество() коллекции строк. Если Строки.Количество()=0, то мы попали в обычную строку. Нас она пока не интересует.
Так как структура иерархическая, будем её обходить в рекурсии.
Процедура СчитатьСуммыДЗ(СтрокиДЗ)
Для Каждого СтрокаДерева Из СтрокиДЗ Цикл
// Если мы в строке, у неё нет итогов, пропустим
Если СтрокаДерева.Строки.Количество() = 0 Тогда
Продолжить;
КонецЕсли;
СчитатьСуммыДЗ(СтрокаДерева.Строки);
// Для текущей записи вычислим сумму строк колонки
СтрокаДерева.ФактОстаток = СтрокаДерева.Строки.Итог("ФактОстаток");
КонецЦикла;
КонецПроцедуры // СчитатьСуммыДЗ()
Сохраним и запустим отчет. Попробуем ввести данные в строке и увидим, что иерархические итоги пересчитались: сначала группировка выбранной строки «1С:Бухгалтерия ПРОФ версия 7.7», потом итоги в группе «Программное обеспечение» справочника Номенклатура и в самом корне нашего дерева, элементе справочника Склады «Склад отдела продаж».
На данном этапе поставленную задачу считаем решенной.
Можно немного усложнить эту обработку, запретив менять колонки, отличные от Факт. остаток. Для этого задействуем обработчик ПередНачаломИзменения
Процедура ТЗВыборкаПередНачаломИзменения(Элемент, Отказ)
// Разрешим ввод только в колонке ФактОстаток
Если Элемент.ТекущаяКолонка.Имя <> "ФактОстаток" Тогда
Отказ = Истина;
КонецЕсли;
КонецПроцедуры
Можно добавить и печать этого дерева:
Процедура ВывестиСекцию(Секция, Выборка)
Область = Макет.ПолучитьОбласть(пСекция);
Область.Параметры.Заполнить(пВыборка);
ЭлементыФормы.ТабДок.Вывести(Область, Выборка.Уровень()+1);
КонецПроцедуры
Процедура ПечататьДерево(СтрокиДЗ)
// Уровни группировок:
// 0 - Склад
// 1 - Товар
// 2 - Регистратор
Для Каждого СтрокаДерева Из СтрокиДЗ Цикл
Если СтрокаДерева.Строки.Количество() = 0 Тогда
Если ЗначениеЗаполнено(СтрокаДерева.Номенклатура) Тогда
ВывестиСекцию("Номенклатура", СтрокаДерева);
КонецЕсли;
Продолжить; // Обошли строки, возврат к группировкам, иначе будет каша
КонецЕсли;
ВывестиСекцию(?(СтрокаДерева.Уровень()=0, "Склад", "Номенклатура"), СтрокаДерева);
ПечататьДерево(СтрокаДерева.Строки);
КонецЦикла;
КонецПроцедуры // СчитатьСуммыДЗ()
Теперь займёмся украшательствами.
Определим обработчик таблицы ПриВыводеСтроки и его код:
Процедура ТЗВыборкаПриВыводеСтроки(Элемент, ОформлениеСтроки, ДанныеСтроки)
Попытка
ВСтрокеГруппа = ДанныеСтроки.Номенклатура.ЭтоГруппа;
Исключение
ВСтрокеГруппа = Неопределено;
КонецПопытки;
Если ВСтрокеГруппа=Неопределено Тогда
ОформлениеСтроки.ЦветФона = Новый Цвет(255, 240, 183);
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Истина, Ложь);
ИначеЕсли ВСтрокеГруппа Тогда
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Истина, Ложь);
ОформлениеСтроки.ЦветФона = Новый Цвет(255, 245, 207);
Иначе
//ОформлениеСтроки.ЦветФона = Новый Цвет(255, 245, 207);
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Ложь, Ложь);
КонецЕсли;
КонецПроцедуры
Вот всё в сборе - и покраска группировок, и выделение групп номенклатуры, и печать...
Добавлено 16 июля 2010
В комментариях поступило предложение сделать двухуровневое дерево:
- Склад
--- Группа товара
----- Товар
Проанализировав колонку Номенклатура, я пришел к выводу, что в группировке по Складу, т.е. в самом верхнем уровне, номенклатура имеет значение NULL. Как раз это пустое значение и нужно заменить на итоговую строку по складу, что можно сделать в запросе в секции ИТОГИ:
|ИТОГИ
| ВЫБОР
| КОГДА Номенклатура ЕСТЬ NULL
| ТОГДА УчетНоменклатурыОстатки.Склад
| ИНАЧЕ Номенклатура
| КОНЕЦ КАК Номенклатура,
| СУММА(КоличествоОстаток)
Вроде бы всё, но остаётся первая колонка Склад, в которой находится иерархия и от неё нужно избавиться - это легко. Достаточно в описании полей запроса закомменитровать описание склада. В совокупоности с выбором по типу значений в итогах получится требуемое двухуровневое дерево:
ВЫБРАТЬ
// УчетНоменклатурыОстатки.Склад КАК Склад,
УчетНоменклатурыОстатки.Номенклатура КАК Номенклатура,
УчетНоменклатурыОстатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
РегистрНакопления.УчетНоменклатуры.Остатки КАК УчетНоменклатурыОстатки
ИТОГИ
ВЫБОР
КОГДА Номенклатура ЕСТЬ NULL
ТОГДА УчетНоменклатурыОстатки.Склад
ИНАЧЕ Номенклатура
КОНЕЦ КАК Номенклатура,
СУММА(КоличествоОстаток)
ПО
УчетНоменклатурыОстатки.Склад,
Номенклатура ТОЛЬКО ИЕРАРХИЯ
Так как из структуры дерева исчезала колонка Склад и значение строки колонки Номенклатура может иметь тип СправочникССылка склад и СправочникСсылка.Номенклатура, необходимо откорректировать макет - в области Склад поменять идентификатор Склад на Номенклатура; визуализацию самого дерева, обработчики табличного поля дерева значений ПриВыводеСтроки и ПередНачаломИзменения:
Процедура КнопкаСформироватьНажатие(Кнопка)
...
// Визуализация дерева
// ЭлементыФормы.ТЗВыборка.Колонки.Склад.Ширина = 40;
ЭлементыФормы.ТЗВыборка.Колонки.Номенклатура.Ширина = 60;
ЭлементыФормы.ТЗВыборка.Колонки.КоличествоОстаток.Ширина = 20;
ЭлементыФормы.ТЗВыборка.Колонки.ФактОстаток.Ширина = 20;
...
КонецПроцедуры
...
Процедура ТЗВыборкаПередНачаломИзменения(Элемент, Отказ)
// Разрешим изменять только колонку ФактОстаток
Если НЕ Элемент.ТекущаяКолонка.Имя = "ФактОстаток" Тогда
Отказ = Истина;
Возврат;
КонецЕсли;
// Теперь в разрешенной колонке разрешим менять только в строках,
// т.к. группы и склады всеравно пересчитываются
СодержимоеНоменклатуры = Элемент.ТекущаяСтрока.Номенклатура;
ВСтрокеЕстьГруппа = (ТипЗнч(СодержимоеНоменклатуры) = Тип("СправочникСсылка.Склады"))
ИЛИ СодержимоеНоменклатуры.ЭтоГруппа;
Отказ = ВСтрокеЕстьГруппа;
КонецПроцедуры
...
Процедура ТЗВыборкаПриВыводеСтроки(Элемент, ОформлениеСтроки, ДанныеСтроки)
ТекущиеДанные = ДанныеСтроки.Номенклатура;
Если ТипЗнч(ТекущиеДанные) = Тип("СправочникСсылка.Склады") Тогда
ОформлениеСтроки.ЦветФона = Новый Цвет(255, 240, 183);
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Истина, Ложь);
ИначеЕсли ТекущиеДанные.ЭтоГруппа Тогда
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Истина, Ложь);
ОформлениеСтроки.ЦветФона = Новый Цвет(255, 245, 207);
Иначе
ОформлениеСтроки.Шрифт = Новый Шрифт(ОформлениеСтроки.Шрифт , , , Ложь, Ложь);
КонецЕсли;
КонецПроцедуры
Поставленная задача решена.
Всё можно в готовом виде взять в прикрепленной обработке.
Выражаю благодарность Павлу Данилковичу за объяснение рекурсивного обхода.
При написании статьи использовались примеры из книги «А. П. Габец, Д. И. Гончаров. 1С:Предприятие 8.1 Простые примеры разработки»
Мне интересно ваше мнение и пожелания по этой обработке.