Программная реализация расчетной системы

Расчетные модули (в дальнейшем - расчетчики) вынесены в отдельные процессы, которые запускаются одновременно с основным ядром системы.

Общение между основным процессом и расчетчика происходит с помощью протокола ampq реализовано на базе RabbitMq.

Среди основных частей расчетной системы можно выделить следующие блоки

  1. Интерфейс получения информации из справочников.
  2. Компилятор языка формул - модуль разбора формул
  3. Модуль приведения формульных выражений к первичным значениям - основная логика формирования формул для ячеек
  4. Модуль выполнения расчета ячеек - получение первичных данных и расчет формул
  5. Модуль агрегации данных - применение правил агрегации и суммирование нескольких документов для разных организаций
  6. Модуль автопрокачки данных - система оптимизации расчетов

Работа со справочниками

В системе используются 3 базы данных.

  1. Основание системы - SQL база, которая хранит в себе описание всех моделей и их взаимосвязей. Работа с базой данных происходит через интерфейс хранимых процедур, а не на прямую.
  2. Mongo - используется в качестве промежуточного слоя. На Mongo реализованы механизмы работы в режиме SandBox - когда пользовательские изменения изолируются друг от друга. В монго нет информации о данных из таблицы [Core].[Cells]
  3. Redis - способ кэширования данных - используется для оптимизации

Для калькулятора реализовано несколько вспомогательных классов, для получения информации из справочников. Все эти классы понаследованы от базового Base.js и расположены в папке ./classes/calculator/helpers. Классы работают с Mongo базой и кэшируют результаты своей деятельности в Redis

Функционал базового класса - получение из кэша и сохранение в кэш плюс своя функция запросов к Mongo - query, которая является оберткой над обычным запросом find, но в зависимости от контекста документа подключает или нет режим работы в песочнице.

module.exports = function(Context){

    var self = this;

    self.Context = _.clone(Context);

    self.query = function(modelName,query,fields){}

    self.cacheKey = function(){}

    self.loadFromCache = function(done){}

    self.saveToCache = function(Result,done){}
}

Документ (Doc.js)

Класс предназначен для получения следующей информации:

  1. Основные поля документа
  2. Если документ рассчитан на работу с дочерними объектами учета - мы получим информацию о возможных объектах учета для данного документа
  3. Информация о том кто этот документ подписывает

Подробнее о некоторых полях:

  • IsShowRoots - Отображать прикрепленные строки полностью или только их дочерние строки
  • IsActiveCondition - Доступна или нет панель настроек в режиме ввода
  • IsOlap IsChart IsPresent IsInput - Доступны или нет режимы OLAP, График, Презентация, Форма ввода
  • Для режима HasChildObjs = true - (документ работает на дочерних объектах учета) доступны дополнительные настройки:
  • Link_docobjtype - фильтрация дочерних объектов учета для документа
  • IsDivObj - отображать дочерние объекты отдельным выбором или склеить ряды для всех объектов учета
  • IsObjToRow - режим похожий на OLAP отчет - когда вместо рядов отображаются дочерние объекты учета
  • IsShowParentObj - дополнительная настройка для IsObjToRow - когда дочерние объекты отображаются вместе с родительскими в структуре "дерево"
{
   CodeDoc: "calc_chernmed",
   NameDoc: "Калькуляция черновой меди",
   PrintNameDoc: "Калькуляция черновой меди",
   PrintNumDoc: "",
   HasChildObjs: true,
   IsShowRoots: false,
   IsActiveCondition: false,
   IsPrimary: false,
   IsAnalytic: false,
   IsOlap: false,
   IsInput: true,
   IsChart: false,
   IsPresent: false,
   IsDivObj: true,
   IsObjToRow: false,
   IsShowParentObj: false,
   CodeModel: "TPFP",
   CodeGrp: "CALC_CU",
   CodeRole: "COST",
   CodeDocType: "MONTHDOC",
   Labels: [{
      IsSignature: true,
      IsApproval: false,
      CodePeriodGrp: "PLAN",
      CodeLabel: "gdir",
      NameLabel: "Генеральный директор",
      Idx: 1,
      Users: {
         473: {
            421: {
               NameUser: "######## ########### ###########",
               CodeUser: "421",
               JobTitle: "Генеральный директор",
               CodeOrg: "473"
            }
         }
      }
   }],
   ChildObjs: ["3272","5341","5342"]
}

Ряд (Row.js)

Получение рядов, отображаемых в таблицах.
Для получения структуры берется информация из ссылок RowDoc (привязанные к документу корневые узлы) с учетом IsExpandTree (показывать только 1 строку или все дочерние узлы)
При работе с OLAP документом IsExpandTree не учитывается и всегда считается false;
В зависимости от параметра документа IsShowRoots корневые узлы будут отображаться или будут отображаться только их дочерние ряды

В зависимости от года передаваемого в запрос могут произойти следующие изменения:
Ряд может быть исключен на основании значений полей FromObsolete и FromYear
Ряд может изменить свой флажок IsFormula в зависимости от полей FormulaFromObsolete и FormulaFromYear

