В этом уроке мы рассмотрим второй вариант работы с иерархической структурой данных в виде дерева, отображаемого в таблице DatabaseTable.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Список назначений платежей
Добавим в приложение список назначений платежей, который будет являться иерархической структурой с категориями и подкатегориями. Этот список на форме будет отображаться в виде дерева в таблице DatabaseTable. Пользователь будет иметь возможность сворачивать и разворачивать ветви дерева.
По завершению раздела у нас получатся формы:
База данных
Таблица для назначений платежей
Выполним скрипт на создание таблицы в базе данных:
CREATE SEQUENCE template.operation_id_seq;
CREATE TABLE template.operation
(
operation_id integer NOT NULL DEFAULT nextval('template.operation_id_seq'::regclass),
operation_category_id bigint,
id_title character varying,
title character varying NOT NULL,
income boolean NOT NULL,
archive boolean NOT NULL DEFAULT false,
CONSTRAINT pk_operation_id PRIMARY KEY (operation_id),
CONSTRAINT fk_operation_category_id FOREIGN KEY (operation_category_id)
REFERENCES template.operation (operation_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE SET NULL,
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 REPLACE FUNCTION template.operation_try_delete(in_operation_id bigint)
RETURNS character varying AS
$BODY$
DECLARE
BEGIN
IF (SELECT NOT EXISTS(SELECT * FROM template.operation WHERE operation_id = in_operation_id)) THEN
RETURN NULL;
END IF;
IF (SELECT EXISTS(SELECT * FROM template.operation WHERE operation_category_id = in_operation_id)) THEN
RETURN 'parent';
END IF;
IF (SELECT used from template.is_used('operation', 'operation_id', ARRAY['operation'], ARRAY[]::text[], in_operation_id)) THEN
UPDATE template.operation
SET archive = TRUE
WHERE operation_id = in_operation_id AND NOT archive;
RETURN 'used';
ELSE
DELETE FROM template.operation
WHERE operation_id = in_operation_id;
RETURN NULL;
END IF;
END;
$BODY$
LANGUAGE plpgsql;
При удалении записи необходимо учитывать, что назначение может быть родительским для других назначений платежей, и использовать другую логику для обработки таких ситуаций. Поэтому функция возвращает тип character varying, в котором может быть одно из значений:
parent - запись является родительской;
used - запись используется в программе и перемещена в архив;
NULL - запись удалена, либо ее нет в базе данных.
Вспомогательная таблица
Давайте создадим таблицу для кассы. Пока она пригодится нам для работы со списком назначений платежей и оплатами в заказе, а позже на ее основе будет реализован модуль кассы.
Выполним скрипт:
CREATE SEQUENCE template.cash_id_seq;
CREATE TABLE template.cash
(
cash_id bigint NOT NULL DEFAULT nextval('template.cash_id_seq'::regclass),
cash_date timestamp without time zone NOT NULL,
account_id smallint NOT NULL,
operation_id integer NOT NULL,
summ numeric NOT NULL,
date_created timestamp without time zone NOT NULL DEFAULT now(),
deleted boolean NOT NULL DEFAULT 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) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fk_operation_id FOREIGN KEY (operation_id)
REFERENCES template.operation (operation_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
Отлично! Теперь можем заняться формой для редактирования списка операций.
Форма списка
На главной форме (TemplateStart.xml) в меню добавим пункт Списки -> Назначения платежей..., по которому будет открываться новая форма (TemplateOperationList.xml).
Создадим необходимые формы для списка назначений платежей с помощью шаблона ArchiveList:
Перейдем в файл описания работы серверной части приложения (Template.xml) и скорректируем запрос OperationSelectSqlQuery:
Template.xml
<SqlQuery Name="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
<SqlQuery Name="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:
<SqlQuery Name="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 REPLACE FUNCTION template.operation_is_used_in_view_of_child(in_operation_id bigint)
RETURNS boolean AS
$BODY$
DECLARE
_operation_id_array bigint[];
_is_system boolean;
BEGIN
WITH RECURSIVE operation_tree(operation_id, id_title) AS(
SELECT
operation_id,
id_title
FROM template.operation
WHERE operation_id = in_operation_id
UNION ALL
SELECT
O.operation_id,
O.id_title
FROM
template.operation O
JOIN 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_system
FROM 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 для проверки, является ли выбранное в качестве категории назначение платежей листом, т.е. не имеет дочерних назначений:
<SqlQuery Name="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
<Command Name="OperationChangedIsIncomeMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Сохранение</Caption>
<Text>
<String>
<Format>При изменении категории будет изменен тип назначения «{0}» с «{1}» на «{2}», что недопустимо, т.к. назначение «{0}» {3}</Format>
<Items>
<Item>
<Object Name="TitleTextBox" />
</Item>
<Item>
<Switch>
<Case>
<When>
<DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
<Fields>
<Field Name="IsIncome" />
</Fields>
</DataConnection>
</When>
<Then>Дохода</Then>
</Case>
<Case>Расхода</Case>
</Switch>
</Item>
<Item>
<Switch>
<Case>
<When>
<DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
<Fields>
<Field Name="IsIncome" />
</Fields>
</DataConnection>
</When>
<Then>Расход</Then>
</Case>
<Case>Доход</Case>
</Switch>
</Item>
<Item>
<Switch>
<Case>
<When>
<Condition Name="OperationIsSystemEqualCondition" />
</When>
<Then>является системным.</Then>
</Case>
<Case>
<When>
<Condition Name="OperationIsUsedEqualCondition" />
</When>
<Then>используется в программе.</Then>
</Case>
</Switch>
</Item>
</Items>
</String>
</Text>
<Icon Type="Warning" />
<Buttons Type="Ok" />
</Command>
Создадим команду MessageBoxCommand для уведомления пользователя, что кассовые операции на выбранное назначение платежа будут привязаны к редактируемому назначению:
TemplateOperationEdit.xml
<Command Name="OperationIsLeafMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Сохранение</Caption>
<Text>
<String>
<Format>При добавлении назначения платежа «{0}» в категорию «{1}» все операции, привязанные к назначению «{1}», будут привязаны к назначению «{0}». Продолжить?</Format>
<Items>
<Item>
<Object Name="TitleTextBox" />
</Item>
<Item>
<DataConnection SourceDataConnection="OperationIsLeafPrimaryGetDataConnection">
<Fields>
<Field Name="Title" />
</Fields>
</DataConnection>
</Item>
</Items>
</String>
</Text>
<Icon Type="Question" />
<Buttons Type="YesNo" />
</Command>
Теперь у нас есть все необходимые условия и команды и мы можем заменить синтаксис команды SaveSequentialCommand на:
В команде в первым блоке <If> проверяем, не является ли редактируемое назначение платежей системным, и совпадает ли его тип с типом выбранной категории. Если тип не совпадает и назначение не системное, то отправляем запрос на сервер для проверки использования назначения платежа.
Во втором блоке <If> проверяем, является ли редактируемое назначение платежей системным или оно используется в программе и его тип не совпадает с типом выбранной категории. Если условие выполняется, то предупреждаем пользователя о смене типа назначения платежей при изменении его категории. Иначе выполняется большой вложенный блок проверок, в котором сначала проверяется, является ли редактируемое назначение платежей листом и выводится соответствующий вопрос пользователю. Затем идет завершающая проверка на возможность сохранения изменений.
Сохранение назначения платежей
Добавим параметры в OperationInsertSetDataConnection:
<SqlQuery Name="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:
<SqlQuery Name="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 REPLACE FUNCTION template.operation_update_child(in_operation_id bigint)
RETURNS void AS
$BODY$
DECLARE
_child record;
_parent record;
BEGIN
SELECT * 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)
LOOP
UPDATE template.operation
SET income = _parent.income
WHERE operation_id = _child.operation_id;
PERFORM template.operation_update_child(_child.operation_id);
END LOOP;
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
<MyObject Name="OperationDeleteButton" Type="Button" Assembly="BaseControls">
<Top>
<Calculate>
<Expression>{0} + 5</Expression>
<Items>
<Item>
<Object Name="OperationEditButton">
<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>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>
<Condition Name="OperationSelectedCondition" />
<Not>
<Condition Name="SelectedOperationIsArchiveCondition" />
</Not>
<Condition Name="SelectedOperationIdGreater0Condition" />
<Condition Name="SelectedOperationIsNotSystemCondition" />
<Not>
<Condition Name="SelectedOperationHasChildrenCondition" />
</Not>
</And>
</Enabled>
<Commands>
<Command Name="OperationDeleteMessageBoxCommand" />
</Commands>
<DisabledMode>True</DisabledMode>
<DisabledText>
<Switch>
<Case>
<When>
<Not>
<Condition Name="OperationSelectedCondition" />
</Not>
</When>
<Then>Выберите запись для удаления.</Then>
</Case>
<Case>
<When>
<Condition Name="SelectedOperationIsArchiveCondition" />
</When>
<Then>Нельзя удалить архивную запись.</Then>
</Case>
<Case>
<When>
<Not>
<Condition Name="SelectedOperationIdGreater0Condition" />
</Not>
</When>
<Then>Выбранная запись является служебной и не может быть удалена.</Then>
</Case>
<Case>
<When>
<Not>
<Condition Name="SelectedOperationIsNotSystemCondition" />
</Not>
</When>
<Then>Выбранное назначение платежа является системным и не может быть удалено.</Then>
</Case>
<Case>
<When>
<Condition Name="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-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.