Дерево Осипова

Дерево Осипова - это на данный момент скорее теоретический, чем практический подход к программированию сложных отчетов. Он будет полезен всем, кто пишет сложные отчеты. Использование этого инструмента позволит писать отчеты легко, независимо от их сложности.

Развитие организации идет от простого к сложному. И если сначала руководителю достаточно простых отчетов, то затем отчеты усложняются и усложняются и съедают все время разрабатывающего их программиста. Программисты многих систем порой завидуют веб-программистам, ведь вся работа по дизайну отдана веб-дизайнеру, а веб-программист пишет только код.

Можно, конечно использовать внешние средства для построения отчеты - наборы вроде Crystal Reports или инструментов попроще вроде сводных таблиц Excel. Но пользователю часто более удобно работать внутри родной программы учета, да и внешние инструменты стоят денег и вызывают проблемы согласования.

Поэтому часто программисту приходится делать отчеты встроенными средствами. Не знаю, как в других системах, а в 1С для вывода отчетов в Excel-подобную таблицу, состоящую из ячеек, у программиста есть только три инструмента:

1. Вывод подготовленной целой строки с переводом на следующую строку или ее части без перевода на следующую строку.
2. Непосредственная работа с ячейкой, указанной координатами.

В принципе, с такими инструментами, особенно с непосредственным доступом можно написать любой отчет. Однако в сложных отчетах вычисление координат, куда надо выводить данные порой занимает больше внимания и труда программиста, чем само извлечение данных. В связи с этим очен актуален вопрос альтернативной навигации по отчету, не сводящейся к простому указанию координат X и Y.

Я имею честь предложить вам свою систему навигации по отчету, которая значительно упрощает программирование отчетов и, как вы увидите далее, способна почти полностью автоматизировать эту работу.

Структура отчета

Рассмотрим для примера достаточно сложный перекрестный отчет:

Описание: R:\ВебМастер\fixin.html\articles\1s_treerep\complex_report.gif

В отчете показано, сколько товаров продано каким клиентам с каких складов в каких городах. Склады указаны по их номерам, таже дополнительно приводится адрес склада. Итоги указаны по товарам и общий итог по каждому складу.

Исходные данные для отчета - таблица вида (Товар, Клиент, Склад, АдресСклада, Продано). С получением данных проблем никаких нет. Т.е. внимание программиста сосредотачивается на выводе отчета и программирование вывода занимает достаточно много времени. Попробуем его сократить и свести к минимуму.

Красной рамкой на рисунке обведена печатная форма отчета, какой ее видит пользователь. Слева и сверху от рамки - новая система координат, разработанная мной. Система координат представляет собой два дерева - левое и верхнее. Дерево состоит из подчиненных друг другу узлов.

Узлы могут подчиняться друг другу только тремя способами (темный цвет - родитель):

Описание: R:\ВебМастер\fixin.html\articles\1s_treerep\kindsofchalign.gif

1. Все дети находятся внизу родителя.
2. Все дети находятся над родителем.
3. Все дети находятся под родителем.

Такая схема подчинения указана для левого дерева, для верхнего дерева аналогично:

1. Все дети находятся под родителем.
2. Все дети находятся слева от родителя.
3. Все дети находятся справа от родителя.

Теперь для указания ячейки мне достаточно указать левый и верхний узел. При этом будет выбрана ячейка, которая состоит из различного числа базовых клеток. Например (Товар1, Город1) = 2 клетки, (Товар1, Склад1) = 1 клетка. Если ячейка состоит из нескольких клеток, то при внесении в нее значения клетки объединяются.

Теперь не нужно долго вычислять координаты ячеек, достаточно описать структуру узлов, занести значения в ячейки по координатам узлов и вывести отчет.

А дальше мы посмотрим, как такая простая схема навигации по отчету позволит существенно упростить программирование отчетов.

Объекты доступа

Формализируем и опишем функционал объекта, который позволит нам организовать подобную схему навигации.

Объект дерево (Tree)

Состоит из объектов:
1. Левый(Left) и Верхний(Top) типа Узел(Node) - левый и верхний корневые узлы деревьев
2. Ячейки(Cells)

Объект Узел(Node)

Содержит список объектов типа Узел(Nodes).

Содержит:
1. Ссылку на своего родителя Родитель(Parent) или пустую ссылку у левой и правой ветвей.
2. Список своих детей - подчиненных узлов Дети(Childrens)
3. Способ подчинения детей - ПодчинениеДетей(ChildAlign)
4. Флажок невидимости - Спрятан(Hide) - истина указывает, что узел не выводится (по умолчанию ложь).

Объект СписокУзлов(NodeList)

Содержит список объектов типа Узел(Nodes). Используется для групповой работы с узлами.

Объект Ареал(Areal)

