Часть 3. Создание цепочек

Часть 1. Введение
Часть 2. Общая структура
Часть 3. Первичные цепочки
Часть 4. Администратор данных
Часть 5. Администратор процессов
Часть 6. Структура приложения
Часть 7. Публикация в GCP

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

Типовые спецификации

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

2019-04-13 Новая Версия KVS
synrc/kvs

Инкапсуляция структуры предприятия

Весь код который нужен для создания фидов мы обычно выносим в приложение с названием ERP. Для каждого конкретного предприятия мы используем свою Github организацию, можно даже другое имя репозитория, но всегда это же имя Erlang/OTP приложления.

erpuno/erp

В этом репозитории будут всегда какие-то реальные данные компании которую мы автоматизируем, если она нам разрешает публиковать свою организационную структуру, первичные данные и словари. Делаем этот репозиторий приватный в отдельных случаях. Сами приложения способны работать с любыми структурами 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, []} ]