Урок 12. Дерево в таблице
В этом уроке мы рассмотрим второй вариант работы с иерархической структурой данных в виде дерева, отображаемого в таблице DatabaseTable.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Список назначений платежей
Добавим в приложение список назначений платежей, который будет являться иерархической структурой с категориями и подкатегориями. Этот список на форме будет отображаться в виде дерева в таблице DatabaseTable. Пользователь будет иметь возможность сворачивать и разворачивать ветви дерева.
По завершению раздела у нас получатся формы:

База данных
Таблица для назначений платежей
Выполним скрипт на создание таблицы в базе данных:
Обратите внимание на строчку:
В ней мы устанавливаем ограничение уникальности значений в поле id_title, которое будем использовать для указания системных назначений платежей.
А в поле income будем отмечать, является ли назначение платежей доходом (true) или расходом (false).
Давайте сразу добавим системное назначение платежей "Оплата заказа", которое нам понадобится в этом уроке:
Функция удаления записи
Создадим функцию для удаления назначения платежей:
При удалении записи необходимо учитывать, что назначение может быть родительским для других назначений платежей, и использовать другую логику для обработки таких ситуаций. Поэтому функция возвращает тип character varying, в котором может быть одно из значений:
parent - запись является родительской;
used - запись используется в программе и перемещена в архив;
NULL - запись удалена, либо ее нет в базе данных.
Вспомогательная таблица
Давайте создадим таблицу для кассы. Пока она пригодится нам для работы со списком назначений платежей и оплатами в заказе, а позже на ее основе будет реализован модуль кассы.
Выполним скрипт:
Поле account_id будет хранить идентификатор счета. Таблицу и формы редактирования списка счетов создадим позже в этом уроке, там же создадим внешний ключ для этого поля.
Отлично! Теперь можем заняться формой для редактирования списка операций.
Форма списка
На главной форме (TemplateStart.xml) в меню добавим пункт Списки -> Назначения платежей..., по которому будет открываться новая форма (TemplateOperationList.xml).

Создадим необходимые формы для списка назначений платежей с помощью шаблона ArchiveList:

Перейдем в файл описания работы серверной части приложения (Template.xml) и скорректируем запрос OperationSelectSqlQuery:
Назначения платежей на форме будут представлены в виде дерева и разбиты на два блока, в которых записи "ДОХОД" и "РАСХОД" будут корневыми категориями.
Поле Expand будет отвечать за разворачивание/сворачивание узлов дерева.
Список назначений платежей
Перейдем в файл TemplateOperationList.xml. В описание тэга <Appearance> добавим шрифт для корневых категорий:
Также добавим пару цветов для закраски в таблице строк корневых категорий и родительских назначений платежей:
Заменим в OperationPrimaryGetDataConnection список полей:
Для создания дерева записей в таблице DatabaseTable будем использовать TreeGetDataConnection, который на основе данных из OperationPrimaryGetDataConnection будет строить дерево.
В тэге <SourceDataConnection> указываем источник данных для дерева. Первых три поля являются обязательными и их порядок строго определен. Первое поле должно соответствовать идентификатору элемента, второе - его отображаемому значению, третье - его состоянию (свернут/развернут узел дерева, если он имеет дочерние элементы).
Помимо обязательных полей можно указывать любое количество дополнительных полей.
В тэге <RelationshipDataConnection> указываем таблицу с двумя колонками: в первой колонке должен быть идентификатор элемента, а во второй - идентификатор родительского элемента. На основе этой таблицы TreeGetDataConnection построит дерево.
В тэге <AdditionalColumns> задаем имена для дополнительных колонок: одно для колонки с признаком наличия дочерних элементов (HasChildren), другое для колонки отображения значения состояния узла (State).
Укажем OperationTreeGetDataConnection в качестве источника данных для таблицы OperationDatabaseTable.
Заменим тэг <Columns> в описании таблицы кодом, в котором описаны все колонки из OperationPrimaryGetDataConnection и дополнительные колонки из OperationTreeGetDataConnection:
Добавим в тэг <Formatting> таблицы OperationDatabaseTable условия форматирования строк:
Запустим приложение и проверим загрузку формы списка и отображение дерева назначений платежей.

Отлично! Переключимся на форму редактирования назначения платежей, а затем вернемся на форму списка и доделаем дерево.
Карточка сущности
В файле TemplateOperationEdit.xml создайте необходимые объекты, чтобы у вас получилась форма вида:

Добавьте на форму параметр OperationCategoryId.
Переделайте OperationPrimaryGetDataConnection, добавив в SQL-запрос поля:
OperationCategoryId - для получения значений используйте то же выражение, которое используем в построении дерева на форме списка назначений платежей (OperationSelectSqlQuery);
IsIncome и IsSystem - они нам понадобятся для проверки в случае изменения типа (доход или расход) на противоположный у системных назначений и у назначений, которые используются в программе.
Список категорий
Перейдем в серверный xml-файл и создадим запрос для получения списка доступных категорий:
Вернемся в файл формы редактирования назначения платежей (TemplateOperationEdit.xml).
Создадим соединение с данными для списка доступных категорий:
Укажем новый OperationCategoryPrimaryGetDataConnection в тэге <ValueList> выпадающего списка поля "Категория", а в качестве значения тэга <Value> того же объекта напишем конструкцию:
Изменение категории назначения платежей
При редактировании системных назначений платежей или назначений, на которые есть кассовые операции, мы должны запрещать пользователю изменять категорию назначения, если это приведет к смене типа назначения платежей (приход/расход).
Для начала нам понадобятся данные о выбранной категории, для этого создадим SecondaryGetDataConnection:
Создадим условие проверки идентификатора выбранной категории, чтобы отсекать служебные записи "ДОХОД" (OperationId = -2) и "РАСХОД" (OperationId = -1):
Создадим условие проверки, что редактируемое назначение платежей является системным:
И условие проверки, что тип редактируемого назначения не совпадает с типом выбранной категории:
Создадим PrimaryGetDataConnection, чтобы всегда иметь свежую информацию об использовании редактируемого назначения платежей в программе:
Создайте самостоятельно команду OperationIsUsedDataConnectionRefreshCommand для обновления OperationIsUsedPrimaryGetDataConnection.
Добавим условие EqualCondition для проверки поля IsUsed:
Создадим PrimaryGetDataConnection для проверки, является ли выбранное в качестве категории назначение платежей листом, т.е. не имеет дочерних назначений:
Создайте самостоятельно команду OperationIsLeafDataConnectionRefreshCommand для обновления OperationIsLeafPrimaryGetDataConnection.
Добавим условия EqualCondition для проверки полей IsLeaf и IsUsed выбранной категории:
Создадим команду MessageBoxCommand для уведомления пользователя о невозможности изменения типа редактируемого назначения платежей:
Создадим команду MessageBoxCommand для уведомления пользователя, что кассовые операции на выбранное назначение платежа будут привязаны к редактируемому назначению:
Теперь у нас есть все необходимые условия и команды и мы можем заменить синтаксис команды SaveSequentialCommand на:
В команде в первым блоке <If> проверяем, не является ли редактируемое назначение платежей системным, и совпадает ли его тип с типом выбранной категории. Если тип не совпадает и назначение не системное, то отправляем запрос на сервер для проверки использования назначения платежа.
Во втором блоке <If> проверяем, является ли редактируемое назначение платежей системным или оно используется в программе и его тип не совпадает с типом выбранной категории. Если условие выполняется, то предупреждаем пользователя о смене типа назначения платежей при изменении его категории. Иначе выполняется большой вложенный блок проверок, в котором сначала проверяется, является ли редактируемое назначение платежей листом и выводится соответствующий вопрос пользователю. Затем идет завершающая проверка на возможность сохранения изменений.
Сохранение назначения платежей
Добавим параметры в OperationInsertSetDataConnection:
Добавим параметры в OperationUpdateSetDataConnection:
Отлично! С карточкой редактирования назначения платежей мы закончили, теперь вернемся в файл списка назначений (TemplateOperationList.xml) и продолжим работать с ним.
Добавление записи
При нажатии на кнопку добавления записи в карточку назначения платежей через параметр OperationCategoryId будем передавать идентификатор выделенной в таблице записи. Этот идентификатор будем использовать для задания родительской категории нового назначения. Но родительской категорией не может быть ни системная запись, ни архивная запись.
Создадим условие EqualCondition проверки признака системного назначения платежей:
Скорректируем синтаксис кнопки создания назначения, добавив в тэг <Enabled> условия проверки, является ли выбранная запись системной или архивной:
Скорректируем команду открытия карточки назначения на добавление записи, будем передавать идентификатор выбранной в таблице записи:
Отлично! Откройте приложение и создайте несколько назначений платежа.