При тонкой фильтрации рядов используются следующие параметры:
NoFiltered - строка (включая всю цепочку родительских узлов) всегда показывается
HasFilteredChild - для всех дочерних строк применяется фильтрация на основании модели ObjGrp - в которой может быть установлена связь между рядом и объектом учета (или группой объектов или типом объектов)

Конструкции, когда по дереву происходит установка флага фильтрации HasFilteredChild->NoFiltered->HasFilteredChild приводят к ошибке.

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

{
   CodeRow: "m200090",
   NameRow: "Рентабельность продаж",
   IndexRow: 0,
   NumRow: "1.3.1",
   IsFormula: true,
   Formula: "$m200080? / $m200060? * 100",
   IsSum: false,
   NoSum: false,
   IsMinus: false,
   IsControlPoint: false,
   IsCalcSum: false,
   IsAgFormula: false,
   AgFormula: "",
   AsAgFormula: true,
   NoDoSum: false,
   UseProdSumGrps: false,
   CodeValuta: "NONE",
   CodeParentRow: "m200080",
   rowpath: "/000000000000/m000/m200/m20006/m200080/m200090/",
   rgt: 4286,
   lft: 4285,
   level: 3,
   Sums: [ ],
   Filter: [ ],
   Tags: [
      "olap_osnpok:1",
      "numberformat:0.##",
      "AsAgFomula:1",
      "fstr:1"
   ],
   IsLeaf: true,
   Format: "0.##"
}

Для рядов подключен механизм работы с деревьями Nested Sets подробнее можно почитать здесь

Корневые узлы (DocRow.js)

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

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

  1. Предпочтение отдается документам без IsOlap IsChart IsPresent
  2. IsExpandTree у ссылки true - предпочтительней
  3. Для документов не на корневых объектах учета core.Docs.HasChildObjs=0
    1. Количество корневых узлов - выбирается документ с наименьшим числом корневых узлов в таблице link.DocRows
    2. Длина кода документа - выбирается документ с наименьшей длиной кода в поле core.Docs.CodeDoc
  4. Для документов на дочерних объектах учета core.Docs.HasChildObjs=1
    1. Определяется тип или класс объекта учета из контекста
    2. В таблице link.DocObjs фильтруется перечень документов, из которых нужный определяется по типу или классу.
    3. Если все параметры сошлись, но осталось несколько документов, то выбирается документ с более коротким кодом core.Docs.CodeDoc

При обращении к классу вы получите хэш таблицу, для вычисления кодов документов

