Часть 3. Создание цепочек
В этой статье я расскажу, как мы описываем схему бизнес-объектов предприятия, и как мы определяем иерархическую структуру идентификаторов цепочек этих объектов. Это определит интерфейс оператора системы, который сможет работать с системой без графической оболочки.
Типовые спецификации
Схема включает в себя всю типовую информацию о бизнес-объектах. Схема ERP модуля определяет базовые объекты организационной структуры, объекты платежной системы PAY и системы учета сотрудников ACC.
ERP SPEC — Таблицы организационной структуры
-type locationType() :: normal | extra.
-record('Loc',
{ id = kvs:seq([],[]) :: [] | term(),
code = [] :: [] | term(),
country = [] :: [] | binary(),
city = [] :: [] | binary(),
address = [] :: [] | binary(),
type = [] :: locationType() }).
-record('Branch',
{ id = kvs:seq([],[]) :: [] | term(),
loc = [] :: [] | #'Loc'{} }).
-record('Inventory',
{ id = [] :: [] | binary(),
name = [] :: [] | binary(),
branch = [] :: [] | #'Branch'{},
type = [] :: term() }).
-record('Organization',
{ name = [] :: [] | binary(),
url = [] :: [] | string(),
location = [] :: [] | #'Loc'{},
type = [] :: term() }).
-record('Person',
{ id = kvs:seq([],[]) :: [] | term(),
cn = [] :: [] | binary(),
name = [] :: [] | binary(),
displayName = [] :: [] | binary(),
location = [] :: #'Loc'{},
type = [] :: term() }).
-record('Employee',
{ id = kvs:seq([],[]) :: [] | binary(),
person = [] :: [] | #'Person'{},
org = [] :: [] | #'Organization'{},
branch = [] :: [] | #'Branch'{},
type = [] :: term() }).
PAY SPEC — Таблицы системы управления платежами
Деньги хранятся в формате {N,M}, где N — количество знаков после запятой, а M — все значимые цифры. Таким образом числа кодируются множественным образом, например единица: 1 = {0,1} = {1,10} = {2,100}. Операция умножения в такой системе выглядит просто mul({A,B},{C,D}) -> {A+C,B*D}.
-type fraction_length() :: integer().
-type digits() :: integer().
-type money() :: {fraction_length(),digits()}.
-record('Payment',
{ invoice = [] :: [] | term(),
volume = [] :: [] | money(),
price = {0,1} :: money(),
instrument = [] :: term(),
type = [] :: paymentType(),
from = [] :: term(),
to = [] :: term() }).
PLM SPEC — Таблицы системы управления жизненным циклом
-record('Acc',
{ id = [] :: [] | binary() | list(),
rate = {0,0} :: money() }).
-record('Product',
{ code = [] :: [] | term(),
id = kvs:seq([],[]) :: [] | binary(),
url = [] :: [] | binary() | list(),
engineer = [] :: [] | #'Person'{},
director = [] :: [] | #'Person'{},
owner = [] :: [] | #'Person'{},
organization = [] :: [] | #'Organization'{},
type = [] :: productType() }).
-record('Investment',
{ id = [] :: [] | term(),
volume = [] :: [] | money(),
price = {0,1} :: money(),
instrument = [] :: term(),
type = [] :: investmentType(),
from = [] :: term(),
to = [] :: term() }).
Создание корневых цепочек
ERP BOOT или пусконаладка предприятия — это процесс заполнения первичных словарей и таблиц фундаментальной информацией. Главным образом это отображение иерархической, организационной структуры предприятия. От работника предприятия, его рабочего места, его бренча, его локальной компании, все бренчи, которое находятся в одной стране, и далее до группы международных компаний с офисами в разных странах мира, и возможно, даже до синдикатов транснациональных корпораций. В зависимости от того, какую организационную структуру предприятия вы хотите, так вы и раскладываете данные на первичные фиды.
ERP BOOT — Организационная структура предприятия
Рассмотрим пример: компания Quanterall, главный подрядчик Aethernity, имеет офисы в Софии, Варне (главный офис компании) и Пловдиве. Сама компания совершает операции только в Болгарии, поэтому группа состоит из одной компании.
Добавляем, сейчас и впредь, данные с помощью обычных list комбинаторов:
-module(erp).
-compile(export_all).
boot() ->
GroupOrgs =
[ #'Organization'{
name="Quanterall",
url="quanterall.com"} ],
HeadBranches =
[ #'Branch'{ loc = #'Loc'{ city =
"Varna", country = "BG" } },
#'Branch'{ loc = #'Loc'{ city =
"Sophia", country = "BG" } },
#'Branch'{ loc = #'Loc'{ city =
"Plovdiv", country = "BG" } } ],
PartnersOrgs =
[ #'Organization'{ name="NYNJA"},
#'Organization'{ name="Catalx"},
#'Organization'{ name="FiaTech"},
#'Organization'{ name="3Stars"},
#'Organization'{ name="SwissEMX"},
#'Organization'{ name="HistoricalPark"},
#'Organization'{ name="Intralinks"} ],
Structure =
[ {"/erp/group", GroupOrgs},
{"/erp/partners", PartnersOrgs},
{"/erp/quanterall", HeadBranches} ],
lists:foreach(fun({Feed, Data}) ->
case kvs:get(writer, Feed) of
{ok,_} -> skip;
{error,_} -> lists:map(fun(X) ->
kvs:append(X,Feed)
end, Data) end end, Structure).
PAY BOOT — Учётность CashFlow
Управление отчетностью аутсорс предприятия достаточно простое: 1) мы принимаем оплаты по инвойсам, периодически выставленным клиентам, регулярно раз в месяц; 2) мы выплачиваем зарплаты раз в месяц. Поэтому фолды группируются календарно и зипуются помесячно.
sal_boot() ->
lists:map(fun(#'Product'{code=C} = P) ->
lists:map(fun(#'Payment'{}=Pay) ->
kvs:append(Pay,
"/plm/"++C++"/outcome") end,
salaries(C))
end, products()).
pay_boot() ->
lists:map(fun(#'Product'{code=C} = P) ->
lists:map(fun(#'Payment'{}=Pay) ->
kvs:append(Pay,
"/plm/"++C++"/income") end,
payments(C))
end, products()).
PLM BOOT — Бюджетирование проектов
Инициализация почасовая для каждого сотрудника по проекту. Этот список будет использоваться для распределения выплат по опционам в будущем.
assignees() ->
lists:map(fun(#'Product'{code=C} = P) ->
case kvs:get(writer,"/plm/"++C++"/staff") of
{error,_} ->
lists:map(fun(#'Person'{}=Person) ->
kvs:append(Person,
"/plm/"++C++"/staff") end,staff(C));
{ok,_} -> skip end
end, products()).
Выплаты по процентам на субконто попроектно:
accounts() ->
lists:map(fun(#'Product'{code=C}) ->
lists:map(fun(#'Acc'{id=Id, rate=R}=SubAcc) ->
Address = lists:concat(["/fin/acc/",C]),
kvs:append(SubAcc,Address),
Feed = lists:concat(["/fin/tx/",Id]),
case kvs:get(writer, Feed) of
{error,_} ->
lists:map(fun(#'Payment'{
invoice=I,price=P, volume=V}=Pay) ->
kvs:append(rate(Pay,SubAcc,C), Feed)
end, payments(C));
{ok,_} -> skip
end
end, acc(C))
end, plm_boot:products()).
За работу с данными отвечает библиотека KVS, как работать с ней читайте в предыдущих выпусках журнала:
Инкапсуляция структуры предприятия
Весь код, который нужен для создания фидов, мы обычно выносим в приложение с названием ERP. Для каждого конкретного предприятия мы используем свою Github организацию, можно даже другое имя репозитория, но всегда это же имя Erlang/OTP приложения.
В этом репозитории всегда будут какие-то реальные данные компании, которую мы автоматизируем, если она нам разрешает публиковать свою организационную структуру, первичные данные и словари. Делаем этот репозиторий приватным в отдельных случаях. Сами приложения способны работать с любыми структурами ERP.
Примеры запросов к хранилищу
Elixir прелюдия:
defmodule PLM.Mixfile do
use Mix.Project
def project() do
[
app: :plm,
version: "0.7.1",
elixir: "~> 1.8.1",
description: "PLM Product Lifecycle Management",
deps: [{:bpe, "~> 4.7.3"}, {:erp, "~> 0.7.6"}]
]
end
def application(),
do: [mod: {PLM.Application, []},
applications: [:rocksdb, :kvs, :bpe, :erp]]
end
Содержимое корневой директории БД предприятия:
> :writer |> :kvs.all |> :lists.sort
[
{:writer, '/acc/quanterall/Plovdiv', 3, [], [], []},
{:writer, '/acc/quanterall/Sophia', 9, [], [], []},
{:writer, '/acc/quanterall/Varna', 23, [], [], []},
{:writer, '/bpe/hist/1562855060639704000', 1, [], [], []},
{:writer, '/bpe/proc', 1, [], [], []},
{:writer, '/erp/group', 1, [], [], []},
{:writer, '/erp/partners', 7, [], [], []},
{:writer, '/erp/quanterall', 3, [], [], []},
{:writer, '/fin/acc/CATALX', 4, [], [], []},
{:writer, '/fin/acc/NYNJA', 4, [], [], []},
{:writer, '/fin/tx/CATALX/R&D', 12, [], [], []},
{:writer, '/fin/tx/CATALX/insurance', 12, [], [], []},
{:writer, '/fin/tx/CATALX/options', 12, [], [], []},
{:writer, '/fin/tx/CATALX/reserved', 12, [], [], []},
{:writer, '/fin/tx/NYNJA/R&D', 5, [], [], []},
{:writer, '/fin/tx/NYNJA/insurance', 5, [], [], []},
{:writer, '/fin/tx/NYNJA/options', 5, [], [], []},
{:writer, '/fin/tx/NYNJA/reserved', 5, [], [], []},
{:writer, '/plm/CATALX/income', 12, [], [], []},
{:writer, '/plm/CATALX/investments', 4, [], [], []},
{:writer, '/plm/CATALX/outcome', 12, [], [], []},
{:writer, '/plm/CATALX/staff', 2, [], [], []},
{:writer, '/plm/NYNJA/income', 5, [], [], []},
{:writer, '/plm/NYNJA/investments', 2, [], [], []},
{:writer, '/plm/NYNJA/outcome', 5, [], [], []},
{:writer, '/plm/NYNJA/staff', 4, [], [], []},
{:writer, '/plm/products', 2, [], [], []}
]
Список компаний, входящих в группу предприятия:
> :kvs.feed '/erp/group'
[{:Organization, 'Quanterall', 'quanterall.com', [], []}]
Список бренч-офисов головной (и единственной) компании группы:
> :kvs.feed '/erp/quanterall'
[
{:Branch, '1562329445378242000',
{:Loc, '1562329445378243000', [], 'BG', 'Plovdiv', [], []}},
{:Branch, '1562329445378241000',
{:Loc, '1562329445378242000', [], 'BG', 'Sophia', [], []}},
{:Branch, '1562329445378234000',
{:Loc, '1562329445378240000', [], 'BG', 'Varna', [], []}}
]
Список контрагентов:
> :kvs.feed '/erp/partners'
[
{:Organization, 'Catalx Exchange Inc.', 'catalx.io', [], []},
{:Organization, 'HistoricalPark', [], [], []},
{:Organization, 'NYNJA, Inc.', 'nynja.io', [], []},
{:Organization, 'Intralinks', [], [], []},
{:Organization, 'SwissEMX', [], [], []},
{:Organization, 'FiaTech', [], [], []},
{:Organization, '3Stars', [], [], []}
]
Бюджетирование проекта по статьям субконто:
> :kvs.feed '/fin/acc/NYNJA'
[
{:Acc, 'NYNJA/insurance', {2, 70}},
{:Acc, 'NYNJA/reserved', {2, 10}},
{:Acc, 'NYNJA/options', {2, 10}},
{:Acc, 'NYNJA/R&D', {2, 10}}
]
Выплаты по опционам для программистов:
> :kvs.feed '/fin/tx/CATALX/options'
[
{:Payment, '1562868880497278000', {0, 1}, {2, 150000}, 'USD', :crypto, [], []},
{:Payment, '1562868880496849000', {0, 1}, {2, 100000}, 'USD', :crypto, [], []},
{:Payment, '1562868880496409000', {0, 1}, {2, 120000}, 'USD', :crypto, [], []},
{:Payment, '1562868880495897000', {0, 1}, {2, 150000}, 'USD', :crypto, [], []},
{:Payment, '1562868880495412000', {0, 1}, {2, 100000}, 'USD', :crypto, [], []},
{:Payment, '1562868880494920000', {0, 1}, {2, 100000}, 'USD', :crypto, [], []},
{:Payment, '1562868880494538000', {0, 1}, {2, 100000}, 'USD', :crypto, [], []},
{:Payment, '1562868880494072000', {0, 1}, {2, 50000}, 'USD', :crypto, [], []},
{:Payment, '1562868880493672000', {0, 1}, {2, 70000}, 'USD', :crypto, [], []},
{:Payment, '1562868880493387000', {0, 1}, {2, 150000}, 'USD', :crypto, [], []},
{:Payment, '1562868880492939000', {0, 1}, {2, 120000}, 'USD', :crypto, [], []},
{:Payment, '1562868880492234000', {0, 1}, {2, 120000}, 'USD', :crypto, [], []}
]
Люди, работающие на проекте:
> :kvs.feed '/plm/NYNJA/staff'
[
{:Person, '1562868880467887000', 'Maxim Sokhatsky', [], [], [], 1, []},
{:Person, '1562868880467886000', 'Yuri Maslovsky', [], [], [], 8, []},
{:Person, '1562868880467885000', 'Nikolay Dimitrov', [], [], [], 4, []},
{:Person, '1562868880467884000', 'Radostin Dimitrov', [], [], [], 4, []},
{:Person, '1562868880467883000', 'Georgi Spasov', [], [], [], 8, []}
]