Урок 12. Дерево в таблице

В этом уроке мы рассмотрим второй вариант работы с иерархической структурой данных в виде дерева, отображаемого в таблице 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)
);

Обратите внимание на строчку:

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> добавим шрифт для корневых категорий:

TemplateOperationList.xml
<FontStyle Name="TitleBoldFont" Font="Segoe UI" Size="9" Bold="True" />

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

TemplateOperationList.xml
<Color Name="Yellow" Red="249" Green="242" Blue="165" Alpha="255" />
<Color Name="LightYellow" Red="255" Green="255" Blue="213" Alpha="255" />

Заменим в OperationPrimaryGetDataConnection список полей:

TemplateOperationList.xml
<DataConnection Name="OperationPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="OperationSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="Expand" />
      <Field Name="OperationId" />
      <Field Name="OperationCategoryId" />
      <Field Name="Title" />
      <Field Name="System" />
      <Field Name="Archive" />
    </Fields>
  </SqlQuery>
</DataConnection>

Для создания дерева записей в таблице DatabaseTable будем использовать TreeGetDataConnection, который на основе данных из OperationPrimaryGetDataConnection будет строить дерево.

TemplateOperationList.xml
<DataConnection Name="OperationTreeGetDataConnection" Type="TreeGetDataConnection" Assembly="DataConnections">
  <SourceDataConnection Name="OperationPrimaryGetDataConnection">
    <Fields>
      <Field Name="OperationId" />
      <Field Name="Title" />
      <Field Name="Expand" />
      <Field Name="OperationCategoryId" />
      <Field Name="System" />
      <Field Name="Archive" />
    </Fields>
  </SourceDataConnection>
  <RelationshipDataConnection Name="OperationPrimaryGetDataConnection">
    <Fields>
      <Field Name="OperationId" />
      <Field Name="OperationCategoryId" />
    </Fields>
  </RelationshipDataConnection>
  <AdditionalColumns>
    <HasChildrenColumn Name="HasChildren" />
    <StateColumn Name="State" CloseState="+" OpenState="-" />
  </AdditionalColumns>
  <Filter FilterByNullValue="False">
    <Field NativeName="Archive" />
    <Value>
      <Object Name="ArchiveFilterComboBox" />
    </Value>
    <DataType Type="BooleanDataType" />
  </Filter>
</DataConnection>

В тэге <SourceDataConnection> указываем источник данных для дерева. Первых три поля являются обязательными и их порядок строго определен. Первое поле должно соответствовать идентификатору элемента, второе - его отображаемому значению, третье - его состоянию (свернут/развернут узел дерева, если он имеет дочерние элементы).

Помимо обязательных полей можно указывать любое количество дополнительных полей.

В тэге <RelationshipDataConnection> указываем таблицу с двумя колонками: в первой колонке должен быть идентификатор элемента, а во второй - идентификатор родительского элемента. На основе этой таблицы TreeGetDataConnection построит дерево.

В тэге <AdditionalColumns> задаем имена для дополнительных колонок: одно для колонки с признаком наличия дочерних элементов (HasChildren), другое для колонки отображения значения состояния узла (State).

Укажем OperationTreeGetDataConnection в качестве источника данных для таблицы OperationDatabaseTable.

Заменим тэг <Columns> в описании таблицы кодом, в котором описаны все колонки из OperationPrimaryGetDataConnection и дополнительные колонки из OperationTreeGetDataConnection:

<Columns>
  <Column Name="OperationId" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
  </Column>
  <Column Name="OperationCategoryId" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
  </Column>
  <Column Name="State" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Title />
    <Width>30</Width>
    <AutoSizeMode Value="None" />
    <ManagementMode>NotAllowed</ManagementMode>
    <Alignment Value="MiddleCenter" />
    <AllowSort Value="False" />
  </Column>
  <Column Name="Title" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Title>Наименование</Title>
    <MinimumWidth>100</MinimumWidth>
    <AutoSizeMode Value="Fill" />
    <AllowSort Value="False" />
  </Column>
  <Column Name="System" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
  </Column>
  <Column Name="Archive" Type="DatabaseTableColumnCheckBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
    <DataType Type="BooleanDataType" />
  </Column>
  <Column Name="Expand" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
  </Column>
  <Column Name="HasChildren" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
    <Visible>False</Visible>
  </Column>
</Columns>

Добавим в тэг <Formatting> таблицы OperationDatabaseTable условия форматирования строк:

<BackColor Name="LightYellow">
  <Expression>HasChildren AND OperationId <![CDATA[>]]> 0</Expression>
</BackColor>
<BackColor Name="Yellow">
  <Expression>OperationId <![CDATA[<]]> 0</Expression>
</BackColor>
<FontStyle Name="TitleBoldFont">
  <Expression>OperationId <![CDATA[<]]> 0</Expression>
</FontStyle>

Запустим приложение и проверим загрузку формы списка и отображение дерева назначений платежей.

Отлично! Переключимся на форму редактирования назначения платежей, а затем вернемся на форму списка и доделаем дерево.

Карточка сущности

В файле 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).

Создадим соединение с данными для списка доступных категорий:

TemplateOperationEdit.xml
<DataConnection Name="OperationCategoryPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="OperationCategorySelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="OperationId" />
      <Field Name="Title" />
      <Field Name="IsIncome" />
    </Fields>
    <Parameters>
      <Parameter NativeName="OperationId" RefreshQuery="False">
        <Value>
          <Parameter Name="OperationId" />
        </Value>
      </Parameter>
      <Parameter NativeName="OperationCategoryId" RefreshQuery="False">
        <Value>
          <DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
            <Fields>
              <Field Name="OperationCategoryId" />
            </Fields>
          </DataConnection>
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>

Укажем новый OperationCategoryPrimaryGetDataConnection в тэге <ValueList> выпадающего списка поля "Категория", а в качестве значения тэга <Value> того же объекта напишем конструкцию:

<Switch>
  <Case>
    <When>
      <Parameter Name="Edit" />
    </When>
    <Then>
      <DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
        <Fields>
          <Field Name="OperationCategoryId" />
        </Fields>
      </DataConnection>
    </Then>
  </Case>
  <Case>
    <Parameter Name="OperationCategoryId" />
  </Case>
</Switch>

Изменение категории назначения платежей

При редактировании системных назначений платежей или назначений, на которые есть кассовые операции, мы должны запрещать пользователю изменять категорию назначения, если это приведет к смене типа назначения платежей (приход/расход).

Для начала нам понадобятся данные о выбранной категории, для этого создадим SecondaryGetDataConnection:

TemplateOperationEdit.xml
<DataConnection Name="OperationCategorySecondaryGetDataConnection" Type="SecondaryGetDataConnection" Assembly="DataConnections">
  <SourceDataConnection Name="OperationCategoryPrimaryGetDataConnection" />
  <Filter>
    <Field NativeName="OperationId" />
    <Value>
      <Object Name="CategoryComboBox" />
    </Value>
    <DataType Type="IntegerDataType" />
  </Filter>
</DataConnection>

Создадим условие проверки идентификатора выбранной категории, чтобы отсекать служебные записи "ДОХОД" (OperationId = -2) и "РАСХОД" (OperationId = -1):

TemplateOperationEdit.xml
<Condition Name="CategoryIdGreater0Condition" Type="GreaterCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CategoryComboBox" />
    </Item>
    <Item>0</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>

Создадим условие проверки, что редактируемое назначение платежей является системным:

TemplateOperationEdit.xml
<Condition Name="OperationIsSystemEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
        <Fields>
          <Field Name="IsSystem" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>True</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

И условие проверки, что тип редактируемого назначения не совпадает с типом выбранной категории:

TemplateOperationEdit.xml
<Condition Name="OperationTypeAndCategoryTypeNotEqualCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="OperationPrimaryGetDataConnection">
        <Fields>
          <Field Name="IsIncome" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>
      <DataConnection SourceDataConnection="OperationCategorySecondaryGetDataConnection">
        <Fields>
          <Field Name="IsIncome" />
        </Fields>
      </DataConnection>
    </Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Создадим PrimaryGetDataConnection, чтобы всегда иметь свежую информацию об использовании редактируемого назначения платежей в программе:

TemplateOperationEdit.xml
<DataConnection Name="OperationIsUsedPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <ManualLoad>True</ManualLoad>
  <SqlQuery Name="OperationIsUsedSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="IsUsed" />
    </Fields>
    <Parameters>
      <Parameter NativeName="OperationId" RefreshQuery="False">
        <Value>
          <Parameter Name="OperationId" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>
OperationIsUsedSelectSqlQuery
Template.xml
<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:

TemplateOperationEdit.xml
<Condition Name="OperationIsUsedEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="OperationIsUsedPrimaryGetDataConnection">
        <Fields>
          <Field Name="IsUsed" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>True</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Создадим PrimaryGetDataConnection для проверки, является ли выбранное в качестве категории назначение платежей листом, т.е. не имеет дочерних назначений:

TemplateOperationEdit.xml
<DataConnection Name="OperationIsLeafPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <ManualLoad>True</ManualLoad>
  <SqlQuery Name="OperationIsLeafSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="Title" />
      <Field Name="IsLeaf" />
      <Field Name="IsUsed" />
    </Fields>
    <Parameters>
      <Parameter NativeName="OperationId" RefreshQuery="False">
        <Value>
          <Object Name="CategoryComboBox" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>
OperationIsLeafSelectSqlQuery
Template.xml
<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 выбранной категории:

TemplateOperationEdit.xml
<Condition Name="OperationCategoryIsLeafEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="OperationIsLeafPrimaryGetDataConnection">
        <Fields>
          <Field Name="IsLeaf" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>True</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

<Condition Name="OperationCategoryIsUsedEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="OperationIsLeafPrimaryGetDataConnection">
        <Fields>
          <Field Name="IsUsed" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>True</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Создадим команду 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 на:

TemplateOperationEdit.xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands Lock="True">
    <If>
      <When>
        <And>
          <Parameter Name="Edit" />
          <Not>
            <Condition Name="OperationIsSystemEqualCondition" />
          </Not>
          <Condition Name="OperationTypeAndCategoryTypeNotEqualCondition" />
        </And>
      </When>
      <Then>
        <Command Name="OperationIsUsedDataConnectionRefreshCommand" />
      </Then>
    </If>
    <If>
      <When>
        <And>
          <Parameter Name="Edit" />
          <Or>
            <Condition Name="OperationIsSystemEqualCondition" />
            <Condition Name="OperationIsUsedEqualCondition" />
          </Or>
          <Condition Name="OperationTypeAndCategoryTypeNotEqualCondition" />
        </And>
      </When>
      <Then>
        <Command Name="OperationChangedIsIncomeMessageBoxCommand" />
      </Then>
      <Else>
        <If>
          <When>
            <Condition Name="CategoryIdGreater0Condition" />
          </When>
          <Then>
            <Command Name="OperationIsLeafDataConnectionRefreshCommand" />
            <If>
              <When>
                <And>
                  <Condition Name="OperationCategoryIsLeafEqualCondition" />
                  <Condition Name="OperationCategoryIsUsedEqualCondition" />
                </And>
              </When>
              <Then>
                <Command Name="OperationIsLeafMessageBoxCommand" />
              </Then>
            </If>
          </Then>
        </If>
        <If>
          <When>
            <Or>
              <Not>
                <Condition Name="CategoryIdGreater0Condition" />
              </Not>
              <Not>
                <And>
                  <Condition Name="OperationCategoryIsLeafEqualCondition" />
                  <Condition Name="OperationCategoryIsUsedEqualCondition" />
                </And>
              </Not>
              <Command Name="OperationIsLeafMessageBoxCommand" Parameter="Yes" />
            </Or>
          </When>
          <Then>
            <If>
              <When>
                <Parameter Name="Edit" />
              </When>
              <Then>
                <Command Name="OperationUpdateSaveCommand" />
              </Then>
              <Else>
                <Command Name="OperationInsertSaveCommand" />
                <Command Name="OperationIdValueSetCommand" />
              </Else>
            </If>
            <Command Name="UpdatedTrueValueSetCommand" />
            <Command Name="FormCloseCommand" />
          </Then>
        </If>
      </Else>
    </If>
  </Commands>
</Command>

В команде в первым блоке <If> проверяем, не является ли редактируемое назначение платежей системным, и совпадает ли его тип с типом выбранной категории. Если тип не совпадает и назначение не системное, то отправляем запрос на сервер для проверки использования назначения платежа.

Во втором блоке <If> проверяем, является ли редактируемое назначение платежей системным или оно используется в программе и его тип не совпадает с типом выбранной категории. Если условие выполняется, то предупреждаем пользователя о смене типа назначения платежей при изменении его категории. Иначе выполняется большой вложенный блок проверок, в котором сначала проверяется, является ли редактируемое назначение платежей листом и выводится соответствующий вопрос пользователю. Затем идет завершающая проверка на возможность сохранения изменений.

Сохранение назначения платежей

Добавим параметры в OperationInsertSetDataConnection:

TemplateOperationEdit.xml
<DataConnection Name="OperationInsertSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <SqlQueries>
    <SqlQuery Name="OperationInsertSqlQuery" Type="Insert" />
  </SqlQueries>
  <Parameters>
    <Parameter NativeName="OperationCategoryId">
      <Value>
        <Switch>
          <Case>
            <When>
              <Condition Name="CategoryIdGreater0Condition" />
            </When>
            <Then>
              <Object Name="CategoryComboBox" />
            </Then>
          </Case>
        </Switch>
      </Value>
    </Parameter>
    <Parameter NativeName="Title">
      <Value>
        <Object Name="TitleTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="IsIncome">
      <Value>
        <DataConnection SourceDataConnection="OperationCategorySecondaryGetDataConnection">
          <Fields>
            <Field Name="IsIncome" />
          </Fields>
        </DataConnection>
      </Value>
    </Parameter>
    <Parameter NativeName="OperationCategoryIsLeaf">
      <Value>
        <And>
          <Condition Name="CategoryIdGreater0Condition" />
          <Condition Name="OperationCategoryIsLeafEqualCondition" />
        </And>
      </Value>
    </Parameter>
  </Parameters>
</DataConnection>
OperationInsertSqlQuery
Template.xml
<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:

TemplateOperationEdit.xml
<DataConnection Name="OperationUpdateSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <SqlQueries>
    <SqlQuery Name="OperationUpdateSqlQuery" Type="Update" />
  </SqlQueries>
  <Parameters>
    <Parameter NativeName="OperationId">
      <Value>
        <Parameter Name="OperationId" />
      </Value>
    </Parameter>
    <Parameter NativeName="OperationCategoryId">
      <Value>
        <Switch>
          <Case>
            <When>
              <Condition Name="CategoryIdGreater0Condition" />
            </When>
            <Then>
              <Object Name="CategoryComboBox" />
            </Then>
          </Case>
        </Switch>
      </Value>
    </Parameter>
    <Parameter NativeName="Title">
      <Value>
        <Object Name="TitleTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="IsIncome">
      <Value>
        <DataConnection SourceDataConnection="OperationCategorySecondaryGetDataConnection">
          <Fields>
            <Field Name="IsIncome" />
          </Fields>
        </DataConnection>
      </Value>
    </Parameter>
    <Parameter NativeName="OperationCategoryIsLeaf">
      <Value>
        <And>
          <Condition Name="CategoryIdGreater0Condition" />
          <Condition Name="OperationCategoryIsLeafEqualCondition" />
        </And>
      </Value>
    </Parameter>
  </Parameters>
</DataConnection>
OperationUpdateSqlQuery
Template.xml
<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 проверки признака системного назначения платежей:

TemplateOperationList.xml
<Condition Name="SelectedOperationIsNotSystemCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="OperationDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">System</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Item>
    <Item>False</Item>
  </Items>
</Condition>

Скорректируем синтаксис кнопки создания назначения, добавив в тэг <Enabled> условия проверки, является ли выбранная запись системной или архивной:

TemplateOperationList.xml
<MyObject Name="OperationAddButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="OperationDatabaseTable">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 5</Expression>
      <Items>
        <Item>
          <Object Name="OperationDatabaseTable">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Width>40</Width>
  <Height>40</Height>
  <TabIndex>3</TabIndex>
  <Hint>Добавить запись...</Hint>
  <BackgroundImage>Images\24x24\plus.png</BackgroundImage>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>1</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <Enabled>
    <Or>
      <Not>
        <Condition Name="OperationSelectedCondition" />
      </Not>
      <And>
        <Condition Name="SelectedOperationIsNotSystemCondition" />
        <Not>
          <Condition Name="SelectedOperationIsArchiveCondition" />
        </Not>
      </And>
    </Or>
  </Enabled>
  <Commands>
    <Command Name="OperationAddFormShowCommand" />
  </Commands>
  <DisabledMode>True</DisabledMode>
  <DisabledText>
    <Switch>
      <Case>
        <When>
          <Not>
            <Condition Name="SelectedOperationIsNotSystemCondition" />
          </Not>
        </When>
        <Then>Выбранное назначение платежа является системным и не может быть выбрано в качества категории при добавлении назначения.</Then>
      </Case>
      <Case>
        <When>
          <Condition Name="SelectedOperationIsArchiveCondition" />
        </When>
        <Then>Выбранное назначение платежа является архивным и не может быть выбрано в качества категории при добавлении назначения.</Then>
      </Case>
    </Switch>
  </DisabledText>
</MyObject>

Скорректируем команду открытия карточки назначения на добавление записи, будем передавать идентификатор выбранной в таблице записи:

TemplateOperationList.xml
<Command Name="OperationAddFormShowCommand" Type="FormShowCommand" Assembly="Commands">
  <Xml Type="Path">TemplateOperationEdit.xml</Xml>
  <Show Type="None" />
  <Multiple Allow="True" />
  <Parameters>
    <Parameter Name="OperationCategoryId">
      <Object Name="OperationDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">OperationId</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Parameter>
  </Parameters>
</Command>

Отлично! Откройте приложение и создайте несколько назначений платежа.

Редактирование записи

Помимо запрета на редактирование архивных записей, мы должны ограничить редактирование служебных строк "ДОХОД" и "РАСХОД".

Создадим условие, где будем проверять, что идентификатор выбранной записи больше нуля. Служебные строки имеют отрицательные идентификаторы: запись "ДОХОД" имеет идентификатор -2, а запись "РАСХОД" - идентификатор -1.

TemplateOperationList.xml
<Condition Name="SelectedOperationIdGreater0Condition" Type="GreaterCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="OperationDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">OperationId</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Item>
    <Item>0</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>

Скорректируем синтаксис кнопки редактирования назначения платежей, добавив в тэг <Enabled> ограничение для служебных строк:

TemplateOperationList.xml
<MyObject Name="OperationEditButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Calculate>
      <Expression>{0} + 5</Expression>
      <Items>
        <Item>
          <Object Name="OperationAddButton">
            <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>4</TabIndex>
  <Hint>Редактировать запись...</Hint>
  <BackgroundImage>Images\24x24\pencil.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" />
    </And>
  </Enabled>
  <Commands>
    <Command Name="OperationEditFormShowCommand" />
  </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>
    </Switch>
  </DisabledText>
</MyObject>

Перейдите в приложение и попробуйте отредактировать разные записи.

Сворачивание/разворачивание узлов дерева

Чтобы сворачивать/разворачивать узлы дерева, необходимо проверять, по какой ячейке кликнул пользователь и имеет ли выделенная запись дочерние строки.

С помощью условия CellClickCondition будем определять, был ли клик по ячейке таблицы:

TemplateOperationList.xml
<Condition Name="OperationCellClickCondition" Type="CellClickCondition" Assembly="Conditions">
  <Table Name="OperationDatabaseTable" />
</Condition>

Вторым условием будем проверять, что клик был по ячейке столбца State, который отвечает за сворачивание/разворачивание узлов. Для этого воспользуемся get-проперти LastCellClickedColumnName таблицы, чтобы получить имя колонки, по ячейке которой был последний клик:

TemplateOperationList.xml
<Condition Name="OperationLastCellClickedColumnNameEqualStateCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="OperationDatabaseTable">
        <Property Name="LastCellClickedColumnName" />
      </Object>
    </Item>
    <Item>State</Item>
  </Items>
</Condition>

Сворачивать/разворачивать мы можем только те узлы, у которых есть дочерние элементы. Здесь нам поможет get-проперти SelectedRowCellValueByColumnName таблицы, которое по имени колонки будет возвращать значение ячейки в выделенной строке:

TemplateOperationList.xml
<Condition Name="SelectedOperationHasChildrenCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="OperationDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">HasChildren</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Item>
    <Item>True</Item>
  </Items>
</Condition>

Соберем все условия в одно условие типа NestedCondition и сделаем его принудительно событийным через тэг <AlwaysChange>:

TemplateOperationList.xml
<Condition Name="OperationToggleFoldingNodeNestedCondition" Type="NestedCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <ConditionExpression>
    <And>
      <Condition Name="OperationCellClickCondition" />
      <Condition Name="OperationLastCellClickedColumnNameEqualStateCondition" />
      <Condition Name="SelectedOperationHasChildrenCondition" />
    </And>
  </ConditionExpression>
</Condition>

Создайте на это условие Execution, оставив тэг <Commands> пустым.

Для сворачивания/разворачивания узла будем использовать set-проперти ToggleFoldingNode у TreeGetDataConnection, в параметр NodeId которого будем передавать идентификатор выбранной записи в таблице:

TemplateOperationList.xml
<Command Name="OperationToggleFoldingNodeValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="OperationTreeGetDataConnection">
    <Property Name="ToggleFoldingNode">
      <Parameters>
        <Parameter Name="NodeId">
          <Object Name="OperationDatabaseTable">
            <Property Name="SelectedRowCellValueByColumnName">
              <Parameters>
                <Parameter Name="ColumnName">OperationId</Parameter>
              </Parameters>
            </Property>
          </Object>
        </Parameter>
      </Parameters>
    </Property>
  </DataConnection>
</Command>

С помощью этой команды мы будем изменять данные в OperationTreeGetDataConnection. Однако это приведет к изменению источника данных в таблице, что послужит причиной для бесконечного цикла выполнения Execution на условие OperationToggleFoldingNodeNestedCondition. Поэтому мы создадим переменную, которую будем использовать для блокировки последовательности команд сворачивания/разворачивания:

TemplateOperationList.xml
<MyObject Name="BlockRefreshVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>True</Value>
</MyObject>

И команду для изменения значения этой переменной:

TemplateOperationList.xml
<Command Name="BlockRefreshVariableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="BlockRefreshVariable">
    <Input />
  </Object>
</Command>

Создадим команду для обновления дерева OperationTreeGetDataConnection:

TemplateOperationList.xml
<Command Name="OperationTreeDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="OperationTreeGetDataConnection" />
  </DataConnections>
</Command>

Теперь можем заполнить тэг <Commands> у Execution, в котором обрабатываем попытку свернуть/развернуть узел дерева:

TemplateOperationList.xml
<Execution>
  <ConditionExpression>
    <Condition Name="OperationToggleFoldingNodeNestedCondition" />
  </ConditionExpression>
  <Commands>
    <If>
      <When>
        <Object Name="BlockRefreshVariable" />
      </When>
      <Then>
        <Command Name="BlockRefreshVariableValueSetCommand">False</Command>
        <Command Name="OperationToggleFoldingNodeValueSetCommand" />
        <Command Name="OperationTreeDataConnectionRefreshCommand" />
        <Command Name="BlockRefreshVariableValueSetCommand">True</Command>
      </Then>
    </If>
  </Commands>
</Execution>

Запустите приложение и проверьте возможность сворачивать/разворачивать узлы дерева.

Удаление записи

Нельзя удалять служебные записи "ДОХОД" и "РАСХОД" и назначения платежей, которые находятся в архиве, содержат дочерние элементы или являются системными.

Добавим необходимые условия в тэг <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:

TemplateOperationList.xml
<Condition Name="OperationDeleteResultIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Command Name="OperationDeleteSaveCommand" />
    </Item>
  </Items>
</Condition>

<Condition Name="OperationDeleteResultEqualParentCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Command Name="OperationDeleteSaveCommand" />
    </Item>
    <Item>parent</Item>
  </Items>
  <DataType Type="StringDataType" />
</Condition>

<Condition Name="OperationDeleteResultEqualUsedCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Command Name="OperationDeleteSaveCommand" />
    </Item>
    <Item>used</Item>
  </Items>
  <DataType Type="StringDataType" />
</Condition>

Создадим команду MessageBoxCommand вывода сообщения, что выбранное назначение содержит дочерние элементы:

TemplateOperationList.xml
<Command Name="OperationHasChildrenWarningMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Удаление</Caption>
  <Text>Выбранное назначение платежа содержит дочерние элементы и не может быть удалено.</Text>
  <Icon Type="Information" />
  <Buttons Type="Ok" />
</Command>

Переделаем Execution, связанный с удалением записи:

TemplateOperationList.xml
<Execution>
  <ConditionExpression>
    <Command Name="OperationDeleteMessageBoxCommand" Parameter="Yes" />
  </ConditionExpression>
  <Commands>
    <Command Name="OperationDeleteSaveCommand" />
    <If>
      <When>
        <Condition Name="OperationDeleteResultIsNullCondition" />
      </When>
      <Then>
        <Command Name="UpdatedTrueValueSetCommand" />
      </Then>
      <ElseIf>
        <When>
          <Condition Name="OperationDeleteResultEqualParentCondition" />
        </When>
        <Then>
          <Command Name="OperationHasChildrenWarningMessageBoxCommand" />
        </Then>
      </ElseIf>
      <ElseIf>
        <When>
          <Condition Name="OperationDeleteResultEqualUsedCondition" />
        </When>
        <Then>
          <Command Name="OperationTryDeleteUsedMessageBoxCommand" />
          <Command Name="UpdatedTrueValueSetCommand" />
        </Then>
      </ElseIf>
    </If>
  </Commands>
</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:

Template.xml
<SqlQuery Name="OperationArchiveUpdateSqlQuery">
  <Text>
    SELECT template.operation_try_archive({OperationId}::bigint, {Archive}::boolean);
  </Text>
</SqlQuery>
template.operation_try_archive(bigint, boolean)
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) в таблицу добавьте колонки:

<Column Name="AccountType" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
  <Visible>False</Visible>
</Column>
<Column Name="AccountTypeText" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
  <Title>Тип счета</Title>
  <Width>100</Width>
  <AutoSizeMode Value="None" />
  <Substitution SourceColumn="AccountType">
    <Structure Type="Table">
      <Row>
        <Item>True</Item>
        <Item>Наличный</Item>
      </Row>
      <Row>
        <Item>False</Item>
        <Item>Безналичный</Item>
      </Row>
    </Structure>
  </Substitution>
</Column>

Обратите внимание, что в качестве таблицы для подстановки значения в тэге <Substitution> используем структуру <Structure Type="Table">.

Не забудьте создать функцию template.account_try_delete(smallint).

Карточка сущности

Перейдем в файл карточки счета (TemplateAccountEdit.xml) и добавим поле "Тип счета":

TemplateAccountEdit.xml
<MyObject Name="AccountTypeLabel" Type="Label" Assembly="BaseControls">
  <Top>
    <Calculate>
      <Expression>{0} + 5</Expression>
      <Items>
        <Item>
          <Object Name="TitleTextBox">
            <Property Name="Bottom" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Top>
  <Left>
    <Object Name="TitleLabel">
      <Property Name="Left" />
    </Object>
  </Left>
  <Width>
    <Object Name="TitleLabel">
      <Property Name="Width" />
    </Object>
  </Width>
  <Height>20</Height>
  <TextAlign>MiddleLeft</TextAlign>
  <FontStyle>LabelFontStyle</FontStyle>
  <ForeColor>LabelForeColor</ForeColor>
  <Text>Тип счета</Text>
</MyObject>

<MyObject Name="AccountTypeComboBox" Type="ComboBox" Assembly="BaseControls">
  <Top>
    <Object Name="AccountTypeLabel">
      <Property Name="Bottom" />
    </Object>
  </Top>
  <Left>
    <Object Name="TitleTextBox">
      <Property Name="Left" />
    </Object>
  </Left>
  <Width>
    <Object Name="TitleTextBox">
      <Property Name="Width" />
    </Object>
  </Width>
  <TabIndex>2</TabIndex>
  <NullValue Show="True" Title="[Не выбрано]" />
  <AutoCompleteMode>SmartSuggest</AutoCompleteMode>
  <ValueList>
    <Structure Type="Table">
      <Row>
        <Item>True</Item>
        <Item>Наличный</Item>
      </Row>
      <Row>
        <Item>False</Item>
        <Item>Безналичный</Item>
      </Row>
    </Structure>
  </ValueList>
  <Value>
    <DataConnection SourceDataConnection="AccountPrimaryGetDataConnection">
      <Fields>
        <Field Name="AccountType" />
      </Fields>
    </DataConnection>
  </Value>
</MyObject>

Для задания значений выпадающего списка так же используем структуру <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:

TemplateOrderEdit.xml
<MyObject Name="TotalPaidSumVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Object Name="OrderPaymentDatabaseTable">
      <Property Name="ColumnSum">
        <Parameters>
          <Parameter Name="ColumnName">Summ</Parameter>
        </Parameters>
      </Property>
    </Object>
  </Value>
</MyObject>

А для объекта TotalDebtTextBox будем использовать переменную TotalDebtVariable:

TemplateOrderEdit.xml
<MyObject Name="TotalDebtVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Calculate>
      <Expression>{0} - {1}</Expression>
      <Items>
        <Item>
          <Object Name="TotalSumToPayVariable" />
        </Item>
        <Item>
          <Object Name="TotalPaidSumVariable" />
        </Item>
      </Items>
    </Calculate>
  </Value>
</MyObject>

Карточка оплаты

Создайте в файле карточки оплаты (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-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

Last updated