В этом уроке мы рассмотрим второй вариант работы с иерархической структурой данных в виде дерева, отображаемого в таблице DatabaseTable.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Список назначений платежей
Добавим в приложение список назначений платежей, который будет являться иерархической структурой с категориями и подкатегориями. Этот список на форме будет отображаться в виде дерева в таблице DatabaseTable. Пользователь будет иметь возможность сворачивать и разворачивать ветви дерева.
По завершению раздела у нас получатся формы:
База данных
Таблица для назначений платежей
Выполним скрипт на создание таблицы в базе данных:
CREATESEQUENCEtemplate.operation_id_seq;CREATETABLEtemplate.operation( operation_id integerNOT NULLDEFAULT nextval('template.operation_id_seq'::regclass), operation_category_id bigint, id_title character varying, title character varying NOT NULL, income booleanNOT NULL, archive booleanNOT NULLDEFAULT false,CONSTRAINT pk_operation_id PRIMARY KEY (operation_id),CONSTRAINT fk_operation_category_id FOREIGN KEY (operation_category_id)REFERENCES template.operation (operation_id) MATCHSIMPLEONUPDATENOACTIONON DELETESETNULL,CONSTRAINT uniq_operation_id_title UNIQUE (id_title));
В ней мы устанавливаем ограничение уникальности значений в поле id_title, которое будем использовать для указания системных назначений платежей.
А в поле income будем отмечать, является ли назначение платежей доходом (true) или расходом (false).
Давайте сразу добавим системное назначение платежей "Оплата заказа", которое нам понадобится в этом уроке:
INSERT INTO template.operation(id_title, title, income)VALUES ('OrderPaymentOperation', 'Оплата заказа', true);
Функция удаления записи
Создадим функцию для удаления назначения платежей:
CREATE OR REPLACEFUNCTIONtemplate.operation_try_delete(in_operation_id bigint)RETURNScharacter varying AS$BODY$DECLAREBEGINIF (SELECTNOTEXISTS(SELECT*FROM template.operation WHERE operation_id = in_operation_id)) THENRETURNNULL;ENDIF;IF (SELECTEXISTS(SELECT*FROM template.operation WHERE operation_category_id = in_operation_id)) THENRETURN'parent';ENDIF; IF (SELECT used from template.is_used('operation', 'operation_id', ARRAY['operation'], ARRAY[]::text[], in_operation_id)) THEN
UPDATE template.operationSET archive = TRUEWHERE operation_id = in_operation_id ANDNOT archive;RETURN'used';ELSEDELETEFROM template.operationWHERE operation_id = in_operation_id;RETURNNULL;ENDIF;END;$BODY$LANGUAGE plpgsql;
При удалении записи необходимо учитывать, что назначение может быть родительским для других назначений платежей, и использовать другую логику для обработки таких ситуаций. Поэтому функция возвращает тип character varying, в котором может быть одно из значений:
parent - запись является родительской;
used - запись используется в программе и перемещена в архив;
NULL - запись удалена, либо ее нет в базе данных.
Вспомогательная таблица
Давайте создадим таблицу для кассы. Пока она пригодится нам для работы со списком назначений платежей и оплатами в заказе, а позже на ее основе будет реализован модуль кассы.
Выполним скрипт:
CREATESEQUENCEtemplate.cash_id_seq;CREATETABLEtemplate.cash( cash_id bigintNOT NULLDEFAULT nextval('template.cash_id_seq'::regclass), cash_date timestamp without time zoneNOT NULL, account_id smallintNOT NULL, operation_id integerNOT NULL, summ numericNOT NULL, date_created timestamp without time zoneNOT NULLDEFAULTnow(), deleted booleanNOT NULLDEFAULT false, date_deleted timestamp without time zone,CONSTRAINT pk_cash_id PRIMARY KEY (cash_id),CONSTRAINT fk_account_id FOREIGN KEY (account_id)REFERENCES template.account (account_id) MATCHSIMPLEONUPDATENOACTIONON DELETENOACTION,CONSTRAINT fk_operation_id FOREIGN KEY (operation_id)REFERENCES template.operation (operation_id) MATCHSIMPLEONUPDATENOACTIONON DELETENOACTION);
Отлично! Теперь можем заняться формой для редактирования списка операций.
Форма списка
На главной форме (TemplateStart.xml) в меню добавим пункт Списки -> Назначения платежей..., по которому будет открываться новая форма (TemplateOperationList.xml).
Создадим необходимые формы для списка назначений платежей с помощью шаблона ArchiveList:
Перейдем в файл описания работы серверной части приложения (Template.xml) и скорректируем запрос OperationSelectSqlQuery:
Template.xml
<SqlQueryName="OperationSelectSqlQuery"> <Text> SELECT true AS "Expand", -2::bigint AS "OperationId", NULL::bigint AS "OperationCategoryId", 'ДОХОД' AS "Title", false AS "System", false AS "Archive" UNION ALL SELECT true AS "Expand", -1::bigint, NULL::bigint, 'РАСХОД', false, false UNION ALL SELECT true AS "Expand", O.operation_id AS "OperationId", COALESCE(O.operation_category_id, CASE WHEN income THEN -2 ELSE -1 END) AS "OperationCategoryId", O.title AS "Title", O.id_title NOTNULL AS "System", O.archive AS "Archive" FROM template.operation O; </Text></SqlQuery>
Назначения платежей на форме будут представлены в виде дерева и разбиты на два блока, в которых записи "ДОХОД" и "РАСХОД" будут корневыми категориями.
Поле Expand будет отвечать за разворачивание/сворачивание узлов дерева.
Список назначений платежей
Перейдем в файл TemplateOperationList.xml. В описание тэга <Appearance> добавим шрифт для корневых категорий:
Для создания дерева записей в таблице DatabaseTable будем использовать TreeGetDataConnection, который на основе данных из OperationPrimaryGetDataConnection будет строить дерево.
В тэге <SourceDataConnection> указываем источник данных для дерева. Первых три поля являются обязательными и их порядок строго определен. Первое поле должно соответствовать идентификатору элемента, второе - его отображаемому значению, третье - его состоянию (свернут/развернут узел дерева, если он имеет дочерние элементы).
Помимо обязательных полей можно указывать любое количество дополнительных полей.
В тэге <RelationshipDataConnection> указываем таблицу с двумя колонками: в первой колонке должен быть идентификатор элемента, а во второй - идентификатор родительского элемента. На основе этой таблицы TreeGetDataConnection построит дерево.
В тэге <AdditionalColumns> задаем имена для дополнительных колонок: одно для колонки с признаком наличия дочерних элементов (HasChildren), другое для колонки отображения значения состояния узла (State).
Укажем OperationTreeGetDataConnection в качестве источника данных для таблицы OperationDatabaseTable.
Заменим тэг <Columns> в описании таблицы кодом, в котором описаны все колонки из OperationPrimaryGetDataConnection и дополнительные колонки из OperationTreeGetDataConnection:
Запустим приложение и проверим загрузку формы списка и отображение дерева назначений платежей.
Отлично! Переключимся на форму редактирования назначения платежей, а затем вернемся на форму списка и доделаем дерево.
Карточка сущности
В файле TemplateOperationEdit.xml создайте необходимые объекты, чтобы у вас получилась форма вида:
Добавьте на форму параметр OperationCategoryId.
Переделайте OperationPrimaryGetDataConnection, добавив в SQL-запрос поля:
OperationCategoryId - для получения значений используйте то же выражение, которое используем в построении дерева на форме списка назначений платежей (OperationSelectSqlQuery);
IsIncome и IsSystem - они нам понадобятся для проверки в случае изменения типа (доход или расход) на противоположный у системных назначений и у назначений, которые используются в программе.
Список категорий
Перейдем в серверный xml-файл и создадим запрос для получения списка доступных категорий:
Template.xml
<SqlQueryName="OperationCategorySelectSqlQuery"> <Text> WITH RECURSIVE tree (operation_id, operation_category_id, title, id_title, income, path, archive) AS ( SELECT -2::bigint, NULL::bigint, 'ДОХОД', NULL, true, ARRAY[-2]::bigint[], false UNION SELECT -1::bigint, NULL::bigint, 'РАСХОД', NULL, false, ARRAY[-1]::bigint[], false UNION SELECT operation_id AS operation_id, operation_category_id, title, id_title, income, ARRAY[CASE WHEN income THEN -2 ELSE -1 END, operation_id]::bigint[] AS path, archive
FROM _t UNION SELECT o2.operation_id AS operation_id, o2.operation_category_id, o2.title, o2.id_title, o2.income, tree.path || ARRAY[o2.operation_id]::bigint[] AS path, o2.archive
FROM _t, _o o2 INNER JOIN tree ON tree.operation_id = o2.operation_category_id ), _t AS ( SELECT * FROM _o WHERE operation_category_id IS NULL ), _o AS( SELECT * FROM template.operation o WHERE operation_id IS DISTINCT FROM {OperationId} AND id_title IS NULL ) SELECT T.operation_id AS "OperationId", CASE WHEN array_length(T.path, 1) > 1 THEN COALESCE(repeat(' ', (array_length(T.path, 1)-1)*2) || ' ', '') ELSE '' END || T.title AS "Title",
income AS "IsIncome" FROM tree T LEFT JOIN ( WITH RECURSIVE current_tree (operation_id, operation_category_id) AS( SELECT operation_id, operation_category_id FROM template.operation O WHERE {OperationCategoryId} IS NOT NULL AND operation_id = {OperationCategoryId} UNION SELECT O.operation_id, COALESCE(O.operation_category_id, CASE WHEN income THEN -2 ELSE -1 END)::bigint FROM template.operation O JOIN current_tree T ON (O.operation_id = T.operation_category_id) ), t AS ( SELECT * FROM current_tree UNION SELECT -2, NULL WHERE EXISTS (SELECT * FROM current_tree WHERE operation_category_id = -2) UNION SELECT -1, NULL WHERE EXISTS (SELECT * FROM current_tree WHERE operation_category_id = -1) ) SELECT * FROM t) T2 ON (T.operation_id = T2.operation_id) WHERE (NOT archive OR T2.operation_id IS NOT NULL) ORDER BY T.path; </Text></SqlQuery>
Вернемся в файл формы редактирования назначения платежей (TemplateOperationEdit.xml).
Создадим соединение с данными для списка доступных категорий:
Укажем новый OperationCategoryPrimaryGetDataConnection в тэге <ValueList> выпадающего списка поля "Категория", а в качестве значения тэга <Value> того же объекта напишем конструкцию:
При редактировании системных назначений платежей или назначений, на которые есть кассовые операции, мы должны запрещать пользователю изменять категорию назначения, если это приведет к смене типа назначения платежей (приход/расход).
Для начала нам понадобятся данные о выбранной категории, для этого создадим SecondaryGetDataConnection:
<SqlQueryName="OperationIsUsedSelectSqlQuery"> <Text> SELECT template.operation_is_used_in_view_of_child({OperationId}::bigint) AS "IsUsed"; </Text></SqlQuery>
Где функция template.operation_is_used_in_view_of_child(bigint) имеет вид:
CREATE OR REPLACEFUNCTIONtemplate.operation_is_used_in_view_of_child(in_operation_id bigint)RETURNSbooleanAS$BODY$DECLARE _operation_id_array bigint[]; _is_system boolean;BEGINWITHRECURSIVE operation_tree(operation_id, id_title) AS(SELECT operation_id, id_titleFROM template.operationWHERE operation_id = in_operation_idUNION ALLSELECT O.operation_id, O.id_titleFROM template.operation OJOIN operation_tree T ON (O.operation_category_id = T.operation_id) )SELECT array_agg(operation_id), bool_or(id_title IS NOT NULL)INTO _operation_id_array, _is_systemFROM operation_tree; RETURN (_is_system OR EXISTS(SELECT * FROM template.cash C WHERE C.operation_id = ANY(_operation_id_array) AND NOT C.deleted));
END;$BODY$LANGUAGE plpgsql;
Создайте самостоятельно команду OperationIsUsedDataConnectionRefreshCommand для обновления OperationIsUsedPrimaryGetDataConnection.
Добавим условие EqualCondition для проверки поля IsUsed:
Создадим PrimaryGetDataConnection для проверки, является ли выбранное в качестве категории назначение платежей листом, т.е. не имеет дочерних назначений:
<SqlQueryName="OperationIsLeafSelectSqlQuery"> <Text> SELECT operation_id AS "OperationId", title AS "Title", income AS "IsIncome", NOT EXISTS(SELECT * FROM template.operation WHERE operation_category_id = O.operation_id) AS "IsLeaf", EXISTS(SELECT * FROM template.cash C WHERE C.operation_id = O.operation_id) AS "IsUsed" FROM template.operation O WHERE operation_id = {OperationId}; </Text></SqlQuery>
Создайте самостоятельно команду OperationIsLeafDataConnectionRefreshCommand для обновления OperationIsLeafPrimaryGetDataConnection.
Добавим условия EqualCondition для проверки полей IsLeaf и IsUsed выбранной категории:
Создадим команду MessageBoxCommand для уведомления пользователя о невозможности изменения типа редактируемого назначения платежей:
TemplateOperationEdit.xml
<CommandName="OperationChangedIsIncomeMessageBoxCommand"Type="MessageBoxCommand"Assembly="Commands"> <Caption>Сохранение</Caption> <Text> <String> <Format>При изменении категории будет изменен тип назначения «{0}» с «{1}» на «{2}», что недопустимо, т.к. назначение «{0}» {3}</Format>
<Items> <Item> <ObjectName="TitleTextBox" /> </Item> <Item> <Switch> <Case> <When> <DataConnectionSourceDataConnection="OperationPrimaryGetDataConnection"> <Fields> <FieldName="IsIncome" /> </Fields> </DataConnection> </When> <Then>Дохода</Then> </Case> <Case>Расхода</Case> </Switch> </Item> <Item> <Switch> <Case> <When> <DataConnectionSourceDataConnection="OperationPrimaryGetDataConnection"> <Fields> <FieldName="IsIncome" /> </Fields> </DataConnection> </When> <Then>Расход</Then> </Case> <Case>Доход</Case> </Switch> </Item> <Item> <Switch> <Case> <When> <ConditionName="OperationIsSystemEqualCondition" /> </When> <Then>является системным.</Then> </Case> <Case> <When> <ConditionName="OperationIsUsedEqualCondition" /> </When> <Then>используется в программе.</Then> </Case> </Switch> </Item> </Items> </String> </Text> <IconType="Warning" /> <ButtonsType="Ok" /></Command>
Создадим команду MessageBoxCommand для уведомления пользователя, что кассовые операции на выбранное назначение платежа будут привязаны к редактируемому назначению:
TemplateOperationEdit.xml
<CommandName="OperationIsLeafMessageBoxCommand"Type="MessageBoxCommand"Assembly="Commands"> <Caption>Сохранение</Caption> <Text> <String> <Format>При добавлении назначения платежа «{0}» в категорию «{1}» все операции, привязанные к назначению «{1}», будут привязаны к назначению «{0}». Продолжить?</Format>
<Items> <Item> <ObjectName="TitleTextBox" /> </Item> <Item> <DataConnectionSourceDataConnection="OperationIsLeafPrimaryGetDataConnection"> <Fields> <FieldName="Title" /> </Fields> </DataConnection> </Item> </Items> </String> </Text> <IconType="Question" /> <ButtonsType="YesNo" /></Command>
Теперь у нас есть все необходимые условия и команды и мы можем заменить синтаксис команды SaveSequentialCommand на:
В команде в первым блоке <If> проверяем, не является ли редактируемое назначение платежей системным, и совпадает ли его тип с типом выбранной категории. Если тип не совпадает и назначение не системное, то отправляем запрос на сервер для проверки использования назначения платежа.
Во втором блоке <If> проверяем, является ли редактируемое назначение платежей системным или оно используется в программе и его тип не совпадает с типом выбранной категории. Если условие выполняется, то предупреждаем пользователя о смене типа назначения платежей при изменении его категории. Иначе выполняется большой вложенный блок проверок, в котором сначала проверяется, является ли редактируемое назначение платежей листом и выводится соответствующий вопрос пользователю. Затем идет завершающая проверка на возможность сохранения изменений.
Сохранение назначения платежей
Добавим параметры в OperationInsertSetDataConnection: