Как могли бы выглядеть регистры в 1С при наличии ООП

в 7:02, , рубрики: 1c, ERP-системы, lsFusion, Анализ и проектирование систем, Блог компании lsFusion, ооп, Программирование
image

В 1С одним из ключевых элементов системы являются регистры. Этот термин имеет свой аналог в английском языке — ledger. Он первоначально появился в бухгалтерской практике, но со временем его логика начала использоваться и в других сферах.

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

Регистр — это набор записей, каждая из которых отражает некоторое изменение состояния для некоторого множества субъектов (или измерений).

В 1С различают 4 вида регистров:

  1. Регистр бухгалтерии
  2. Регистр расчета
  3. Регистр накоплений
  4. Регистр сведений

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

Регистры накоплений

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

Для этого объявим абстрактный класс SkuLedger:

CLASS ABSTRACT SkuLedger 'Регистр изменения остатка товара';

Формально, один экземпляр которого будет отражать единичное изменение остатка по заданному товару и заданному складу на определенное количество (положительное или отрицательное).

Зададим у него измерения как абстрактные свойства типов Sku (товар) и Stock (склад) соответственно. Их нужно будет реализовать при наследовании конкретных классов от класса регистра:

sku 'SKU' = ABSTRACT Sku (SkuLedger);
stock 'Склад' = ABSTRACT Stock (SkuLedger);

Добавим свойство, которое будет обозначать время изменения остатка:

dateTime 'Дата/время' = ABSTRACT DATETIME (SkuLedger);

И, наконец, добавим то, что называется в 1С ресурсом. А именно количество, на которое изменяется остаток данной записью регистра.

quantity 'Кол-во' = ABSTRACT NUMERIC[14,2] (SkuLedger);

Теперь посчитаем текущий остаток на основе записей в регистре:

balance (Stock st, Sku sk) 'Остаток' = GROUP SUM quantity(SkuLedger l) IF stock(l) = st AND sku(l) = sk;

По умолчанию, в базе данных не будет храниться текущий остаток. При обращении к нему будет формироваться запрос, который рассчитает его исходя из текущего состояния регистра. Чтобы оно хранилось постоянно и автоматически обновлялось при изменении регистра, нужно в конце его объявления добавить ключевое слово MATERIALIZED. Наличием этого флага, по сути, и определяется будет это регистр накопления остатков или оборотов в терминологии 1С.

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

CONSTRAINT balance(Stock st, Sku sk) > 0 
    MESSAGE 'Остаток по товару на складе должен быть положительным';

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

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

balance (Stock st, Sku sk, INTEGER year) = 
    GROUP SUM quantity(SkuLedger l) IF stock(l) = st AND sku(l) = sk AND extractYear(dateTime(l)) = year MATERIALIZED;

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

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

balance 'Остаток на время' (Stock st, Sku sk, DATETIME dt) = 
    GROUP SUM quantity(SkuLedger l) IF stock(l) = st AND sku(l) = sk AND dateTime(l) <= dt;

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

balance 'Остаток на время' (Stock st, Sku sk, DATETIME dt) =
   currentBalance(sk, st) (-) (GROUP SUM quantity(SkuLedger l) IF stock(l) = st AND sku(l) = sk AND dateTime(l) > dt);

Естественно, это будет эффективно выполняться, только если по свойству dateTime построен индекс:

INDEX dateTime(SkuLedger l);

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

Теперь покажем, как проводить по регистру документы. Предположим у нас объявлен документ поступления товаров на склад:

CLASS Receipt 'Поступление на склад';
dateTime 'Дата/время' = DATA DATETIME (Receipt);
stock 'Склад' = DATA Stock (Receipt);

CLASS ReceiptDetail 'Строка поступления на склад';
receipt 'Поступление' = DATA Receipt (ReceiptDetail) NONULL DELETE;

sku 'SKU' = DATA SKU (ReceiptDetail);

quantity 'Кол-во' = DATA NUMERIC[14,2] (ReceiptDetail);
price 'Цена' = DATA NUMERIC[14,2] (ReceiptDetail);

Необходимо сделать, чтобы он проводился по созданному нами регистру. Для этого мы возьмем строку поступления ReceiptDetail и унаследуем ее от абстрактного класса SkuLedger:

EXTEND CLASS ReceiptDetail : SkuLedger;

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

dateTime(ReceiptDetail d) += dateTime(receipt(d));

stock(ReceiptDetail d) += stock(receipt(d));

sku(ReceiptDetail d) += sku(d);
quantity(ReceiptDetail d) += quantity(d);

Количество и товар мы подставляем непосредственно из строки, а время и склад из документа.

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

Перемещение со склада на склад

CLASS Transfer 'Перемещение со склада на склад';
posted 'Проведен' = DATA BOOLEAN (Transfer);
dateTime 'Дата/время' = DATA DATETIME (Transfer);

fromStock 'Склад (откуда)' = DATA Stock (Transfer);
toStock 'Склад (куда)' = DATA Stock (Transfer);

CLASS TransferDetail 'Строка отгрузки со склада';
transfer 'Поступление' = DATA Transfer (TransferDetail) NONULL DELETE;

sku 'SKU' = DATA SKU (TransferDetail);

quantity 'Кол-во' = DATA NUMERIC[14,2] (TransferDetail);
price 'Цена' = DATA NUMERIC[14,2] (TransferDetail);

Его также нужно провести по этому регистру, но уже дважды. Один раз он должен списать со склада (откуда) перемещенное количество по товару, а второй раз добавить его к складу назначения. Для того, чтобы списать товар будем использовать ту же логику наследования:

Проведение расхода

EXTEND CLASS TransferDetail : SkuLedger;

dateTime(TransferDetail d) += dateTime(transfer(d));

stock(TransferDetail d) += fromStock(transfer(d));

sku(TransferDetail d) += sku(d);
quantity(TransferDetail d) += -quantity(d);

Так как это расходная операция, то количество берем с минусом, а в качестве склада подставляем склад отправителя.

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

CLASS TransferSkuLedger 'Перемещение на склад (регистр)' : SkuLedger;
transferSkuLedger = AGGR TransferSkuLedger WHERE stockTo(transfer(TransferDetail transferDetail));

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

dateTime(TransferSkuLedger d) += dateTime(transfer(transferDetail(d)));

stock(TransferSkuLedger d) += toStock(transfer(transferDetail(d)));

sku(TransferSkuLedger d) += sku(transferDetail(d));
quantity(TransferSkuLedger d) += quantity(transferDetail(d));

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

К слову, в 1С с этим есть определенные проблемы, так как строка документа может порождать только одну запись в регистре:

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

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

Регистр сведений

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

Объявление такого регистра абсолютно идентично логике регистра накоплений. Построим для примера регистр изменения цены поступления:

Регистр изменения цены поступления

CLASS ABSTRACT PriceLedger 'Регистр изменения цены поступления';

dateTime 'Дата/время' = ABSTRACT DATETIME (PriceLedger);

sku 'SKU' = ABSTRACT SKU (PriceLedger);
stock 'Склад' = ABSTRACT Stock (PriceLedger);

price 'Цена' = ABSTRACT NUMERIC[14,2] (PriceLedger);

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

price 'Цена' (Stock st, Sku sk, DATETIME dt) = 
    GROUP LAST price(PriceLedger l)
          ORDER dateTime(l), l
          WHERE dateTime(l) <= dt
          BY stock(l), sku(l);

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

INDEX stock(PriceLedger l), sku(l), dateTime(l), l;

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

Проведение по регистру сведений идет также, как и в регистре накоплений:

EXTEND CLASS ReceiptDetail : PriceLedger;

dateTime(ReceiptDetail d) += dateTime(receipt(d));

stock(ReceiptDetail d) += stock(receipt(d));

sku(ReceiptDetail d) += sku(d);
price(ReceiptDetail d) += price(d);

Заключение

Схема регистров в 1С позволяет делать то, что в обычном программировании реализуется при помощи наследования и композиции (там регистр был бы просто интерфейсом). Тем самым, они пытаются в частном случае решить проблему отсутствия этих механизмов в 1С платформе. Хотя, фактически, регистр является интерфейсом, который может реализовывать либо сама строка документа, либо некий агрегированный объект (композиция), созданный на ее основе.

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

Автор: VitalyB24

Источник


* - обязательные к заполнению поля