> For the complete documentation index, see [llms.txt](https://wfsys.gitbook.io/wt-practice/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wfsys.gitbook.io/wt-practice/main/lesson_self-work_cash.md).

# Урок 13. Самостоятельная

В этом уроке вы самостоятельно реализуете модуль кассы для проведения финансовых операций, добавите отчет по бюджету с детализацией и настроите выгрузку отчета в Excel документ.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](/wt-practice/main/lesson_tree_in_the_table.md#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](/wt-practice/main/lesson_list_form.md#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Касса&#x20;

Форма кассы должна открываться с главной формы через пункт меню **Финансы -> Касса**.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjEn0BA13tx2OiVOkqkiY%2F01.png?alt=media&amp;token=45877275-b31e-4cef-b43d-8e255c055d30" alt=""><figcaption></figcaption></figure>

### Форма кассы

Создайте форму кассы с глобальным фильтром по дате операции. По умолчанию должны отображаться операции за текущий месяц.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FbzGxvJz4dGyIfpvqzW0c%2Fimage.png?alt=media&amp;token=b0a41432-1f16-4ea8-9297-271b81557ecd" alt=""><figcaption></figcaption></figure>

В запросе для кассы для параметров `{DateStart}` и `{DateFinish}` используйте функцию *convert\_date\_filter()*, которая приводит дату фильтра к дате со временем в часовой зоне сервера.&#x20;

#### Таблица операций

Таблица кассовых операций должна содержать колонки:&#x20;

* RowNumber (№) - порядковый номер строки;
* CashId - идентификатор кассовой операции из таблицы template.cash;
* OrderId - идентификатор заказа из таблицы template.order. Актуально только для операций "Оплата заказа";
* Date (Дата операции) - дата в формате "dd.MM.yyyy HH:mm";
* AccountTitle (Счет) - наименование счета;
* OperationTitle (Назначение платежа) - наименование назначения платежа;
* Income (Приход) - сумма кассовой операции или оплаты заказа, если тип назначение платежа "Доход";
* Expense (Расход) - сумма кассовой операции, если тип назначение платежа "Расход";
* ClientTitle (Клиент) - имя клиента.

#### Редактирование таблицы кассовых операций

Операции системного назначения "Оплата заказа" можно *создавать, редактировать и удалять только* из карточки заказа.

При попытке открыть для редактирование операцию "Оплата заказа" должна открываться карточка заказа, в котором эта оплата была создана. При сохранении изменений в заказе и его оплатах данные на форме кассы должны обновляться. Так же необходимо прокидывать уведомление об изменении заказов на главную форму, чтобы обновлять список заказов.

Остальные операции редактируются в карточке кассовой операции.

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

Удаление кассовой операции делать через поля deleted и date\_deleted - это позволит не терять данные.

{% hint style="info" %}
Чтобы текст на кнопках не наезжал на иконку, добавьте в начало текста несколько пробелов.&#x20;

Для иконки используйте тэг [`<Image>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/button#image), а для выравнивания - [`<ImageAlign>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/button#image_align).
{% endhint %}

#### Результирующие поля

Под таблицей создайте результирующие поля (TextBox), которые будут привязаны к столбцам **Income** (Приход) и **Expense** (Расход).

Для задания левой координаты объекта TextBox используйте get-проперти [ColumnLeft](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_column_left), которое вернет левую координату столбца таблицы относительно контейнера, в котором располагается таблица.

{% hint style="info" %}
Если таблица имеет горизонтальную полосу прокрутки, то следует использовать  get-проперти [ColumnOffset](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_column_offset), которое вернет левую координату столбца таблицы относительно контейнера, в котором располагается таблица, с учетом сдвига полосы прокрутки по горизонтали.&#x20;
{% endhint %}

Для задания ширины объекта TextBox используйте get-проперти [ColumnWidth](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_column_width), которое вернет ширину определенного столбца.

Высоту объектов TextBox задайте равной 35 и переведите их в многострочный режим, указав тэг [`<Multiline>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/textbox#multiline) со значением True. Если это не сделать, высота объекта TextBox останется стандартной.

Текст необходимо выровнять по правому краю.

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

<details>

<summary>Пример тэга &#x3C;Text> для результирующего поля</summary>

```xml
<Text>
  <String>
	<Format>{0} /
{1}</Format>
	<Items>
	  <Item>
		<DataTypeFormat Type="DecimalDataType" Format="N2">
		  <Object Name="CashDatabaseTable">
			<Property Name="SelectedCellsSumByColumnName">
			  <Parameters>
				<Parameter Name="ColumnName">Income</Parameter>
			  </Parameters>
			</Property>
		  </Object>
		</DataTypeFormat>
	  </Item>
	  <Item>
		<DataTypeFormat Type="DecimalDataType" Format="N2">
		  <Object Name="CashDatabaseTable">
			<Property Name="ColumnSum">
			  <Parameters>
				<Parameter Name="ColumnName">Income</Parameter>
			  </Parameters>
			</Property>
		  </Object>
		</DataTypeFormat>
	  </Item>
	</Items>
  </String>
</Text>
```

</details>

Для таблицы задайте свойство [MultiSelect](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#multi_select) со значением **True** для атрибута `Value`. Это позволит пользователю выделять несколько строк.

{% hint style="info" %}
Если пользователи могут менять видимость колонок в таблице DatabaseTable (данная возможность будет рассматриваться в уроке [Пользовательские настройки](/wt-practice/multiplayer_mode/lesson_user_settings.md#vidimost-poryadok-i-shirina-kolonok)), то видимость результирующих полей следует привязать к видимости соответствующих колонок через get-проперти [ColumnVisible](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_column_visible).
{% endhint %}

#### Выгрузка данных в Excel

Для кнопки "Экспорт в Excel..." используйте картинку из архива и команду типа [ExportToExcelCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/export_to_excel_command).

{% file src="/files/QNugA1ooO95tGr2qXnQo" %}
Иконка для кнопки
{% endfile %}

Создайте xlsx-файл шаблона для экспорта в Excel кассы за выбранный период. При выгрузке данных у вас должен получаться документ вида:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FbQSMKoWboXYBrUFrCluc%2Fimage.png?alt=media\&token=1f41b8b6-bdea-4b96-bc3b-218cc7d6a3de)

### Карточка кассовой операции

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FOlA8FXvkwTGIS0RFwqRD%2Fimage.png?alt=media&amp;token=d172f07c-ca51-4da8-a021-e147582cabd2" alt=""><figcaption></figcaption></figure>

Для поля "Назначение платежа" (OperationComboBox) используйте SQL-запрос:

{% code title="Template.xml" %}

```xml
<SqlQuery Name="OperationCashSelectSqlQuery">
  <Text>
    SELECT
      operation_id AS "OperationId",
      title AS "Title",
      archive AS "Archive"
    FROM template.operation O
    WHERE
      id_title IS NULL AND
      NOT EXISTS (SELECT * FROM template.operation O2 WHERE O2.operation_category_id = O.operation_id)
    ORDER BY title, operation_id ASC;
  </Text>
</SqlQuery>
```

{% endcode %}

Запрос возвращает назначения платежей, которые не являются системными и не имеют дочерних назначений.

Дополнительно можете перевести форму списка назначений платежа в режим выбора. При этом для кнопки "Выбрать" на форме списка необходимо реализовать проверку, что выделенное назначение не имеет дочерних элементов и не является служебным или системным.

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

## Отчет по бюджету

Форма отчета по бюджету должна открываться с главной формы (TemplateStart.xml) через пункт меню **Отчеты-> Отчет по бюджету**.

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FYpBlcBj4CGVwLK4h74Fr%2F02.png?alt=media\&token=b1091e8f-2753-4992-a5ee-6dfe66b4ccde)

### Основной отчет

Отчет должен быть выполнен в виде таблицы с представлением данных в виде дерева. Реализуйте фильтр по дате кассовой операции, в котором по умолчанию должен стоять текущий месяц.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FAcNXM1aeuIVE3eTEc39d%2Fimage.png?alt=media&amp;token=08e2c301-9edb-4a50-835d-132a9226b3dc" alt=""><figcaption></figcaption></figure>

Для получения данных для отчета используйте SQL-запрос, приведенный ниже.

<details>

<summary>BudgetReportSelectSqlQuery</summary>

{% code title="Template.xml" %}

```xml
<SqlQuery Name="BudgetReportSelectSqlQuery">
  <Text>
    WITH RECURSIVE _operation_tree (operation_id, operation_category_id, title, income, archive, path, child_array) AS (
      SELECT -2::integer, NULL::bigint, 'ДОХОД', True, False, ARRAY[-2]::integer[], template.operation_get_child(null, true)

      UNION

      SELECT -1::integer, NULL::bigint, 'РАСХОД', False, False, ARRAY[-1]::integer[], template.operation_get_child(null, false)

      UNION

      SELECT
        O.operation_id,
        T.operation_id,
        O.title,
        O.income,
        O.archive,
        T.path || O.operation_id,
        template.operation_get_child(O.operation_id)
      FROM
        template.operation O, _operation_tree T
      WHERE
        COALESCE(O.operation_category_id, CASE WHEN O.income THEN -2 ELSE -1 END) = T.operation_id
    ), _cash AS (
      SELECT
        Oper.operation_id,
        Oper.income,
        SUM(summ) AS summ
      FROM
        template.cash C
        JOIN template.operation Oper USING(operation_id)
        LEFT JOIN template.order_payment OP USING(cash_id)
        LEFT JOIN template.order O USING(order_id)
      WHERE
        NOT C.deleted AND
        NOT COALESCE(O.deleted, false) AND
        C.cash_date BETWEEN convert_date_filter({DateStart}::timestamp) AND convert_date_filter({DateFinish}::timestamp) + interval '1d - 1s'
      GROUP BY Oper.operation_id
    ), report_tmp AS (
      SELECT
        O.operation_id,
        O.operation_category_id,
        O.title AS operation_title,
        O.income,
        O.archive,
        O.path,
        COALESCE(C.summ, 0) summ
      FROM
        _operation_tree O
        LEFT JOIN LATERAL
        (
          SELECT SUM(summ) AS summ
          FROM _cash C
          WHERE C.operation_id = ANY(O.child_array)
        ) AS C ON True
    )
    SELECT
      true AS "Expand",
      operation_id AS "OperationId",
      operation_category_id AS "OperationCategoryId",
      operation_title AS "OperationTitle",
      summ AS "Summ",
      operation_id IN (-1, -2) AS "Total",
      income AS "Income",
      false AS "Profit"
    FROM report_tmp
    WHERE
      NOT archive OR
      summ != 0

    UNION ALL

    SELECT
      true AS "Expand",
      -3 AS "OperationId",
      NULL AS "OperationCategoryId",
      'Прибыль' AS "OperationTitle",
      SUM(summ * CASE WHEN income THEN 1 ELSE -1 END) AS "Summ",
      false AS "Total",
      NULL AS "Income",
      true AS "Profit"
    FROM report_tmp
    WHERE operation_id IN (-1, -2);
  </Text>
</SqlQuery>
```

{% endcode %}

Обратите внимание, что для параметров `{DateStart}` и `{DateFinish}` используется функция *convert\_date\_filter()*, которая приводит дату фильтра к дате со временем в часовой зоне сервера.

В запросе используется функция template.operation\_get\_child(), которая возвращает массив идентификаторов всех вложенных дочерних элементов выбранного назначения платежа:

```sql
CREATE OR REPLACE FUNCTION template.operation_get_child(
    in_operation_id bigint,
    in_income boolean DEFAULT true)
  RETURNS bigint[] AS
$BODY$
DECLARE
  _child_operation_id_array bigint[];
  _child_operation_id bigint;
  _child_operation_record record;
  _result bigint[];
BEGIN
  IF (in_operation_id IS NOT NULL)
  THEN
    _child_operation_id_array = (SELECT array_agg(operation_id ORDER BY operation_id) FROM template.operation WHERE operation_category_id = in_operation_id);

    IF (_child_operation_id_array IS NULL)
    THEN
      _result = ARRAY[in_operation_id];
    ELSE
      FOREACH _child_operation_id IN ARRAY _child_operation_id_array
        LOOP
          _result = _result || (SELECT template.operation_get_child(_child_operation_id));
        END LOOP;
    END IF;
  ELSE
    FOR _child_operation_record IN (SELECT operation_id FROM template.operation WHERE (income = in_income OR in_income IS NULL) AND operation_category_id IS NULL ORDER BY operation_id)
    LOOP
      _result = _result || (SELECT template.operation_get_child(_child_operation_record.operation_id));
    END LOOP;
  END IF;

  RETURN _result;
END;
$BODY$
  LANGUAGE plpgsql;
```

</details>

Под таблицей отсутствуют результирующие поля, так как в самой таблице есть строка "Прибыль", которая считает итоговое значение.

{% hint style="info" %}
Для каждого отчета все необходимые запросы лучше группировать в отдельный SqlQueryPermission, для которого создавать отдельную Role. Это позволит в будущем гибче настраивать права доступа к отдельным отчетам.
{% endhint %}

Создайте xlsx-файл шаблона для экспорта в Excel отчета за выбранный период. При выгрузке данных у вас должен получаться документ вида:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FwcRfGIiVVh0oyeD2XiQL%2Fimage.png?alt=media\&token=77bfcbdd-749a-4e1f-9103-ed53b6f13293)

В шаблоне должно быть три блока: "ДОХОД", "РАСХОД" и "Прибыль". Для получения нужных строк из таблицы DatabaseTable используйте get-проперти [FilteredColumnValues](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_filtered_column_values).

### Детализация отчета

Каждый отчет должен иметь форму детализации. На ней пользователь может проверить, как формируется числовое значение позиции основного отчета.

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

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjeEkoU4H4SsufIjlPb50%2Fimage.png?alt=media&amp;token=0f78bce3-fec6-4d5b-a5a4-28559f438ece" alt=""><figcaption></figcaption></figure>

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

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

Для получения данных для отчета используйте SQL-запрос, приведенный ниже.

<details>

<summary>BudgetReportDetailSelectSqlQuery</summary>

{% code title="Template.xml" %}

```xml
<SqlQuery Name="BudgetReportDetailSelectSqlQuery">
  <Text>
    WITH operation_tmp AS (
      SELECT template.operation_get_child({OperationId}, {Income}) AS operation_id_array
    )
    SELECT
      C.cash_id AS "CashId",
      O.order_id AS "OrderId",
      C.cash_date AS "Date",
      A.title AS "AccountTitle",
      Oper.title AS "OperationTitle",
      C.summ * (CASE WHEN NOT Oper.income AND {OperationId} IS NULL AND {Income} IS NULL THEN -1 ELSE 1 END)AS "Summ",
      Cl.title AS "ClientTitle"
    FROM
      operation_tmp, template.cash C
      JOIN template.account A USING(account_id)
      JOIN template.operation Oper USING(operation_id)
      LEFT JOIN template.order_payment OP USING(cash_id)
      LEFT JOIN template.order O USING(order_id)
      LEFT JOIN template.client Cl USING(client_id)
    WHERE
      NOT C.deleted AND
      NOT COALESCE(O.deleted, false) AND
      C.cash_date BETWEEN convert_date_filter({DateStart}::timestamp) AND convert_date_filter({DateFinish}::timestamp) + interval '1d - 1s' AND
      C.operation_id = ANY(operation_id_array)
    ORDER BY C.cash_date, C.cash_id;
  </Text>
</SqlQuery>
```

{% endcode %}

Обратите внимание, что для параметров `{DateStart}` и `{DateFinish}` используется функция *convert\_date\_filter()*, которая приводит дату фильтра к дате со временем в часовой зоне сервера.

</details>

Из формы детализации отчета пользователь должен попадать в карточку кассовой операции или на форму заказа, если выбрана "Оплата заказа". Это позволит узнать детали операции или скорректировать ее данные. Если в заказе были сохранены изменения, то необходимо обновлять данные в детализации отчета, а так же прокидывать уведомление об изменения на форму основного отчета и на главную форму, чтобы обновлять данные на этих формах.

Создайте xlsx-файл шаблона для экспорта в Excel детализации отчета за выбранный период. При выгрузке данных у вас должен получаться документ вида:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FncuuHpANxURT4DQlZuWb%2Fimage.png?alt=media\&token=8fec3260-a167-4a6d-aac3-9213e1eab1b0)

Отлично! Теперь у нас в проекте есть финансовый блок.

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, а также бэкап базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson13-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson13-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson13-answer.zip</a></td></tr></tbody></table>
