В этом уроке мы рассмотрим второй вариант работы с иерархической структурой данных в виде дерева, отображаемого в таблице 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:
<SqlQueryName="OperationInsertSqlQuery"> <Text> INSERT INTO template.operation ( title, operation_category_id, income ) VALUES ( {Title}, {OperationCategoryId}, {IsIncome} ) RETURNING operation_id; UPDATE template.cash SET operation_id = currval('template.operation_id_seq') WHERE {OperationCategoryIsLeaf} AND operation_id = {OperationCategoryId}; </Text></SqlQuery>
Вторая часть запроса необходима для того, чтобы кассовые операции привязать к новому назначению платежей, если старое перестало быть листом. Функция currval позволяет получить текущее значение последовательности, а в данном случае идентификатор только что добавленной записи.
В статье Функции nextval и currval в Базе знаний дана информация о способах получения значения последовательности.
Добавим параметры в OperationUpdateSetDataConnection:
<SqlQueryName="OperationUpdateSqlQuery"> <Text> UPDATE template.operation SET title = CASE WHEN id_title IS NULL THEN {Title} ELSE title END, operation_category_id = {OperationCategoryId}, income = {IsIncome} WHERE operation_id = {OperationId}; SELECT template.operation_update_child({OperationId}::bigint); UPDATE template.cash SET operation_id = {OperationId} WHERE {OperationCategoryIsLeaf} AND operation_id = {OperationCategoryId}; </Text></SqlQuery>
Третья часть запроса необходима для того, чтобы кассовые операции привязать к новому назначению платежей, если старое перестало быть листом.
Здесь помимо обновления данных самого назначения платежа необходимо вызвать функцию template.operation_update_child(bigint) на изменения типа назначения для всех дочерних элементов.
CREATE OR REPLACEFUNCTIONtemplate.operation_update_child(in_operation_id bigint)RETURNS void AS$BODY$DECLARE _child record; _parent record;BEGINSELECT*INTO _parent FROM template.operation WHERE operation_id = in_operation_id;FOR _child IN (SELECT*FROM template.operation WHERE operation_category_id = in_operation_id)LOOPUPDATE template.operationSET income = _parent.incomeWHERE operation_id = _child.operation_id; PERFORM template.operation_update_child(_child.operation_id);ENDLOOP;END;$BODY$LANGUAGE plpgsql;
Отлично! С карточкой редактирования назначения платежей мы закончили, теперь вернемся в файл списка назначений (TemplateOperationList.xml) и продолжим работать с ним.
Добавление записи
При нажатии на кнопку добавления записи в карточку назначения платежей через параметр OperationCategoryId будем передавать идентификатор выделенной в таблице записи. Этот идентификатор будем использовать для задания родительской категории нового назначения. Но родительской категорией не может быть ни системная запись, ни архивная запись.
Создадим условие EqualCondition проверки признака системного назначения платежей:
Отлично! Откройте приложение и создайте несколько назначений платежа.
Редактирование записи
Помимо запрета на редактирование архивных записей, мы должны ограничить редактирование служебных строк "ДОХОД" и "РАСХОД".
Создадим условие, где будем проверять, что идентификатор выбранной записи больше нуля. Служебные строки имеют отрицательные идентификаторы: запись "ДОХОД" имеет идентификатор -2, а запись "РАСХОД" - идентификатор -1.
Вторым условием будем проверять, что клик был по ячейке столбца State, который отвечает за сворачивание/разворачивание узлов. Для этого воспользуемся get-проперти LastCellClickedColumnName таблицы, чтобы получить имя колонки, по ячейке которой был последний клик:
Сворачивать/разворачивать мы можем только те узлы, у которых есть дочерние элементы. Здесь нам поможет get-проперти SelectedRowCellValueByColumnName таблицы, которое по имени колонки будет возвращать значение ячейки в выделенной строке:
Создайте на это условие Execution, оставив тэг <Commands> пустым.
Для сворачивания/разворачивания узла будем использовать set-проперти ToggleFoldingNode у TreeGetDataConnection, в параметр NodeId которого будем передавать идентификатор выбранной записи в таблице:
С помощью этой команды мы будем изменять данные в OperationTreeGetDataConnection. Однако это приведет к изменению источника данных в таблице, что послужит причиной для бесконечного цикла выполнения Execution на условие OperationToggleFoldingNodeNestedCondition. Поэтому мы создадим переменную, которую будем использовать для блокировки последовательности команд сворачивания/разворачивания:
Запустите приложение и проверьте возможность сворачивать/разворачивать узлы дерева.
Удаление записи
Нельзя удалять служебные записи "ДОХОД" и "РАСХОД" и назначения платежей, которые находятся в архиве, содержат дочерние элементы или являются системными.
Добавим необходимые условия в тэг <Enabled> кнопки OperationDeleteButton:
TemplateOperationList.xml
<MyObjectName="OperationDeleteButton"Type="Button"Assembly="BaseControls"> <Top> <Calculate> <Expression>{0} + 5</Expression> <Items> <Item> <ObjectName="OperationEditButton"> <PropertyName="Bottom" /> </Object> </Item> </Items> </Calculate> </Top> <Left> <ObjectName="OperationAddButton"> <PropertyName="Left" /> </Object> </Left> <Width> <ObjectName="OperationAddButton"> <PropertyName="Width" /> </Object> </Width> <Height> <ObjectName="OperationAddButton"> <PropertyName="Height" /> </Object> </Height> <TabIndex>5</TabIndex> <Hint>Удалить запись</Hint> <BackgroundImage>Images\24x24\delete.png</BackgroundImage> <BackgroundImageLayout>Center</BackgroundImageLayout> <FlatStyle>Flat</FlatStyle> <FlatBorderSize>1</FlatBorderSize> <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor> <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor> <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor> <Enabled> <And> <ConditionName="OperationSelectedCondition" /> <Not> <ConditionName="SelectedOperationIsArchiveCondition" /> </Not> <ConditionName="SelectedOperationIdGreater0Condition" /> <ConditionName="SelectedOperationIsNotSystemCondition" /> <Not> <ConditionName="SelectedOperationHasChildrenCondition" /> </Not> </And> </Enabled> <Commands> <CommandName="OperationDeleteMessageBoxCommand" /> </Commands> <DisabledMode>True</DisabledMode> <DisabledText> <Switch> <Case> <When> <Not> <ConditionName="OperationSelectedCondition" /> </Not> </When> <Then>Выберите запись для удаления.</Then> </Case> <Case> <When> <ConditionName="SelectedOperationIsArchiveCondition" /> </When> <Then>Нельзя удалить архивную запись.</Then> </Case> <Case> <When> <Not> <ConditionName="SelectedOperationIdGreater0Condition" /> </Not> </When> <Then>Выбранная запись является служебной и не может быть удалена.</Then> </Case> <Case> <When> <Not> <ConditionName="SelectedOperationIsNotSystemCondition" /> </Not> </When> <Then>Выбранное назначение платежа является системным и не может быть удалено.</Then> </Case> <Case> <When> <ConditionName="SelectedOperationHasChildrenCondition" /> </When> <Then>Выбранное назначение платежа содержит дочерние элементы и не может быть удалено.</Then> </Case> </Switch> </DisabledText></MyObject>
Так как теперь функция template.operation_try_delete(bigint) возвращает строку, то нам нужно переделать проверку результата команды удаления.
Создадим условия проверки результата выполнения команды OperationDeleteSaveCommand:
Создадим команду MessageBoxCommand вывода сообщения, что выбранное назначение содержит дочерние элементы:
TemplateOperationList.xml
<Command Name="OperationHasChildrenWarningMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Удаление</Caption>
<Text>Выбранное назначение платежа содержит дочерние элементы и не может быть удалено.</Text>
<Icon Type="Information" />
<Buttons Type="Ok" />
</Command>
Переделаем Execution, связанный с удалением записи:
Запустите приложение и проверьте удаление назначения платежа.
Архивирование записи
Мы не можем отправлять в архив служебные записи "ДОХОД" и "РАСХОД" и системные назначения платежей.
Добавим необходимые условия в тэг <Enabled> кнопки OperationArchiveButton:
TemplateOperationList.xml
<MyObject Name="OperationArchiveButton" Type="Button" Assembly="BaseControls">
<Top>
<Calculate>
<Expression>{0} + 5</Expression>
<Items>
<Item>
<Object Name="OperationDeleteButton">
<Property Name="Bottom" />
</Object>
</Item>
</Items>
</Calculate>
</Top>
<Left>
<Object Name="OperationAddButton">
<Property Name="Left" />
</Object>
</Left>
<Width>
<Object Name="OperationAddButton">
<Property Name="Width" />
</Object>
</Width>
<Height>
<Object Name="OperationAddButton">
<Property Name="Height" />
</Object>
</Height>
<TabIndex>6</TabIndex>
<Hint>
<Switch>
<Case>
<When>
<Condition Name="SelectedOperationIsArchiveCondition" />
</When>
<Then>Восстановить запись из архива</Then>
</Case>
<Case>Перенести запись в архив</Case>
</Switch>
</Hint>
<BackgroundImage>
<Switch>
<Case>
<When>
<Or>
<Condition Name="SelectedOperationIsArchiveCondition" />
<Object Name="ArchiveFilterComboBox" />
</Or>
</When>
<Then>Images\24x24\unarchive.png</Then>
</Case>
<Case>Images\24x24\archive.png</Case>
</Switch>
</BackgroundImage>
<BackgroundImageLayout>Center</BackgroundImageLayout>
<FlatStyle>Flat</FlatStyle>
<FlatBorderSize>1</FlatBorderSize>
<FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
<FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
<FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
<Enabled>
<And>
<Condition Name="OperationSelectedCondition" />
<Condition Name="SelectedOperationIdGreater0Condition" />
<Condition Name="SelectedOperationIsNotSystemCondition" />
</And>
</Enabled>
<Commands>
<Command Name="OperationArchiveMessageBoxCommand" />
</Commands>
<DisabledMode>True</DisabledMode>
<DisabledText>
<Switch>
<Case>
<When>
<Not>
<Condition Name="OperationSelectedCondition" />
</Not>
</When>
<Then>
<Switch>
<Case>
<When>
<Object Name="ArchiveFilterComboBox" />
</When>
<Then>Для восстановления из архива выберите запись.</Then>
</Case>
<Case>Для переноса в архив выберите запись.</Case>
</Switch>
</Then>
</Case>
<Case>
<When>
<Not>
<Condition Name="SelectedOperationIdGreater0Condition" />
</Not>
</When>
<Then>Выбранная запись является служебной и не может быть перенесена в архив.</Then>
</Case>
<Case>
<When>
<Not>
<Condition Name="SelectedOperationIsNotSystemCondition" />
</Not>
</When>
<Then>Выбранное назначение платежа является системным и не может быть перенесено в архив.</Then>
</Case>
</Switch>
</DisabledText>
</MyObject>
Теперь нам нужно переделать логику переноса в архив назначений платежа. Запрос OperationArchiveUpdateSqlQuery, который создался при выполнении паттерна создания формы списка, нам не подходит, так как у нас есть иерархическая структура назначений платежа.
В OperationArchiveSetDataConnection добавьте параметр Archive. Передавайте в него значение, противоположное значению из ячейки колонки Archive в выделенной строке таблицы. Для этого используйте тэг <Not>.
Переделайте текст запроса OperationArchiveUpdateSqlQuery:
CREATE OR REPLACE FUNCTION template.operation_try_archive(
in_operation_id bigint,
in_archive boolean)
RETURNS character varying AS
$BODY$
DECLARE
_is_system boolean;
BEGIN
WITH RECURSIVE tree (operation_id, id_title) AS (
SELECT
operation_id,
id_title
FROM template.operation
WHERE operation_id = in_operation_id
UNION
SELECT
O.operation_id,
O.id_title
FROM
template.operation O
JOIN tree T ON O.operation_category_id = T.operation_id
)
SELECT bool_or(id_title IS NOT NULL) FROM tree INTO _is_system;
IF (_is_system) THEN
RETURN 'system';
END IF;
UPDATE template.operation
SET archive = in_archive
WHERE operation_id = in_operation_id;
PERFORM template.operation_archive_child(in_operation_id) WHERE in_archive;
PERFORM template.operation_unarchive_parent(in_operation_id) WHERE NOT in_archive;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql;
Функция архивирования дочерних элементов:
CREATE OR REPLACE FUNCTION template.operation_archive_child(in_operation_id bigint)
RETURNS void AS
$BODY$
DECLARE
_archive boolean;
_operation_id bigint;
BEGIN
SELECT archive INTO _archive FROM template.operation WHERE operation_id = in_operation_id;
IF (NOT _archive) THEN RETURN; END IF;
FOR _operation_id IN (SELECT operation_id FROM template.operation WHERE operation_category_id = in_operation_id)
LOOP
UPDATE template.operation
SET archive = _archive
WHERE operation_id = _operation_id;
PERFORM template.operation_archive_child(_operation_id);
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
Функция разархивирования родительской записи:
CREATE OR REPLACE FUNCTION template.operation_unarchive_parent(in_operation_id bigint)
RETURNS void AS
$BODY$
DECLARE
_operation_category_id bigint;
_archive boolean;
BEGIN
SELECT operation_category_id, archive INTO _operation_category_id, _archive
FROM template.operation WHERE operation_id = in_operation_id;
IF (_archive) THEN RETURN; END IF;
IF (_operation_category_id IS NOT NULL) THEN
UPDATE template.operation
SET archive = _archive
WHERE operation_id = _operation_category_id;
PERFORM template.operation_unarchive_parent(_operation_category_id);
END IF;
END;
$BODY$
LANGUAGE plpgsql;
Запустите приложение и проверьте работу с архивом.
Список счетов
Добавим в приложение список счетов, который будем использовать в кассовых операциях и в оплатах в заказе.
По завершению раздела у нас получатся формы:
База данных
Выполним скрипт на создание таблицы в базе данных:
CREATE SEQUENCE template.account_id_seq;
CREATE TABLE template.account
(
account_id smallint NOT NULL DEFAULT nextval('template.account_id_seq'::regclass),
title character varying NOT NULL,
cash boolean NOT NULL,
archive boolean NOT NULL DEFAULT false,
CONSTRAINT pk_account_id PRIMARY KEY (account_id)
);
В поле cash будем задавать тип счета: наличный (true) или безналичный (false).
Форма списка
На главной форме (TemplateStart.xml) в меню добавим пункт Списки -> Счета..., по которому будет открываться новая форма (TemplateAccountList.xml).
Создадим необходимые формы для списка счетов с помощью шаблона ArchiveList:
Самостоятельно скорректируйте SQL-запрос для получения списка счетов и связанный с ним PrimaryGetDataConnection, добавив колонку AccountType, в которую должны попадать данный из поля cash таблицы template.account.
На форме списка счетов (TemplateAccountList.xml) в таблицу добавьте колонки:
Для задания значений выпадающего списка так же используем структуру <Structure Type="Table">.
Запустите приложение, проверьте загрузку форм и добавьте несколько счетов, с которыми позже будем работать.
Отлично! Теперь можем перейти к заказам и добавить в них таблицу оплат.
Оплаты в заказе
В этом разделе на форме заказа создадим таблицу оплат и кнопки для ее редактирования, а также добавим итоговые поля с суммой всех оплат в заказе и итоговой задолженности:
База данных
Выполним скрипт на создание таблицы в базе данных:
CREATE SEQUENCE template.order_payment_id_seq;
CREATE TABLE template.order_payment
(
order_payment_id bigint NOT NULL DEFAULT nextval('template.order_payment_id_seq'::regclass),
order_id bigint NOT NULL,
cash_id bigint NOT NULL,
CONSTRAINT pk_order_payment_id PRIMARY KEY (order_payment_id),
CONSTRAINT fk_cash_id FOREIGN KEY (cash_id)
REFERENCES template.cash (cash_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fk_order_id FOREIGN KEY (order_id)
REFERENCES template."order" (order_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
Дату, счет и сумму оплаты мы будем хранить в таблице template.cash, которую создали ранее в уроке, а в таблице template.order_payment будем хранить привязку кассовой операции к заказу через их идентификаторы.
В серверном файле (Template.xml) переделаем запрос на получение списка оплат в заказе:
Template.xml
<SqlQuery Name="OrderPaymentSelectSqlQuery">
<Text>
SELECT
OP.order_payment_id AS "OrderPaymentId",
C.cash_id AS "CashId",
C.cash_date AS "Date",
C.account_id AS "AccountId",
C.summ AS "Summ"
FROM
template.order_payment OP
JOIN template.cash C USING(cash_id)
WHERE
NOT C.deleted AND
OP.order_id = {OrderId}
ORDER BY C.cash_date, OP.order_payment_id;
</Text>
</SqlQuery>
Так же скорректируем запрос на удаление оплаты:
Template.xml
<SqlQuery Name="OrderPaymentDeleteSqlQuery">
<Text>
UPDATE template.cash
SET
deleted = TRUE,
date_deleted = NOW()
FROM
template.order_payment OP
WHERE
cash.cash_id = OP.cash_id AND
OP.order_payment_id = {OrderPaymentId};
</Text>
</SqlQuery>
Форма заказа
Перейдем в файл карточки заказа (TemplateOrderEdit.xml). Под панелью OrderPositionPanel создадим панель OrderPaymentPanel высотой200.
Таблица оплат
С помощью паттерна Table создадим таблицу оплат, поместив курсор в описание панели OrderPaymentPanel.
Скорректируйте таблицу OrderPaymentDatabaseTable, добавив необходимые колонки. Для колонки Счет (AccountTitle) используйте тэг <Substitution> по колонке AccountId. Для этого создайте AccountPrimaryGetDataConnection. Так же добавьте атрибут ChangeForm="False" для таблицы оплат, чтобы изменение ее источника данных не приводило к изменению формы и активации кнопки "Сохранить" - сохранение оплаты уже было на дочерней форме.
Не забудьте добавить соответствующие поля в OrderPaymentPrimaryGetDataConnection.
Итоговые значения
На панель TotalPanel добавьте поля "Сумма оплат" (TotalPaidSumTextBox) и "Задолженность" (TotalDebtTextBox).
Для TotalPaidSumTextBox создадим переменную TotalPaidSumVariable:
Создайте в файле карточки оплаты (TemplateOrderPaymentEdit.xml) все необходимые элементы, чтобы у вас получилась форма вида:
При добавлении новой оплаты передавайте на форму сумму задолженности по заказу и подставляйте ее в поле "Сумма".
Переделаем запросы:
OrderPaymentByIdSelectSqlQuery
Template.xml
<SqlQuery Name="OrderPaymentByIdSelectSqlQuery">
<Text>
SELECT
C.cash_date AS "Date",
C.account_id AS "AccountId",
C.summ AS "Summ"
FROM
template.order_payment OP
JOIN template.cash C USING(cash_id)
WHERE
OP.order_payment_id = {OrderPaymentId};
</Text>
</SqlQuery>
OrderPaymentInsertSqlQuery
Template.xml
<SqlQuery Name="OrderPaymentInsertSqlQuery">
<Text>
INSERT INTO template.cash(
cash_date,
account_id,
operation_id,
summ
)
SELECT
{Date},
{AccountId},
O.operation_id,
{Summ}
FROM
template.operation O
WHERE
id_title = 'OrderPaymentOperation';
INSERT INTO template.order_payment(
order_id,
cash_id
)
SELECT
{OrderId},
currval('template.cash_id_seq'::regclass)
RETURNING order_payment_id;
</Text>
</SqlQuery>
Функция currval позволяет получить текущее значение последовательности, а в данном случае идентификатор только что добавленной записи.
В статье Функции nextval и currval в Базе знаний дана информация о способах получения значения последовательности.
OrderPaymentUpdateSqlQuery
Template.xml
<SqlQuery Name="OrderPaymentUpdateSqlQuery">
<Text>
UPDATE template.cash
SET
cash_date = {Date},
account_id = {AccountId},
summ = {Summ}
FROM
template.order_payment OP
WHERE
cash.cash_id = OP.cash_id AND
OP.order_payment_id = {OrderPaymentId};
</Text>
</SqlQuery>
Отлично! Запустите приложение и проверьте загрузку форм. Добавьте несколько оплат в заказ.
Осталось на главной форме в списке заказов реализовать отображение информации об оплатах и задолженностях по заказам.
Список заказов
На главной форме (TemplateStart) в таблицу OrderDatabaseTable добавьте колонки: Сумма заказа (TotalOrderCost), Сумма оплат (TotalPaymentSumm) и Задолженность (TotalDebt):
Скорректируйте текст запроса OrderSelectSqlQuery.
Итоги
В этом уроке мы познакомились с TreeGetDataConnection и возможностью отображать дерево записей в таблице DatabaseTable. На следующем уроке вы самостоятельно реализуете модуль кассы и отчет по бюджету с выгрузкой его в документ Excel с помощью команды ExportToExcelCommand.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.