{
    WithChildObjs: {
    z100: {
        Empty: {
            Empty: "calc_balproiz"
        },
        CALCED: {
            CE_RUDAFE: "calc_bru",
            CE_ZRK: "calc_bru",
            CE_SHN: "calc_bru",
            CE_RUDAOBFE: "calc_bru",
            CE_CABM: "calc_cab_vid",
            CE_AGL: "calc_chermet",
            CE_CHUG: "calc_chermet",
            CE_STL: "calc_chermet",
            CE_STPK: "calc_chermet",
            CE_STKL: "calc_chermet",
            ...
            CE_STP: "calc_stp_vid",
            CE_TNP: "calc_tnp_vid",
            CE_UC: "calc_uc_vid",
            CE_UGOL: "calc_ugol_dob",
            CE_ZN: "calc_zn"
        }
    },
    z112: {
        Empty: {
            Empty: "calc_ruda"
        },
        CALCEDOLD: {
            CE_SCR: "calc_ruda_pered",
    ...
    },
    NoChildObjs: {
        a120: "anpok_os",
        a150: "anprot",
        a151: "anprot_cena",
        a152: "anprot_ss",
        k112: "balans2015_cons",
        a111: "balans_an",
        r126: "balans_ds",
        m179: "balans_filial",
        r214: "finres_ras",
        ...
        v214: "finres_val",
        p214: "finres_zatr",
        m215: "free_active",
        a250: "inv_an",
        r250: "inv_ras",
        i200: "inv_teo",
        i210: "inv_teo_macro",
        b102: "kovenant_amort",
        b103: "kovenant_control",
        b120: "kovenant_gpb",
        b101: "kovenant_normativ",
        b130: "kovenant_sb",
        b131: "kovenant_sb_2012",
        b110: "kovenant_vtb",
        ...
        b265: "zp_zatr_conto69"
    }
}

Объект учета (Div.js)

Получение информации по объектам учета. Полная информация распределена по следующим таблицам:

Модель Поля
obj CodeObjType, CodeOrg, CodeParentObj
org CodeDiv, CodeOtrasl, CodeCity
city CodeRegion
objtype CodeObjClass
objtag информация о Тэгах

Обращаясь к этому классу вы получаете собранную полную информацию обо всех объектах учета плюс информацию по иерархии RootObj, CodeParentObj, Children, AllChildren :

{
    CodeObj: "473",
    IsFormula: false,
    Formula: "",
    CodeValuta: "RUB",
    CodeObjType: "OWNORG",
    CodeOrg: "473",
    CodeParentObj: "353",
    CodeDiv: "MET",
    CodeObjClass: "ORGS",
    Groups: [
        "G4",
        "OPR",
        "PRD_CHOP",
        "G1",
        "G5",
        "DIV_MET",
        "G12",
        "PS_UGMK",
        "G2"
    ],
    CodeCity: "35379",
    CodeRegion: "56",
    CodeOtrasl: "ЦМ",
    Tags: [
        "carow:CA_STRUCT2000"
    ],
    RootObj: "353",
    Children: [
        "1873",
        "1920",
        "1914",
        "1915",
        "1917",
        "1918",
        "1919",
        "1923",
        "3852",
        "5829"    
    ],
    AllChildren: [
        "1873",
        "3272",
        "5341",
        "5342",
        "1920",
        "1914",
        "1915",
        "1917",
        "1918",
        "1919",
        "1923",
        "3852",
        "5829"
    ]
}

Период (Period.js)

В системе есть следующие типы периодов

  1. Обычные (расчетные) все остальные типы так или иначе приводятся к ним
19: {
      CodePeriod: "19",
      Name: "сен.",
      SName: "сен.",
      MCount: 1,
      MonthStart: 9,
      Conditions: {
         ismonth: true,
         issumperiod: false,
         iskorrperiod: false,
         isplanperiod: false,
         isozhidperiod: false
      }
}
  1. Формульные периоды (начинаются со знака "-"). Содержат хэш таблицу преобразования периода из контекста в расчетный период. Если в качестве значения используются массивы, то расчетчик суммирует входящие в массив периоды.
-201: {
   1: "1",
   2: "2",
   3: "3",
   4: "4",
   12: "22",
   13: "1",
   14: "24",
   15: "25",
  ...
   619: "629",
   6110: "6210",
   6111: "6211",
   6112: "6212"
},

Кроме информации о периодах класс отдает информацию о том, какое название периода использовать после формульных преобразований (позднее эта информация используется в получении колонок документа). Эта информация хранится в хэш таблице DisplayNames.

DisplayNames: {
   -201: {
      403: "403"
   },
   -406: {
      406: "406"
   },
   -311: {
      31: "311",
      32: "312",
      33: "313",
      34: "314"
   },
   ...

Модуль выполнения расчета ячеек

Реализация модуля находится в файле ./classes/calculator/AssoiCalculatorHelper.js.

var Evaluator = function(Unmapper){    
    var self = this;
    self.Context = Unmapper.Context;
    // Ячейки, которые нужно посчитать
    self.HowToCalculate = Unmapper.HowToCalculate;
    // Посчитанные ячейки
    self.Calculated = {};
    // Какие поля нужно запросить при загрузке первичных ячеек
    // в зависимости от кода валюты в контексте
    self.Valutas   = {"RUB":"ReportValue","USD":"ReportValue1","EUR":"ReportValue2"};    
    // Выбранное запрашиваемое поле 
    self.Field = 'Value';
    // Публичная функция, которая вызвается снаружи
    self.Calculate = function(done){...}
    // Внутренние функции
    // Убираем промежуточные переменнные
    self.FilterResults = function(done){...}
    // Хэш-таблица с информацией о загруженных первичных ячейках
    self.PrimariesInfo = {...};    
    // Загрузка первичных ячеек
    self.LoadPrimaries = function(Primaries,done){...}
    // Ограничение на глубину вложенности переменных
    self.maxRecursions    = 200;    
    self.currentRecursion = 1;
    // Рекурсивная функция вычисления
    self._calculate = function(done){...}
    // Проверка на то, что все зависимые переменные вычислены
    self._isCalculateble = function(Vars){...}    
    // Вычисление формулы
    self._calculateFormula = function(CellName,Formula,Vars){...}
}

При инициализации передается результат работы предыдущего модуля Unmapper

Логика работы заключается в следующем.

  1. Загружаются все первичные переменные используемые в формулах LoadPrimaries
  2. Вызывается рекурсивно функция _calculate, которая пробегает по всем не посчитанным переменным из hash-таблицы HowToCalculate, проверяет, что может их вычислить (все переменные, необходимые для вычисления, находятся в hash-таблице Calculated) с помощью функции _isCalculateble, вычисляет их с помощью функции _calculateFormula, заносит результат в hash-таблицу Calculated и исключает переменную из hash-таблицы HowToCalculate. Функция выполняется до тех пор пока все переменные не будут посчитаны
  3. После окончания вычислений происходит фильтрация результата - FilterResults (отсеиваются промежуточные переменные - не запрошенные пользователем для расчета документа)

Возможные доработки:

  1. В качестве оптимизации вычислений, можно упорядочить ячейки таким образом, чтобы вычисление произошло за 1 проход без рекурсий.
  2. Вынести значения кодов валют для первичных ячеек в настройки системы
  3. Информация о первичных ячейках возвращает IdUser (пользователя, который внес информацию). Необходимо переделать его на CodeUser.
  4. Реализовать функцию возврата первичных значений в PostgreSQL с поддержкой механизма распараллеливания вручную через индекс по функции
  5. При запросе первичных ячеек учитывать, что некоторые переменные требуют перевода валюты .tovaluta('NONE') в старом синтаксисе и ['NONE'] - в новом.

results matching ""

    No results matching ""