Документы содержат информацию о произошедших событиях, на основании которых генерируются движения – изменения итогов. Аналогично справочникам система для каждого типа документа генерирует класс для хранения одного документа.

Тип документов задает:

набор полей в шапке;

набор табличных частей;

набор подтипов документа.

Документ состоит из:

общей шапки – набора полей, который присутствует во всех типах документов;

шапки – набора полей, который есть только в документе данного типа;

набора табличных частей – принадлежащего документу массива данных тех же типов, что и для справочников.

Сами по себе табличные части – отдельные объекты и описываются также отдельно.

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

Потребность в тех или иных типах документов определяет бизнес-логика. Это могут быть, например, документы прихода, кассовые документы, инвентаризации и т.п.

Событие, информацию о котором хранит документ, может быть растянуто во времени, например, продажа проходит через этапы заказа, набора на складе, оплаты и выдачи. Если информация о промежуточных этапах не обладает безусловной ценностью, можно несколько пожертвовать ее доступностью (информация останется, но не в столь явном виде) в пользу удобства пользователя или разработчика и представить этапы как подтипы документа. Аналогия подтипа документа – состояние. Для каждого типа документа разработчик задает возможные подтипы. Кроме описания предметной области и отображения текущего состояния администратор системы распределяет права на отдельные подтипы системы, например, он может выдать права на выполнение всех операций в одном подтипе и только на чтение в остальных подтипах.

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

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

35_example

Рассмотрим на примере взаимосвязь документов, итогов и работу обработчика проведения.

Для начала купим товар "1" у поставщика "1" на склад "1".

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

RegistersDoc1

В этом процессе буду задействованы 3 справочника:

RegistersTab1

При сохранении документа "1" вызывается обработчик проведения и генерирует движение "504". На основании этого движения ядро вносит следующие изменения в итоги:

в итог "остатки на складе (движения)" добавляется строка, увеличивающая остаток товара "1" на складе "1" на количество и сумму прихода;

в итог "долг контрагентов" добавляется строка, уменьшающая долг поставщика "1" на сумму прихода.

Registers1

Для итога "остатки на складе (движения)" в приведенном примере поля документ, товар и склад являются измерениями и ссылаются на соответствующие таблицы, а поля количество и сумма – переменными.

После покупки продадим этот же товар.

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

RegistersDoc2

На этом этапе в дополнение к предыдущим в процесс включается еще один справочник:

RegistersTab2

При сохранении документа "3" вызывается обработчик проведения и генерирует движения "505" и "506". На основании этих движений ядро вносит следующие две пары изменений в итоги:

в итог "остатки на складе (движения)" добавляется строка, уменьшающая остаток товара "1" на складе "1" на количество расхода и сумму, соответствующую себестоимости предыдущего прихода;

в итог "продажи" добавляется строка, увеличивающая количество продаваемого товара "1" клиенту "2" на сумму себестоимости продажи;

в итог "продажи" добавляется строка, уменьшающая количество продаваемого товара "1" клиенту "2" на сумму продажи (по отпускной цене), таким образом за счет разницы цен у нас образуется выручка;

в итог "долг контрагентов" добавляется строка, увеличивающая долг клиента "2" на сумму продажи.

При сохранении документа "2" вызывается обработчик проведения и генерирует движение "507". На основании этого движения ядро вносит следующие изменения в итоги:

в итог "долг контрагентов" добавляется строка, уменьшающая долг клиента "2" на сумму оплаты;

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

Registers2

В реальности из-за разницы закупочных цен на товары себестоимость вычисляется не так однозначно, как показано в этом примере.

Рассмотрим как раз такой случай. Купим товар "1" у другого поставщика "3" по цене, отличающейся от предыдущей (документ "4"), а затем продадим его еще раз клиенту "4" (документы "5" и "6"):

RegistersDoc3

RegistersTab3

При сохранении документа "4" вызывается обработчик проведения и генерирует движение "508". На основании этого движения ядро вносит следующие изменения в итоги:

в итог "остатки на складе (движения)" добавляется строка, увеличивающая остаток товара "1" на складе "1" на количество и сумму прихода;

в итог "долг контрагентов" добавляется строка, уменьшающая долг поставщика "3" на сумму прихода.

При сохранении документа "6" вызывается обработчик проведения и генерирует движения "509" и "510". На основании этих движений ядро вносит следующие две пары изменений в итоги:

в итог "остатки на складе (движения)" добавляется строка, уменьшающая остаток товара "1" на складе "1" на количество расхода, сумма этого списания, будучи себестоимостью, подлежит вычислению, потому движение создается с пустым полем;

в итог "продажи" добавляется строка, увеличивающая количество продаваемого товара "1" клиенту "4", поле сумма опять, будучи себестоимостью, остается пустой;

в итог "продажи" добавляется строка, уменьшающая количество продаваемого товара "1" клиенту "4" на сумму продажи (по отпускной цене);

в итог "долг контрагентов" добавляется строка, увеличивающая долг клиента "4" на сумму продажи.

При сохранении документа "5" вызывается обработчик проведения и генерирует движение "511". На основании этого движения ядро вносит следующие изменения в итоги:

в итог "долг контрагентов" добавляется строка, уменьшающая долг клиента "4" на сумму оплаты;

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

Registers3

Незаполненные при записи движений переменные итогов, являются аналитическими. В случае итога "остатки на складе (движения)" – это переменная сумма. В случае покупки сумма нам известна, потому она в составе проводки попадает в итог, но при расходе она требует вычисления, поэтому поле остается незаполненным. Переменная же количество оперативная, нам всегда однозначно известно какое количество товара мы приходуем, списываем, перемещаем или продаем.

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

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

Различить таблицы можно по префиксам:

TB_tablename оперативная сводная таблица, в которой для каждого набора измерений хранится только один набор сводных значений переменных. В приведенном примере это таблица "остатки на складе (сводные)", где для каждого товара на складе хранится только текущий остаток и его стоимость;

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

TD_tablename аналитическая детализированная таблица, в которой хранятся те же движения, что и в TR_tablename, только уже рассчитанные. Заполняется по результату расчета итогов. В приведенном примере это таблица "остатки на складе (движения аналитические)";

TT_tablename аналитическая сводная таблица, в которой для каждого набора измерений хранится только один набор сводных значений переменных из таблицы TD_tablename. В приведенном примере это таблица "остатки на складе (сводные аналитические)".

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

Registers4

Сам расчет себестоимости производится обработчиком – драйвером итога. В системе реализован метод подсчета себестоимости по FIFO, в соответствии с которым драйвер итога и рассчитывает себестоимость продажи, делая две записи в таблице "остатки на складе (движения аналитические)". Прикладной разработчик имеет возможность реализовать расчет иным методом, например, по LIFO или среднему, написав свой драйвер итога, хотя подобное действие будет не более, чем данью доисторическим временам управленческого учета, когда правильный расчет себестоимости FIFO был невозможен по причине невероятной трудоемкости.

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

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