Урок 9. Список категорий
На этом уроке мы познакомимся с объектом DatabaseTree, который отображает данные в виде дерева значений, что позволяет лучше воспринимать иерархическую структуру данных. Для этого создадим форму списка товарно-материальных ценностей (ТМЦ), которые будут разделяться на категории.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Новый список
Для начала создадим простенький список единиц измерений, который пригодится при создании списка ТМЦ.
База данных
Для списка единиц измерения создайте таблицу template.unit с колонками:
unit_id smallint NOT NULL - первичный ключ;
title character varying NOT NULL - наименование;
short_title character varying NOT NULL - сокращенное обозначение;
archive boolean NOT NULL DEFAULT false - признак архивной записи.
Создайте функцию template.unit_try_delete(smallint).
Форма списка и карточка редактирования
Создайте формы для редактирования списка единиц измерения:

Для этого можете воспользоваться паттерном ArchiveList из архива:
Пример заполнение настроек паттерна для списка единиц измерения:

Редактор автоматически создаст формы списка (TemplateUnitList.xml) и карточки сущности (TemplateUnitEdit.xml), а также в серверный xml-файл добавит все необходимые запросы.
После применения паттерна перейдем в xml-файл серверной части и перенесем роль UnitEditRole в группу GuestGroup, а также добавим нужные поля на форму карточки сущности, колонки в таблицу на форме списка и скорректируем запросы.
На главной форме в меню добавьте пункт Списки -> Единицы измерения..., по которому будет открываться новая форма TemplateUnitList.xml.

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