Редактирование записи
Помимо запрета на редактирование архивных записей, мы должны ограничить редактирование служебных строк "ДОХОД" и "РАСХОД".
Создадим условие, где будем проверять, что идентификатор выбранной записи больше нуля. Служебные строки имеют отрицательные идентификаторы: запись "ДОХОД" имеет идентификатор -2, а запись "РАСХОД" - идентификатор -1.
Скорректируем синтаксис кнопки редактирования назначения платежей, добавив в тэг <Enabled> ограничение для служебных строк:
Перейдите в приложение и попробуйте отредактировать разные записи.
Сворачивание/разворачивание узлов дерева
Чтобы сворачивать/разворачивать узлы дерева, необходимо проверять, по какой ячейке кликнул пользователь и имеет ли выделенная запись дочерние строки.
С помощью условия CellClickCondition будем определять, был ли клик по ячейке таблицы:
Вторым условием будем проверять, что клик был по ячейке столбца State, который отвечает за сворачивание/разворачивание узлов. Для этого воспользуемся get-проперти LastCellClickedColumnName таблицы, чтобы получить имя колонки, по ячейке которой был последний клик:
Сворачивать/разворачивать мы можем только те узлы, у которых есть дочерние элементы. Здесь нам поможет get-проперти SelectedRowCellValueByColumnName таблицы, которое по имени колонки будет возвращать значение ячейки в выделенной строке:
Соберем все условия в одно условие типа NestedCondition и сделаем его принудительно событийным через тэг <AlwaysChange>:
Создайте на это условие Execution, оставив тэг <Commands> пустым.
Для сворачивания/разворачивания узла будем использовать set-проперти ToggleFoldingNode у TreeGetDataConnection, в параметр NodeId которого будем передавать идентификатор выбранной записи в таблице:
С помощью этой команды мы будем изменять данные в OperationTreeGetDataConnection. Однако это приведет к изменению источника данных в таблице, что послужит причиной для бесконечного цикла выполнения Execution на условие OperationToggleFoldingNodeNestedCondition. Поэтому мы создадим переменную, которую будем использовать для блокировки последовательности команд сворачивания/разворачивания:
И команду для изменения значения этой переменной:
Создадим команду для обновления дерева OperationTreeGetDataConnection:
Теперь можем заполнить тэг <Commands> у Execution, в котором обрабатываем попытку свернуть/развернуть узел дерева:
Запустите приложение и проверьте возможность сворачивать/разворачивать узлы дерева.

Удаление записи
Нельзя удалять служебные записи "ДОХОД" и "РАСХОД" и назначения платежей, которые находятся в архиве, содержат дочерние элементы или являются системными.
Добавим необходимые условия в тэг <Enabled> кнопки OperationDeleteButton:
Так как теперь функция template.operation_try_delete(bigint) возвращает строку, то нам нужно переделать проверку результата команды удаления.
Создадим условия проверки результата выполнения команды OperationDeleteSaveCommand:
Создадим команду MessageBoxCommand вывода сообщения, что выбранное назначение содержит дочерние элементы:
Переделаем Execution, связанный с удалением записи:
Запустите приложение и проверьте удаление назначения платежа.
Архивирование записи
Мы не можем отправлять в архив служебные записи "ДОХОД" и "РАСХОД" и системные назначения платежей.
Добавим необходимые условия в тэг <Enabled> кнопки OperationArchiveButton:
Теперь нам нужно переделать логику переноса в архив назначений платежа. Запрос OperationArchiveUpdateSqlQuery, который создался при выполнении паттерна создания формы списка, нам не подходит, так как у нас есть иерархическая структура назначений платежа.
В OperationArchiveSetDataConnection добавьте параметр Archive. Передавайте в него значение, противоположное значению из ячейки колонки Archive в выделенной строке таблицы. Для этого используйте тэг <Not>.
Переделайте текст запроса OperationArchiveUpdateSqlQuery:
Запустите приложение и проверьте работу с архивом.
Список счетов
Добавим в приложение список счетов, который будем использовать в кассовых операциях и в оплатах в заказе.
По завершению раздела у нас получатся формы:

База данных
Выполним скрипт на создание таблицы в базе данных:
В поле cash будем задавать тип счета: наличный (true) или безналичный (false).
Ранее в уроке создали таблицу template.cash, в которой указали поле account_id, Теперь можем на это поле создать внешний ключ, который будет ссылаться на новую таблицу:
Форма списка
На главной форме (TemplateStart.xml) в меню добавим пункт Списки -> Счета..., по которому будет открываться новая форма (TemplateAccountList.xml).

Создадим необходимые формы для списка счетов с помощью шаблона ArchiveList:

Самостоятельно скорректируйте SQL-запрос для получения списка счетов и связанный с ним PrimaryGetDataConnection, добавив колонку AccountType, в которую должны попадать данный из поля cash таблицы template.account.
На форме списка счетов (TemplateAccountList.xml) в таблицу добавьте колонки:
Обратите внимание, что в качестве таблицы для подстановки значения в тэге <Substitution> используем структуру <Structure Type="Table">.
Не забудьте создать функцию template.account_try_delete(smallint).
Карточка сущности
Перейдем в файл карточки счета (TemplateAccountEdit.xml) и добавим поле "Тип счета":
Для задания значений выпадающего списка так же используем структуру <Structure Type="Table">.
Запустите приложение, проверьте загрузку форм и добавьте несколько счетов, с которыми позже будем работать.

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

База данных
Выполним скрипт на создание таблицы в базе данных:
Дату, счет и сумму оплаты мы будем хранить в таблице template.cash, которую создали ранее в уроке, а в таблице template.order_payment будем хранить привязку кассовой операции к заказу через их идентификаторы.
В серверном файле (Template.xml) переделаем запрос на получение списка оплат в заказе:
Так же скорректируем запрос на удаление оплаты:
Форма заказа
Перейдем в файл карточки заказа (TemplateOrderEdit.xml). Под панелью OrderPositionPanel создадим панель OrderPaymentPanel высотой 200.
Таблица оплат
С помощью паттерна Table создадим таблицу оплат, поместив курсор в описание панели OrderPaymentPanel.

Скорректируйте таблицу OrderPaymentDatabaseTable, добавив необходимые колонки. Для колонки Счет (AccountTitle) используйте тэг <Substitution> по колонке AccountId. Для этого создайте AccountPrimaryGetDataConnection. Так же добавьте атрибут ChangeForm="False" для таблицы оплат, чтобы изменение ее источника данных не приводило к изменению формы и активации кнопки "Сохранить" - сохранение оплаты уже было на дочерней форме.
Не забудьте добавить соответствующие поля в OrderPaymentPrimaryGetDataConnection.
Итоговые значения
На панель TotalPanel добавьте поля "Сумма оплат" (TotalPaidSumTextBox) и "Задолженность" (TotalDebtTextBox).
Для TotalPaidSumTextBox создадим переменную TotalPaidSumVariable:
А для объекта TotalDebtTextBox будем использовать переменную TotalDebtVariable:
Карточка оплаты
Создайте в файле карточки оплаты (TemplateOrderPaymentEdit.xml) все необходимые элементы, чтобы у вас получилась форма вида:

При добавлении новой оплаты передавайте на форму сумму задолженности по заказу и подставляйте ее в поле "Сумма".
Переделаем запросы:
Отлично! Запустите приложение и проверьте загрузку форм. Добавьте несколько оплат в заказ.

Осталось на главной форме в списке заказов реализовать отображение информации об оплатах и задолженностях по заказам.
Список заказов
На главной форме (TemplateStart) в таблицу OrderDatabaseTable добавьте колонки: Сумма заказа (TotalOrderCost), Сумма оплат (TotalPaymentSumm) и Задолженность (TotalDebt):

Скорректируйте текст запроса OrderSelectSqlQuery.
Итоги
В этом уроке мы познакомились с TreeGetDataConnection и возможностью отображать дерево записей в таблице DatabaseTable. На следующем уроке вы самостоятельно реализуете модуль кассы и отчет по бюджету с выгрузкой его в документ Excel с помощью команды ExportToExcelCommand.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.
Last updated