Представляет собой некое подмножество всех ячеек Cells.
Содержит левый/верхний узлы или левый/верхний список узлов, которыми это множество ячеек задано.
Свойства называются Левый(Left) и Верхний(Top).

Используется для итогов, форматирования дизайна ячеек, автозаполнения из таблицы и прочих целей.

Объект Ячейки(Cells)

Содержит двумерный массив всех ячеек дерева Ячейка(Cell).
Первая координата - узел левого дерева, вторая - узел верхнего дерева.
Можно перебрать все ячейки, а можно получить доступ к ним по координатам узлов.
Содержит координаты левого/верхнего узлов или левого/верхнего списоков узлов, которыми это множество ячеек задано.

Методы доступа:
1. Оператор [i, j] - доступ к ячейку по узлу левого дерева i и верхнего дерева j.

Объект Ячейка(Cell)

Содержит:
1. Значение (Value) - значение хранимое в ячейке
2. Текст (Text) - текстовое представление значения
3. Идентификатор (ID) - уникальный в пределах подчинения ключ объекта (для поиска среди детей), использовать необязательно.
4. Вычисляемые координаты Кол(X),Стр(Y) и размеры Ширина(SizeX), Высота(SizeY) ячейки (в базовых клетках).

Для увеличения скорости работы программист сначала определяет узлы, заносит в ячейки значения, производит вспомогательные вычисления и только когда все ячейки вычислены, производится финальное вычисление размеров и координат ячеек. Хотя, конечно, можно это делать и динамически, но это потребует очень изощренных алгоритмов.

Программирование отчета

Рассмотрим, как может выглядеть код для отрисовки отчета из нашего примера. В примере будет использован русский код и объекты на английском языке.

Сначала создадим само дерево.

Tree=New Tree

Создадим фиксированные узлы для левого дерева:

Tree.Left.Add(ID="ГрГород")
Tree.Left.Add(ID="ГрСклад")
Tree.Left.Add(ID="ГрАдрес")
Tree.Left.Add(ID="ОбщЛев")
Tree.Left.Add(ID="ИтогСтрок")

Создадим фиксированные узлы для правого дерева:

Tree.Left.Add(ID="Гр1")
Tree.Left.Add(ID="ГрСклад")
Tree.Left.Add(ID="ГрАдрес")
Tree.Left.Add(ID="ОбщЛев")
Tree.Left.Add(ID="ИтогСтрок")

ОбщЛев и ОбщВерх - это узлы, у которых будет заранее неизвестное количество детей.
Left и RightNode заданы по умолчанию при создании дерева.
Для исполнения дизайна нам пришлось сделать у Гр1 два подчиненных узла 0 и Гр2. Так можно организовать вывод клиента со сдвиегом.
В принципе, над списком можно предусмотреть и операции вставки, но нам достаточно только метода Add(Добавить).

Далее нарисуем шапку таблицы:

1. Нарисуем товары

For Each Unique Товар из Продажи do
    Node=Tree.Left.Get(ID="ОбщЛев").Add(Value=Товар)
    Cell= Cells[Node][Tree.Top.Get(ID="Гр1")]
    Cell.Value=Товар
Next

У отчета появилась шапка по товарам - список товаров слева.

2. Нарисуем заголовки по городам

For Each Unique Город из Продажи do
    Node=Tree.Top.Get(ID="ОбщВерх").Add(Value=Город)
    Cell=Cells[Tree.Top.Get(ID="ГрГород")][Node]
    Cell.Value=Город
Next

У отчета появилась шапка по городам - список товаров сверху.

3. Будем обрабатывать всю таблицу продаж, по ходу создавая узлы для существующих клиентов, складов и рисуя шапку.