База данных
Создадим в базе данных таблицы:
template.material_category - категории товарно-материальных ценностей;
template.material - товарно-материальные ценности.
CREATE SEQUENCE template.material_category_id_seq;
CREATE TABLE template.material_category
(
material_category_id bigint NOT NULL DEFAULT nextval('template.material_category_id_seq'::regclass),
parent_material_category_id bigint,
title character varying NOT NULL,
archive boolean NOT NULL DEFAULT false,
CONSTRAINT pk_material_category_id PRIMARY KEY (material_category_id),
CONSTRAINT fk_parent_material_category_id FOREIGN KEY (parent_material_category_id)
REFERENCES template.material_category (material_category_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
Поле parent_material_category_id будем использовать для хранения идентификатора родительской категории.
CREATE SEQUENCE template.material_id_seq;
CREATE TABLE template.material
(
material_id bigint NOT NULL DEFAULT nextval('template.material_id_seq'::regclass),
material_category_id bigint NOT NULL,
title character varying NOT NULL,
unit_id bigint NOT NULL,
unit_price numeric NOT NULL,
archive boolean NOT NULL DEFAULT false,
CONSTRAINT pk_material_id PRIMARY KEY (material_id),
CONSTRAINT fk_material_category_id FOREIGN KEY (material_category_id)
REFERENCES template.material_category (material_category_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fk_unit_id FOREIGN KEY (unit_id)
REFERENCES template.unit (unit_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
Подготовка формы
Создайте пустую форму TemplateMaterialList.xml с заголовком "Товарно-материальные ценности".
В главном меню на стартовой форме (TemplateStart.xml) добавьте пункт меню Списки -> ТМЦ..., по которому будет открываться новая форма со списком товарно-материальных ценностей.

Перейдем в файл TemplateMaterialList.xml, где в ContentPanel добавим три панели:
MaterialCategoryPanel - левая панель, в которой будет описываться дерево категорий товарно-материальные ценностей. Ей можно задать ширину в 400 пикселей;
MaterialPanel - правая панель, в которой будет описываться таблица товарно-материальных ценностей. Эта панель будет занимать все оставшееся место;
MaterialSeparatePanel - разделитель между панелей.
Дерево значений
Займемся левой частью формы, а именно созданием дерева категорий для товарно-материальных ценностей. В итоге у вас должна получится форма подобного вида:

В панели MaterialCategoryPanel создайте:
Надпись "Категории" (MaterialCategoryLabel), которая будет выступать заголовком для дерева значений;
Дерево категорий (MaterialCategoryDatabaseTree) - объект типа DatabaseTree. Тэг
<Items>
оставьте пустым - его заполним позже;Кнопки редактирования дерева категорий (добавить, редактировать и удалить).
Для проверки наличия выделенного узла дерева DatabaseTree будем использовать get-проперти SelectedItemId, которое будем проверять в условии типа IsNotNullCondition:
<Condition Name="MaterialCategorySelectedCondition" Type="IsNotNullCondition" Assembly="Conditions">
<Items>
<Item>
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Item>
</Items>
<DataType Type="IntegerDataType" />
</Condition>
Для кнопок добавления и редактирования категории создадим команды:
<Command Name="MaterialCategoryAddFormShowCommand" Type="FormShowCommand" Assembly="Commands">
<Xml Type="Path">TemplateMaterialCategoryEdit.xml</Xml>
<Show Type="None" />
<Multiple Allow="True" />
<Parameters>
<Parameter Name="ParentMaterialCategoryId">
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Parameter>
</Parameters>
</Command>
<Command Name="MaterialCategoryEditFormShowCommand" Type="FormShowCommand" Assembly="Commands">
<Xml Type="Path">TemplateMaterialCategoryEdit.xml</Xml>
<Show Type="None" />
<Multiple Allow="True" />
<Parameters>
<Parameter Name="Edit">True</Parameter>
<Parameter Name="MaterialCategoryId">
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Parameter>
</Parameters>
</Command>
В командах используем тэг <Multiple>
, чтобы иметь возможность открывать несколько экземпляров формы одновременно.
Для отслеживания двойного клика по узлу дерева используйте условие DoubleClickCondition. Создайте <Execution>
, который будет отслеживать это условие.
Создайте самостоятельно команду MaterialCategoryDeleteMessageBoxCommand для кнопки удаления записи в дереве категорий.
Конструкции <Execution>
на эти команды можете оставить пустыми.
Карточка редактирования узла дерева
Создадим форму для редактирования категории (TemplateMaterialCategoryEdit.xml), используя паттерн EntityForm:

Добавьте на форму объекты ParentMaterialCategoryLabel и ParentMaterialCategoryComboBox, которые будут описывать родительскую категорию. Тэги <ValueList>
и <Value>
для выпадающего списка оставьте пустыми - их заполним позже.
Таким образом, у вас должна получиться форма вида:

Перейдем в xml-файл серверной части (Template.xml) и создадим запрос для получения списка категорий:
<SqlQuery Name="ParentMaterialCategorySelectSqlQuery">
<Text>
WITH RECURSIVE tree_tmp (material_category_id, parent_material_category_id, title, path) AS (
SELECT material_category_id, parent_material_category_id, title, ARRAY[material_category_id]::bigint[]
FROM parent_mc
UNION ALL
SELECT MC.material_category_id, MC.parent_material_category_id, MC.title, T.path || ARRAY[MC.material_category_id]::bigint[]
FROM
mc_tmp MC
JOIN tree_tmp T ON T.material_category_id = MC.parent_material_category_id
), mc_tmp AS (
SELECT *
FROM template.material_category
WHERE material_category_id IS DISTINCT FROM {MaterialCategoryId} OR {WithChild}
), parent_mc AS (
SELECT *
FROM mc_tmp
WHERE parent_material_category_id IS NULL
)
SELECT
T.material_category_id AS "MaterialCategoryId",
CASE WHEN array_length(T.path, 1) > 1 THEN COALESCE(repeat('—', (array_length(T.path, 1) - 1)), '') || ' ' ELSE '' END || T.title AS "Title"
FROM
tree_tmp T
ORDER BY T.path, T.material_category_id;
</Text>
</SqlQuery>
Этот запрос будем использовать в двух местах: в карточке категории ТМЦ для выбора родительской категории и в карточке ТМЦ для выбора категории товарно-материальной ценности. Поэтому сразу добавим в него параметр {WithChild}
, которым будем разделять место использования запроса: из карточки категории будем передавать false, а из карточки ТМЦ - true.
В запросе стоит обратить внимание на условие IS DISTINCT FROM
, которое для значений не NULL работает так же, как оператор <>
. Однако, если оба сравниваемых значения NULL, результат будет false, и только если одно из значений NULL, возвращается true. Следовательно, если параметр {MaterialCategoryId}
будет иметь значение NULL
, то условие вернет true.
Таким образом, для карточки категории в параметр {MaterialCategoryId}
будем передавать идентификатор редактируемой категории, чтобы исключить ее из списка возможных родительских категорий. Тем самым мы избежим того, что бы в качестве родительской категории была выбрана сама редактируемая категория или ее дочерние категории.
В этом запросе используется вычисление рекурсивного запроса, с помощью указания служебного слова RECURSIVE
. Подробнее про рекурсивные запросы можете почитать по ссылке.
Добавьте запрос ParentMaterialCategorySelectSqlQuery в разрешение MaterialCategoryViewSqlQueryPermission, которое создалось автоматически при выполнении паттерна EntityForm.
Скорректируем запрос MaterialCategoryByIdSelectSqlQuery, чтобы он возвращал идентификатор родительской категории:
<SqlQuery Name="MaterialCategoryByIdSelectSqlQuery">
<Text>
SELECT
MC.title AS "Title",
MC.parent_material_category_id AS "ParentMaterialCategoryId"
FROM
template.material_category MC
WHERE
MC.material_category_id = {MaterialCategoryId};
</Text>
</SqlQuery>
Скорректируйте самостоятельно запросы MaterialCategoryInsertSqlQuery и MaterialCategoryUpdateSqlQuery, чтобы они сохраняли значение ParentMaterialCategoryId, переданное с формы.
Вернемся в файл TemplateMaterialCategoryEdit.xml и скорректируем MaterialCategoryPrimaryGetDataConnection:
<DataConnection Name="MaterialCategoryPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<ManualLoad>
<Not>
<Parameter Name="Edit" />
</Not>
</ManualLoad>
<SqlQuery Name="MaterialCategoryByIdSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="Title" />
<Field Name="ParentMaterialCategoryId" />
</Fields>
<Parameters>
<Parameter NativeName="MaterialCategoryId">
<Value>
<Parameter Name="MaterialCategoryId" />
</Value>
</Parameter>
</Parameters>
</SqlQuery>
</DataConnection>
Обратите внимание, что у MaterialCategoryPrimaryGetDataConnection появился тэг <ManualLoad>
- признак определяет, будет ли загрузка данных происходить вместе с загрузкой формы или только после ручного обновления соединения с данными. Значение тэга обратно значению параметра Edit. Когда форма открыта на редактирование (значение параметра Edit = True), значение тэга <ManualLoad>
будет False - DataConnection будет обновляться вместе с загрузкой формы. А если форма будет открыта на создание, то значение тэга станет True, и DataConnection будет обновляться только по команде. Таким образом, мы можем исключать лишнее обращение к базе данных, если уверены, что там нет нужных данных.
Самостоятельно измените MaterialCategoryInsertSetDataConnection и MaterialCategoryUpdateSetDataConnection, добавив параметр ParentMaterialCategoryId, в который будет передаваться значение из ParentMaterialCategoryComboBox.
Создадим соединения с данными для загрузки списка родительских категорий:
<DataConnection Name="ParentMaterialCategoryPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<SqlQuery Name="ParentMaterialCategorySelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
</Fields>
<Parameters>
<Parameter NativeName="MaterialCategoryId" RefreshQuery="False">
<Value>
<Parameter Name="MaterialCategoryId" />
</Value>
</Parameter>
<Parameter NativeName="WithChild" RefreshQuery="False">
<Value>False</Value>
</Parameter>
</Parameters>
</SqlQuery>
</DataConnection>
Как вы помните, на форме TemplateMaterialList.xml в команде MaterialCategoryAddFormShowCommand на дочернюю форму передавался параметр ParentMaterialCategoryId, давайте создадим этот параметр:
<Parameter Name="ParentMaterialCategoryId" />
Теперь можем доделать ParentMaterialCategoryComboBox, заполнив его тэги <ValueList>
и <Value>
:
<MyObject Name="ParentMaterialCategoryComboBox" Type="ComboBox" Assembly="BaseControls">
<Top>
<Object Name="ParentMaterialCategoryLabel">
<Property Name="Bottom" />
</Object>
</Top>
<Left>
<Object Name="ParentMaterialCategoryLabel">
<Property Name="Left" />
</Object>
</Left>
<Width>
<Object Name="ParentMaterialCategoryLabel">
<Property Name="Width" />
</Object>
</Width>
<TabIndex>1</TabIndex>
<NullValue Show="True" Title="[Не выбрано]" />
<AutoCompleteMode>SmartSuggest</AutoCompleteMode>
<ValueList>
<DataConnection SourceDataConnection="ParentMaterialCategoryPrimaryGetDataConnection">
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
</Fields>
</DataConnection>
</ValueList>
<Value>
<Switch>
<Case>
<When>
<Parameter Name="Edit" />
</When>
<Then>
<DataConnection SourceDataConnection="MaterialCategoryPrimaryGetDataConnection">
<Fields>
<Field Name="ParentMaterialCategoryId" />
</Fields>
</DataConnection>
</Then>
</Case>
<Case>
<Parameter Name="ParentMaterialCategoryId" />
</Case>
</Switch>
</Value>
</MyObject>
Откройте приложение, проверьте загрузку форм и попробуйте создать несколько категорий, одна из которых не будет иметь родительской категории (например, категория "Бумага"), а остальные категории будут ссылаться на нее. Выпадающий список будет иметь вид:

Отлично! Теперь можем продолжить работать с деревом значений на форме списка.
Запрос для DatabaseTree
Перейдем в xml-файл серверной части (Template.xml) и добавим запрос для получения списка категорий (MaterialCategoryListSelectSqlQuery) и запрос для получения взаимосвязей элементов дерева (MaterialCategoryRelationSelectSqlQuery):
<SqlQuery Name="MaterialCategoryListSelectSqlQuery">
<Text>
SELECT
MC.material_category_id AS "MaterialCategoryId",
MC.title AS "Title"
FROM
template.material_category MC
ORDER BY MC.title, MC.material_category_id;
</Text>
</SqlQuery>
<SqlQuery Name="MaterialCategoryRelationSelectSqlQuery">
<Text>
SELECT
material_category_id AS "MaterialCategoryId",
parent_material_category_id AS "ParentMaterialCategoryId"
FROM
template.material_category;
</Text>
</SqlQuery>
Не забудем добавить их в MaterialCategoryViewSqlQueryPermission.
Вернемся в файл формы списка ТМЦ (TemplateMaterialList.xml) и создадим загружающее соединение с данными:
<DataConnection Name="MaterialCategoryPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<SqlQueries>
<SqlQuery Name="MaterialCategoryListSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
</Fields>
</SqlQuery>
<SqlQuery Name="MaterialCategoryRelationSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="ParentMaterialCategoryId" />
</Fields>
</SqlQuery>
</SqlQueries>
</DataConnection>
Сразу создайте команду DataConnectionRefreshCommand для его обновления.
Обязательный тэг <Items>
объекта MaterialCategoryDatabaseTree ожидает соединение с данными с двумя таблицами:
Первая таблица должна описывать линейный список элементов дерева и иметь два поля, соответствующие идентификатору элемента и его отображаемое значение;
Вторая таблица должна описывать взаимосвязи элементов дерева и иметь поля: идентификатор элемента и идентификатор родительского элемента.
Таким образом, в тэге <Items>
пропишем следующее значение:
<DataConnection SourceDataConnection="MaterialCategoryPrimaryGetDataConnection">
<SourceQuery Name="MaterialCategoryListSelectSqlQuery">
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
</Fields>
</SourceQuery>
<SourceQuery Name="MaterialCategoryRelationSelectSqlQuery">
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="ParentMaterialCategoryId" />
</Fields>
</SourceQuery>
</DataConnection>
Откроем форму списка ТМЦ и проверим, что у нас получилось.

Обновление дерева
Создадим команду, в которой через set-проперти SelectedItemId объекта DatabaseTree будем выделять нужный элемент в нашем дереве категорий:
<Command Name="MaterialCategorySelectInTreeValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId">
<Input />
</Property>
</Object>
</Command>
Дополним <Execution>
последовательностями команд:
<Execution>
<ConditionExpression>
<Command Name="MaterialCategoryAddFormShowCommand" Parameter="Updated" />
</ConditionExpression>
<Commands>
<Command Name="MaterialCategoryDataConnectionRefreshCommand" />
<Command Name="MaterialCategorySelectInTreeValueSetCommand">
<Command Name="MaterialCategoryAddFormShowCommand" Parameter="MaterialCategoryId" />
</Command>
<Command Name="UpdatedTrueValueSetCommand" />
</Commands>
</Execution>
<Execution>
<ConditionExpression>
<Command Name="MaterialCategoryEditFormShowCommand" Parameter="Updated" />
</ConditionExpression>
<Commands>
<Command Name="MaterialCategoryDataConnectionRefreshCommand" />
<Command Name="MaterialCategorySelectInTreeValueSetCommand">
<Command Name="MaterialCategoryEditFormShowCommand" Parameter="MaterialCategoryId" />
</Command>
<Command Name="UpdatedTrueValueSetCommand" />
</Commands>
</Execution>
Вызов команды UpdatedTrueValueSetCommand позволит при закрытии формы списка ТМЦ уведомить родительскую форму о наличии изменений.
Запустите приложение и попробуйте добавить пару категорий, чтобы проверить работу <Execution>
.
Удаление категории
Сложность удаления категории ТМЦ заключается в том, что мы не можем удалить категорию, если она содержит подкатегории и/или позиции ТМЦ. Для проверки этих условий создадим функции.
Первая будет на проверку наличия вложенных позиций ТМЦ:
CREATE OR REPLACE FUNCTION template.material_category_has_materials(in_material_category_id bigint)
RETURNS boolean AS
$BODY$
BEGIN
IF (EXISTS (SELECT * FROM template.material WHERE material_category_id = in_material_category_id))
THEN
RETURN TRUE;
END IF;
RETURN
bool_or(template.material_category_has_materials(material_category_id))
FROM template.material_category
WHERE parent_material_category_id = in_material_category_id;
END;
$BODY$
LANGUAGE plpgsql;
Вторая будет на попытку удаления категории с проверкой на наличие подкатегорий и вложенных позиций ТМЦ:
CREATE OR REPLACE FUNCTION template.material_category_try_delete(in_material_category_id bigint)
RETURNS character varying AS
$BODY$
DECLARE
_has_child_categories boolean = FALSE;
_has_child_materials boolean = FALSE;
_result character varying;
BEGIN
IF (SELECT NOT EXISTS(SELECT * FROM template.material_category WHERE material_category_id = in_material_category_id))
THEN
RETURN NULL;
END IF;
IF (SELECT used FROM template.is_used('material_category', 'material_category_id', ARRAY[]::text[], ARRAY[]::text[], in_material_category_id))
THEN
_has_child_categories = EXISTS (SELECT * FROM template.material_category WHERE parent_material_category_id = in_material_category_id);
_has_child_materials = template.material_category_has_materials(in_material_category_id);
_result = 'Выбранная категория ТМЦ ' ||
CASE
WHEN _has_child_categories AND _has_child_materials
THEN 'содержит вложенные категории и позиции ТМЦ.'
WHEN _has_child_categories
THEN 'содержит вложенные категории ТМЦ.'
WHEN _has_child_materials
THEN 'содержит вложенные позиции ТМЦ.'
ELSE 'используется в программе.'
END;
RETURN _result;
ELSE
DELETE FROM template.material_category
WHERE material_category_id = in_material_category_id;
RETURN NULL;
END IF;
END;
$BODY$
LANGUAGE plpgsql;
Если мы не можем удалить категорию с подкатегориями и/или вложенными позициями ТМЦ, то будем возвращать сообщение с причиной невозможности удаления и предлагать пользователю сделать каскадное удаление категорий, подкатегорий и вложенных позиций ТМЦ.
Создадим функцию для каскадного удаления:
CREATE OR REPLACE FUNCTION template.material_category_delete_cascade(in_material_category_id bigint)
RETURNS void AS
$BODY$
BEGIN
IF (SELECT NOT EXISTS(SELECT * FROM template.material_category WHERE material_category_id = in_material_category_id)) THEN
RETURN;
END IF;
PERFORM template.material_try_delete(array_agg(material_id))
FROM template.material
WHERE material_category_id = in_material_category_id;
PERFORM template.material_category_delete_cascade(material_category_id)
FROM template.material_category
WHERE parent_material_category_id = in_material_category_id;
IF (SELECT used FROM template.is_used('material_category', 'material_category_id', ARRAY[]::text[], ARRAY[]::text[], in_material_category_id))
THEN
UPDATE template.material_category
SET archive = TRUE
WHERE material_category_id = in_material_category_id AND NOT archive;
ELSE
DELETE FROM template.material_category
WHERE material_category_id = in_material_category_id;
END IF;
END;
$BODY$
LANGUAGE plpgsql;
Создадим функцию template.material_try_delete(bigint[]):
CREATE OR REPLACE FUNCTION template.material_try_delete(in_material_id_array bigint[])
RETURNS character varying AS
$BODY$
DECLARE
_material_id bigint;
_deleted_count integer := 0;
_archived_count integer := 0;
_archived_array varchar[];
BEGIN
FOR i IN 1..COALESCE(array_length(in_material_id_array, 1), 0)
LOOP
_material_id = in_material_id_array[i];
raise notice '_material_id: %', _material_id;
IF (SELECT used FROM template.is_used('material', 'material_id', ARRAY[]::text[], ARRAY[]::text[], _material_id))
THEN
UPDATE template.material
SET archive = True
WHERE material_id = _material_id AND NOT archive;
_archived_count = _archived_count + 1;
_archived_array = _archived_array || (SELECT title FROM template.material WHERE material_id = _material_id);
ELSE
DELETE FROM template.material
WHERE material_id = _material_id;
_deleted_count = _deleted_count + 1;
END IF;
END LOOP;
RETURN CASE WHEN _archived_count > 0 THEN
'Удалено ТМЦ: ' || _deleted_count ||
E'.\rПеремещено в архив: ' || _archived_count ||
E'.\r\rСписок ТМЦ, перемещенных в архив:\r' || array_to_string(array_sort(_archived_array), E',\r') ELSE NULL END;
END;
$BODY$
LANGUAGE plpgsql;
Функция template.material_try_delete(bigint[]) возвращает сообщение о количестве удаленных и перемещенных в архив записей, а также наименования ТМЦ, отправленных в архив. Но функция template.material_category_delete_cascade(bigint), которая ее вызывает, никак не обрабатывает возвращаемое сообщение и не передает его в результат SQL-запроса, чтобы можно было получить сообщение на форме и отобразить его пользователю. Вы можете самостоятельно реализовать данную функциональность.
В функции template.material_try_delete(bigint[]) используется функция сортировки массива:
CREATE OR REPLACE FUNCTION public.array_sort(anyarray)
RETURNS anyarray AS
$BODY$
SELECT ARRAY(SELECT unnest($1) ORDER BY 1);
$BODY$
LANGUAGE sql;
Перейдем в серверный xml-файл и создадим запросы на удаление категорий:
<SqlQuery Name="MaterialCategoryDeleteSqlQuery">
<Text>
SELECT template.material_category_try_delete({MaterialCategoryId}::bigint);
</Text>
</SqlQuery>
<SqlQuery Name="MaterialCategoryDeleteCascadeSqlQuery">
<Text>
SELECT template.material_category_delete_cascade({MaterialCategoryId}::bigint);
</Text>
</SqlQuery>
Добавьте их в MaterialCategoryEditSqlQueryPermission.
Вернемся на форму списка ТМЦ и категорий (TemplateMaterialList.xml) и создадим SetDataConnection:
<DataConnection Name="MaterialCategoryDeleteSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
<Workflow Name="Template" />
<Parameters>
<Parameter NativeName="MaterialCategoryId">
<Value>
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Value>
</Parameter>
</Parameters>
<SqlQueries>
<SqlQuery Name="MaterialCategoryDeleteSqlQuery" Type="Delete" />
</SqlQueries>
</DataConnection>
<DataConnection Name="MaterialCategoryDeleteCascadeSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
<Workflow Name="Template" />
<Parameters>
<Parameter NativeName="MaterialCategoryId">
<Value>
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Value>
</Parameter>
</Parameters>
<SqlQueries>
<SqlQuery Name="MaterialCategoryDeleteCascadeSqlQuery" Type="Delete" />
</SqlQueries>
<Refresh>
<DataConnection Name="MaterialCategoryPrimaryGetDataConnection" />
<DataConnection Name="MaterialPrimaryGetDataConnection" />
</Refresh>
</DataConnection>
Добавим команды типа SaveCommand:
<Command Name="MaterialCategoryDeleteSaveCommand" Type="SaveCommand" Assembly="Commands">
<DataConnections>
<DataConnection Name="MaterialCategoryDeleteSetDataConnection" />
</DataConnections>
</Command>
<Command Name="MaterialCategoryDeleteCascadeSaveCommand" Type="SaveCommand" Assembly="Commands">
<DataConnections>
<DataConnection Name="MaterialCategoryDeleteCascadeSetDataConnection" />
</DataConnections>
</Command>
Создадим условие IsNullCondition для проверки результата выполнения запроса на удаление категории:
<Condition Name="MaterialCategoryDeleteCommandResultIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
<Items>
<Item>
<Command Name="MaterialCategoryDeleteSaveCommand" />
</Item>
</Items>
</Condition>
И создадим команду для отображения полученного от сервера сообщения:
<Command Name="MaterialCategoryTryDeleteUsedMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Удаление</Caption>
<Text>
<String>
<Format>{0}\rВы можете попытаться удалить выбранную категорию вместе с вложенными элементами. В случае невозможности удаления записи будут отправлены в архив.\rПродолжить?</Format>
<Items>
<Item>
<Command Name="MaterialCategoryDeleteSaveCommand" />
</Item>
</Items>
</String>
</Text>
<Icon Type="Question" />
<Buttons Type="YesNo" />
</Command>
Добавим Execution:
<Execution>
<ConditionExpression>
<Command Name="MaterialCategoryDeleteMessageBoxCommand" Parameter="Yes" />
</ConditionExpression>
<Commands>
<Command Name="MaterialCategoryDeleteSaveCommand" />
<If>
<When>
<Condition Name="MaterialCategoryDeleteCommandResultIsNullCondition" />
</When>
<Then>
<Command Name="MaterialCategoryDataConnectionRefreshCommand" />
<Command Name="UpdatedTrueValueSetCommand" />
</Then>
<Else>
<Command Name="MaterialCategoryTryDeleteUsedMessageBoxCommand" />
</Else>
</If>
</Commands>
</Execution>
<Execution>
<ConditionExpression>
<Command Name="MaterialCategoryTryDeleteUsedMessageBoxCommand" Parameter="Yes" />
</ConditionExpression>
<Commands>
<Command Name="MaterialCategoryDeleteCascadeSaveCommand" />
</Commands>
</Execution>
Паттерн Table
Теперь можем заняться правой частью формы и создать сам список товарно-материальных ценностей.
На панель MaterialPanel добавим таблицу с кнопками редактирования. Для этого воспользуйтесь паттерном Table:

Выполним следующие настройки паттерна:

После применения паттерна перейдем в xml-файл серверной части и перенесем роль MaterialEditRole в группу GuestGroup, а также скорректируем текст запроса:
<SqlQuery Name="MaterialSelectSqlQuery">
<Text>
SELECT
M.material_id AS "MaterialId",
M.material_category_id AS "MaterialCategoryId",
M.title AS "Title",
M.unit_id AS "UnitId",
M.unit_price AS "UnitPrice"
FROM
template.material M
ORDER BY M.title;
</Text>
</SqlQuery>
Таким образом форма списка (TemplateMaterialList.xml) примет вид:

Добавим на форму соединение с данными для загрузки списка единиц измерений:
<DataConnection Name="UnitPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<SqlQuery Name="UnitSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="UnitId" />
<Field Name="ShortTitle" />
</Fields>
</SqlQuery>
</DataConnection>
Добавим в таблицу MaterialDatabaseTable колонки:
<Column Name="UnitId" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
<Visible>False</Visible>
</Column>
<Column Name="UnitShortTitle" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
<Title>Ед. измерения</Title>
<Width>100</Width>
<AutoSizeMode Value="None" />
<Substitution SourceColumn="UnitId">
<DataConnection SourceDataConnection="UnitPrimaryGetDataConnection">
<Fields>
<Field Name="UnitId"/>
<Field Name="ShortTitle" />
</Fields>
</DataConnection>
</Substitution>
</Column>
<Column Name="UnitPrice" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
<Title>Цена продажи</Title>
<Width>100</Width>
<AutoSizeMode Value="None" />
<DataType Type="DecimalDataType" Format="N2" />
<Alignment Value="MiddleRight" />
</Column>
Скорректируем команду MaterialAddFormShowCommand, добавив параметр MaterialCategoryId:
<Command Name="MaterialAddFormShowCommand" Type="FormShowCommand" Assembly="Commands">
<Xml Type="Path">TemplateMaterialEdit.xml</Xml>
<Show Type="None" />
<Parameters>
<Parameter Name="MaterialCategoryId">
<Object Name="MaterialCategoryDatabaseTree">
<Property Name="SelectedItemId" />
</Object>
</Parameter>
</Parameters>
</Command>
Карточка ТМЦ
Первым делом перейдем в серверный xml-файл и скорректируем запросы:
<SqlQuery Name="MaterialByIdSelectSqlQuery">
<Text>
SELECT
M.material_category_id AS "MaterialCategoryId",
M.title AS "Title",
M.unit_id AS "UnitId",
M.unit_price AS "UnitPrice"
FROM
template.material M
WHERE
M.material_id = {MaterialId};
</Text>
</SqlQuery>
<SqlQuery Name="MaterialInsertSqlQuery">
<Text>
INSERT INTO template.material (
title,
material_category_id,
unit_id,
unit_price
)
VALUES (
{Title},
{MaterialCategoryId},
{UnitId},
{UnitPrice}
)
RETURNING material_id;
</Text>
</SqlQuery>
<SqlQuery Name="MaterialUpdateSqlQuery">
<Text>
UPDATE template.material
SET
title = {Title},
material_category_id = {MaterialCategoryId},
unit_id = {UnitId},
unit_price = {UnitPrice}
WHERE
material_id = {MaterialId};
</Text>
</SqlQuery>
Редактор с помощью паттерна Table уже создал нужный файл формы (TemplateMaterialEdit.xml). Перейдем в него, чтобы создать объекты интерфейса.
Добавим в параметры формы параметр MaterialCategoryId.
Скорректируем соединение с данными MaterialPrimaryGetDataConnection, добавив необходимые поля:
<DataConnection Name="MaterialPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<ManualLoad>
<Not>
<Parameter Name="Edit" />
</Not>
</ManualLoad>
<SqlQuery Name="MaterialByIdSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
<Field Name="UnitId" />
<Field Name="UnitPrice" />
</Fields>
<Parameters>
<Parameter NativeName="MaterialId" RefreshQuery="False">
<Value>
<Parameter Name="MaterialId" />
</Value>
</Parameter>
</Parameters>
</SqlQuery>
</DataConnection>
Создадим соединение с данными MaterialCategoryPrimaryGetDataConnection для получения списка категорий:
<DataConnection Name="MaterialCategoryPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<SqlQuery Name="ParentMaterialCategorySelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="MaterialCategoryId" />
<Field Name="Title" />
</Fields>
<Parameters>
<Parameter NativeName="MaterialCategoryId" RefreshQuery="False">
<Value>
<DataConnection SourceDataConnection="MaterialPrimaryGetDataConnection">
<Fields>
<Field Name="MaterialCategoryId" />
</Fields>
</DataConnection>
</Value>
</Parameter>
<Parameter NativeName="WithChild" RefreshQuery="False">
<Value>True</Value>
</Parameter>
</Parameters>
</SqlQuery>
</DataConnection>
В нем используем запрос ParentMaterialCategorySelectSqlQuery, который писали для карточки редактирования категории ТМЦ.
Самостоятельно создайте все необходимые элементы формы, чтобы форма имела вид:

Если форма открыта на редактирование, то в поле "Категория" (MaterialCategoryComboBox) должна отображаться категория редактируемого ТМЦ, иначе должен подставляться идентификатор из параметра MaterialCategoryId.
Для создания выпадающего списка единиц измерений (UnitComboBox) используйте материал прошлого урока, где мы реализовывали логику редактирования выпадающего списка клиентов и режим выбора на форме списка клиентов. Также для UnitComboBox используйте соединение с данными:
<DataConnection Name="UnitPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
<SqlQuery Name="UnitSelectSqlQuery" Type="Select">
<Workflow Name="Template" />
<Fields>
<Field Name="UnitId" />
<Field Name="ShortTitle" />
<Field Name="Archive" />
</Fields>
<Filter Type="Nested">
<Or>
<Filter>
<Field NativeName="Archive" />
<Value>False</Value>
<DataType Type="BooleanDataType" />
</Filter>
<Filter>
<Field NativeName="UnitId" />
<Value>
<DataConnection SourceDataConnection="MaterialPrimaryGetDataConnection">
<Fields>
<Field Name="UnitId" />
</Fields>
</DataConnection>
</Value>
<DataType Type="IntegerDataType" />
</Filter>
</Or>
</Filter>
</SqlQuery>
</DataConnection>
Скорректируйте MaterialInsertSetDataConnection и MaterialUpdateSetDataConnection.
Запустите приложение и попробуйте создать несколько позиций для списка товарно-материальный ценностей с разными категориями.

Как видите, независимо от того, какую категорию выбрали, в списке отображаются все записи.
Реализуйте с помощью SecondaryGetDataConnection фильтрацию данных для таблицы ТМЦ на основе выбранной в дереве DatabaseTree категории.

Самостоятельно
Как вы могли заметить, в таблицах template.material_category и template.material есть колонки archive, которые мы используем в функциях на удаление категорий и ТМЦ.
Ваша задача реализовать на форме списка ТМЦ фильтр архивных и актуальных записей и кнопки для работы с архивом категорий и ТМЦ.
Для этого вам понадобятся функции:
Последовательность запросов для архивации категорий ТМЦ:
<SqlQuery Name="MaterialCategoryArchiveSqlQuery">
<Text>
UPDATE template.material_category
SET archive = {Archive}
WHERE material_category_id = {MaterialCategoryId};
SELECT template.material_category_archive_child({MaterialCategoryId})
WHERE {Archive};
SELECT template.material_category_unarchive_parent({MaterialCategoryId})
WHERE NOT {Archive};
</Text>
</SqlQuery>
Отправив категорию в архив, необходимо отправить в архив все дочерние категории и ТМЦ, принадлежащие выбранной категории и дочерним категориям. Восстановив категорию из архива, необходимо восстановить и все родительские категории. Это позволит корректно отображать дерево категорий на форме.
Последовательность запросов для архивации ТМЦ:
<SqlQuery Name="MaterialArchiveSqlQuery">
<Text>
UPDATE template.material
SET archive = {Archive}
WHERE material_id = {MaterialId};
UPDATE template.material_category MC
SET archive = False
FROM template.material M
WHERE
MC.material_category_id = M.material_category_id AND
material_id = {MaterialId} AND
NOT M.archive;
SELECT template.material_category_unarchive_parent(M.material_category_id)
FROM template.material M
WHERE
M.material_id = {MaterialId} AND
NOT M.archive;
</Text>
</SqlQuery>
При восстановлении ТМЦ из архива необходимо восстановить ее категорию и все родительские категории.
Запросы MaterialCategoryArchiveSqlQuery и MaterialArchiveSqlQuery используйте на соответствующих кнопках на форме списка ТМЦ.
Переделаем запрос на построение списка категорий, чтобы в дереве отображался признак архивной записи:
В соединение с данными MaterialCategoryPrimaryGetDataConnection для списка категорий добавим фильтр архивных записей:
Для дерева MaterialCategoryDatabaseTree используйте вложенный тэг <Sorted>
со значением True, чтобы при изменении фильтра узлы дерева сохраняли сортировку.
Итоги
На уроке мы познакомились с объектом DatabaseTree, который отображает древовидную структуру данных. Также создали пару списков, которые будем использовать в следующем уроке для расширения возможностей карточки заказа.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.
Last updated