For Each Прод из Продажи do
    ClientNode=Tree.Left.Get(ID="ОбщЛев").Get(Value=Прод.Товар).Create(Value=Прод.Клиент)
    if (ClientNode.JustCreated)
        Cell[ClientNode][Tree.Top.Get(ID="Гр1").Get(ID="Гр2")].Value=Прод.Клиент
    StoreNode=Tree.Top.Get(ID="ОбщЛев").Get(Value=Прод.Город).Create(Value=Прод.Склад)
    if (StoreNode.JustCreated)
        Cell[Tree.Top.Get(ID="ГрГород")")][StoreNode].Value=Прод.Склад
    Cell[ClientNode][StoreNode].Value=Прод.Продано
Next

Метод Create создает узел только тогда, когда не нашел узел по заданным условиям поиска. Первый вызов функции JustCreated возвращает истину, последующие - ложь. Это нужно, чтобы рисовать шапку только один раз.

У отчета появилась шапка по клиентам слева и по складам сверху. Также в ячейки на пересечении клиента и склада занесены цифры по количеству продаж.

5. Для вычисления итогов можно перебрать значения из входящих в итог ячеек и просуммировать их. Но впоследствии будут указаны более эффективные методы подытоживания, поэтому здесь они не рассматриваются.

6. Остальные ячейки (адреса складов, фиксированная шапка в левом верхнем углу, общий итог) здесь пропускаются и оставляются в качестве домашнего задания. Если пришлете код на псевдо-языке, я опубликую.

7. Запускается процедура рассчета ячеек дерева.

8. Дерево выводится по рассчитанным координатам средствами внутреннего языка системы. При этом производится установка нужного шрифта, цвета фона и прочих элементов оформления, согласно координатам ячеек.

Ускорение программирования

Облегчаем вывод

Итак, мы значительно облегчили себе работу, но это еще не все, что можно сделать. Нам все равно пришлось писать код для однообразных операций. Давайте избавимся от этого.

Попробуем заполнять ячейки по-другому.

Можно ли написать функцию Заполнить(Fill), которой мы передаем исходную таблицу данных, указываем, где она должна расставлять данные, где отрисовывать шапку? Можно, сейчас продемонстрируем как.

Попробуем формализовать то, что мы делаем:

Исходная таблица - Таблица.
#ОбщЛев=Tree.Left.Get(ID="ОбщЛев")
#ОбщЛев=Tree.Top.Get(ID="ОбщВерх")
#Гр1=Tree.Top.Get(ID="Гр1")
#Гр2=Tree.Top.Get(ID="Гр1").Get(ID="Гр2")
#ГрГород=Tree.Left.Get(ID="ГрГород")
#ГрСклад=Tree.Left.Get(ID="ГрСклад")
#ГрАдрес=Tree.Left.Get(ID="ГрАдрес")
#Товар=Назначать по Таблица.Товар подчиненные узлу #ОбщЛев
#Клиент=Назначать по Таблица.Клиент подчиненные узлу #Товар
#Город=Назначать по Таблица.Город подчиненные узлу #ОбщВерх
#Склад=Назначать по Таблица.Склад подчиненные узлу #Город
После создания узлов для только что созданных узлов рисовать шапку:
#Товар - в шапку заносим Таблица.Товар в ячейку [#Товар][#Гр1]
#Клиент - в шапку заносим Таблица.Клиент в ячейку [#Клиент][#Гр2]
#Город - в шапку заносим Таблица.Город в ячейку [#ГрГород][#Город]
#Склад - в шапку заносим Таблица.Склад в ячейку [#ГрСклад][#Склад]
#Склад - в шапку заносим Таблица.АдресСклада в ячейку [#ГрАдрес][#Склад]

Таким образом, основной способ заполнения данных в таблицу формализован. Осталось соотнести его с псевдо-языком программирования.

Для псевдо-языка нужно указать список вычислимых в процессе прохода таблицы узлов (Товар, Клиент, Город, Склад).
Далее при создании каждого узла будет вызываться метод ПриСозданииУзла(OnCreateNode).
После прохождения строки таблицы (т.е. после создания всех динамических узлов) будет вызываться метод ПослеСозданияУзлов(AfterNodesCreate). Здесь можно будет установить шапку.
Т.е. необязательно писать еще один язык запросов, можно все сделать методами.
Подробнее как это сделать будет описано позже.

Облегчаем подсчет итогов

Применим ту же схему для подсчета итогов.
Формализуем задачу для примера:
#Товары=список узлов, который состоит из всех, родитель которых #ОбщЛев
#Города=список узлов, который состоит из всех, родитель которых #ОбщВерх
#Склады =список узлов, который состоит из всех, родитель которых в списке #Города
или #Склады можно получить, сделав отбор по узлам - те узлы, тип которых в Value - Склад

1. Вычислить новый итог для каждого #Товар из #Товары и каждого #Склад из #Склады, как Сумма(Среднее, Количество и т.п.) Ареала[все, подчиненные #Товар][#Склад].
2. Вычислить новый итог для каждого #Склад из #Склады, как Сумма(Среднее, Количество и т.п.) Ареала[#Товары][#Склад].

Опять же, реализация этих формальных условий позже, главное, что рассчет итогов можно формализовать.

Резюме

Предлагается очень интересный подход к написанию сложных отчетов простыми методами.
Реализация разрабатывается на vba-подобном языке 1С. Будут приветствоваться реализации на других языках. Предыдущая, более громоздкая схема уже реализована на 1С, но она слишком медленная и использует другую, хотя и похожую модель структуры отчета.
Приветствую любое сотрудничество.

P.S.:

На момент написания статьи СКД и 1с8 еще не существовало, но метод навигации по отчету представляется интересным, особенно для нетривиальных отчетов, которых нельзя реализовать на СКД.