💰 IEE 365 | Интеграция Интеграционное приложение с ELMA365 Манифест решения · Монолит является источником исторических данных для 365 в режиме «непрерывной миграции» (CM - continuous migration) · Небольшие изменения в функционале монолита возможны, если они критичны для бизнеса в конкретный момент времени · Создание новой функциональности должно быть только на стороне 365 · Плавный переход с монолита на 365 предполагает постепенный отказ от использования функционала в монолите и заменой его (с возможным рефакторингом) в 365 · Контур в монолите (не связанный с другими контурами) замененный в 365 должен быть отключен и недоступен для последующего использования · В случае связанности контуров их отключение должно происходить без потери действующих активных связей · Запрещается обратная синхронизация исторических данных из 365 что будет противоречить данному манифесту · Заказчик обязан понимать и принимать положения данного манифеста для успешной реализации проекта Описание функционала Базовый (ограниченный) функционал ITino.ELMA.E365.Common: Настройки подключения к ELMA365 Пинг ELMA365 (определение статуса) Проверка наличия модуля на стороне ELMA365 Принудительная синхронизация старт-флагом E365ForceSync Возможность включать/отключать синхронизацию: Пользователи Оргструктура Файлы Контрагенты (без учета связанных элементов CRM) Контакты (без учета связанных элементов CRM) Комментарии к сущностям Действия для сущностей Проверка оргструктуры на возможность выгрузки Точки расширения: IForceSyncHandler IDataItemExtension IFeedItemProvider Механизм маппинга идентификаторов между системами Возможность установить статус Приложений В случае невозможности связи по __id используется специальный механизм на стороне ELMA3/4 Поддержка лимитов для SaaS редакции ELMA3/4 👉️ ELMA365: Передача дополнительных (кастомных) свойств сущностей Простые типы Сущности Файлы Документы Блоки в сущностях Произвольные сущности конфигурации Процессы Синхронный/асинхронный запуск из скриптов процессов Запуск с контролем выполнения или без Простые параметры контекста Сущности (справочники) в параметрах контекста Файлы Версия исполняемого процесса (переменная processVersion ) ELMA3/4 👈️ ELMA365: Получение __id для создаваемых приложений Статусы выполнения процессов Расширенный функционал с установленным модулем RMG 365 | Интеграция с ELMA3/4 в ELMA365: ELMA3/4 👉️ ELMA365: Установка связи Администратора инстанса (ELMA3/4) с Супервизором системы (ELMA365) Перенос (создание/изменение) Пользователей ФИО Эл. почта Сотовый Рабочий Дата рождения Дата приема на работу Фото (если в ELMA3/4 пустое, то не обновляется в ELMA365) Временная зона Отображаемая должность (включается в настройках) Должности (привязка к оргструктуре) Блокировка/разблокировка Пользователей Перенос (создание) оргструктуры - добавление и изменение элементов Протоколирование проблем и предупреждений при экспорте оргструктуры и подсказки что исправить TODO Смена порядка и удаление элементов в оргструктуре ELMA3/4 👈️ ELMA365: Получение версии модуля и серверного времени в UTC Получение версии и редакции ELMA365 Получение идентификатора Супервизора системы Получение идентификаторов Пользователей при переносе Получение идентификаторов элементов оргструктуры Ограничения Компоненты собираются под свежие сборки ядра 3.15 и 4.1. Кастомные сборки под старые релизы возможны по договоренности Не поддерживается кластер Enterprise более чем с одним активным узлом Смена супервизора не предусмотрена - будет потеряна связь с Администратором инстанса Пересоздание корневого элемента оргструктуры в ELMA3/4 не поддерживается Перенос (создание) пользователей в ELMA365 осуществляется только для статусов Активный (не заблокирован) В ELMA365 не переносится информация о AD/LDAP пользователей - создаются обычный пользователи с авторизацией по email. Интеграцию нужно настраивать в ELMA365 вручную. Не поддерживается синхронизация изменения порядка и удаления элементов оргструктуры В поступлениях используется валюта Рубли Размер файла не может быть больше 50 Мб Не поддерживаются блоки Не поддерживаются блок в блоке Передача блоков при запуске процессов не предусмотрена Переносится только заполненный аватар пользователя. Если его удалить в ELMA3/4, то он останется в ELMA365. Это связано с особенностями формирования "пустых" аватаров в ELMA365. Контур CRM Функционал управления клиентами ITino.ELMA.E365.CRM: Возможность включать/отключать синхронизацию справочников и сущностей BackLog Принудительная инициализация старт-флагом E365InitCRM Маппинг базовых сущностей в ELMA3/4 и ELMA365 ФЛ - Контакт ЮЛ - Компания Контакт ЮЛ - Контакт Отрасль - Отрасль Источник возможности - Источник лидов Возможность - Лид Контакт возможности - Контакт Сделка - Сделка (Основание) Поступление - Поступление (план) Валюта - Валюта Маркетинговое мероприятие - Маркетинговое мероприятие Маппинг сущностей ITino.ELMA.CRM в ELMA3/4 и ELMA365 Бизнес группа - Компания Контакт Бизнес группы - Контакт Филиал - Компания Контакт Филиала - Контакт Предприниматель - Компания Контакт Предпринимателя - Контакт Маппинг сущностей ITino.ELMA.CRM.RU в ELMA3/4 и ELMA365 ЮЛ РФ - Компания Контакт ЮЛ РФ - Контакт ИП РФ - Компания Контакт ИП РФ - Контакт Банковский реквизиты РФ - Банковский реквизиты (Компания) ELMA3/4 👉️ ELMA365: Отрасли Название Источники лидов Название Компании - наполнение полей определяется в зависимости от передаваемого объекта Контакты - наполнение полей определяется в зависимости от передаваемого объекта Лиды Статус Название Ответственный Компания Контакты (контроль создания дублей для Компании и Лида) Источник лида UtmCampaing UtmSource UtmMedium Сделки Статус Название Компания Бюджет (поддержка валюты) Поступления (план) Контрагент Основание (сделка) Сумма поступления Плановая дата Фактическая сумма (равна сумме поступления) Фактическая дата Статус (Ожидает, Оплачено, Отменено) Валюты Название Код Маркетинговые мероприятия Название Описание Ответственный Дата начала Дата окончания Бюджет ELMA3/4 👈️ ELMA365: Контур ECM Функционал управления документооборотом ITino.ELMA.E365.Documents: Возможность включать/отключать синхронизацию справочников и сущностей Перенос номенклатуры при публикации Перенос информации о регистрации документа (поддерживается множественная регистрация в разных делах) Возможность отображать в ленте ELMA365 Информация по согласованию документа Информация по ознакомлению с документом Маппинг базовых сущностей в ELMA3/4 и ELMA365 Мои юр. лица ELMA3/4 👉️ ELMA365: Номенклатура Место регистрации Название Раздел Название Дело Название Вид нумерации (Вречную или Автоматически) Следующий номер Шаг (всегда 1) Максимальное количество знаков Документопоток Шаблон номера {$GroupIndex} удаляется при переносе {$RegCardNumber} заменяется на {$__index} Регистрационная карточка документа Регистрационный номер Дата регистрации Мои юр. лица Название Полное наименование Юридический адрес Фактическия адрес ИНН КПП Расчетный счет Корреспондентский счет Банк БИК Телефон Ограничения Номенклатура Ссылки на дело не поддерживаются (миграция прерывается) Вложеность разделов не должна быть более одного уровня (миграция прерывается) Дело должно иметь хотя бы один типа документа и документопотока (иначе исключается из миграции) Дело не должно иметь разные документопотоки (миграция прерывается) {$GroupIndex} не поддерживается и удаляется при передаче Документопоток Договора не поддерживается (миграция прерывается) TODO  Сквозная нумерация не поддерживается Регистрационная карточка документа Автором регистрации является пользователь миграции Контур Проекты Функционал управления проектами ITino.ELMA.E365.Projects:   Контур Управление договорами Функционал управления проектами ITino.ELMA.E365.BS.Contracts:   Точки расширения IForceSyncHandler Используйте наследование от ForceSyncHandler /// /// [ExtensionPoint(ServiceScope.Shell)] public interface IForceSyncHandler { /// /// Включено /// bool Enabled { get; } /// /// Тип сущности /// Type Type { get; } /// /// Выполнить миграцию /// /// Дополнительные условия /// void Process(string query = null); } IDataItemExtension Используйте наследование от DataItemExtension /// /// [ExtensionPoint(ServiceScope.Shell)] public interface IDataItemExtension { /// /// Проверка типа от IBaseDataItem /// /// Тип /// bool CheckType(Type type); /// /// Получить кастомные простые свойства сущности /// /// /// JObject GetCustomSimple(IBaseDataItem item, IEntity entity); /// /// Получить связанные (справочники) кастомные свойства сущности /// /// /// void GetCustomLinked(IBaseDataItem item, IEntity entity); } Хелперы ServerHelper /// /// Возвращает признак среды разработки /// public static bool IsDev /// /// Логгер интеграции /// public static ILog E365Logger { get; } /// /// Запустить процесс в ELMA365 /// /// Контекст процесса монолита /// Пространство ELMA365 /// Процесс ELMA365 /// Модель контекста процесса ELMA365 /// Контроль выполнения в ELMA365 /// Асинхронно /// Вызывать исключение /// Класс контекста процесса public static Guid E365StartProcess(T context, string @namespace, string code, Action> action, bool ack = false, bool async = false, bool throwException = false) /// /// Полная синхронизация справочника /// /// Тип сущности /// public static bool E365FullSync(Type type) /// /// Синхронизация системной информации (оргструктура и пользователи) /// /// public static bool E365SystemSync()   ListenerHelper /// /// Проверка на возможность мягкого удаления /// /// Событие /// Проверочное действие /// Тип public static void TrySoftDeleting(PreUpdateEvent @event, Action action) where T : class, IEntity /// Получить старое значение /// Тип /// Событие /// Название свойства /// Старое значение public static T GetOldValue(PostUpdateEvent @event, string propertyName) /// Получить старое значение /// Тип /// Событие /// Название свойства /// Старое значение public static T GetOldValue(PreUpdateEvent @event, string propertyName) /// Получить значение свойства /// Тип /// Событие /// Название свойства /// Значение public static T GetValue(PostUpdateEvent @event, string propertyName) /// Присвоить значение свойству /// Событие /// Название свойства /// Значение public static void SetValue(PreUpdateEvent @event, string propertyName, object value) /// Получить значение свойства /// Тип /// Событие /// Название свойства /// Значение public static T GetValue(PostInsertEvent @event, string propertyName) /// Присвоить значение свойству /// Событие /// Название свойства /// Значение public static void SetValue(PreInsertEvent @event, string propertyName, object value)   Менеджеры E365ProcessLinkManager /// /// Запущен ли процесс в ELMA365 /// /// /// public bool IsRunning(Guid uid) /// /// Статус процесса в ELMA365 /// /// /// public WorkflowInstanceStatus GetStatus(Guid uid) /// /// Получить список запущенных процессов /// /// public IDictionary GetActiveProcesses()   Примеры Реализация миграции для справочника Страна Для передачи данных сущности из ELMA3/4 в ELMA365 в глобальном модуле необходимо определить класс маппинга. Пример для справочника Страна (с дополнительными полями в конфигурации ELMA3/4). В ELMA365 создаем Приложение как предложено здесь: Элемент обмена данными (маппинг): using ITino.ELMA.E365.Common.Models; using Newtonsoft.Json; namespace E365 { public class CountryDataItem : BaseDataItem { public override string Path => "app/_clients/rmgCountry"; [JsonProperty(PropertyName = "__name")] public string Name { get; set; } public string Code { get; set; } public string ShortName { get; set; } public long? CountryCode { get; set; } public string EnglishName { get; set; } public string Alpha2 { get; set; } public string Alpha3 { get; set; } public string Location { get; set; } public string LocationPrecise { get; set; } } } Лисенер для регистрации изменений элемента сущности: using EleWise.ELMA.Runtime.NH.Listeners; using EleWise.ELMA.ComponentModel; using NHibernate.Event; using ITino.ELMA.E365.Common.Components; using EleWise.ELMA.Model.Common; using EleWise.ELMA.Model.Entities; using ITino.ELMA.E365.Common.Managers; using EleWise.ELMA; using ITino.ELMA.CRM.Models; namespace E365 { [Component] public class CountryListener : PostFlushEventListener { public override void OnPostInsert(PostInsertEvent @event) { SyncItem(@event.Entity as ICOCountry, true); } /// public override void OnPostUpdate(PostUpdateEvent @event) { SyncItem(@event.Entity as ICOCountry, true); } public static Pair SyncItem(ICOCountry item, bool syncLink = false) { var data = new CountryDataItem(); if (item == null) return new Pair(data, item); data.Uid = item.Uid; data.Name = item.Name; data.Code = item.Code; data.ShortName = item.ShortName; data.CountryCode = item.CountryCode; data.EnglishName = item.EnglishName; data.Alpha2 = item.Alpha2; data.Alpha3 = item.Alpha3; data.Location = item.Location; data.LocationPrecise = item.LocationPrecise; E365DataItemManager.Instance.PushItem(typeof(CountryDataItem), item.Uid, data, item, SR.T("Страна: {0}", item.Name)); return new Pair(data, item); } } } Обработчик события полной синхронизации справочника. Так же используется для вызова принудительной синхронизации всех данных из хелпера: using System; using System.Linq; using EleWise.ELMA.ComponentModel; using EleWise.ELMA.Model.Services; using ITino.ELMA.E365.Common.Components; using EleWise.ELMA.Model.Managers; using ITino.ELMA.CRM.Models; namespace E365 { [Component] public class CountrySyncHandler : ForceSyncHandler { public override Type Type => InterfaceActivator.TypeOf(); public override void Process() { EntityManager.Instance.FindAll().ToList().ForEach(x => CountryListener.SyncItem(x)); } } } ICOCountry является расширением модульной сущности ICountry Полная принудительная миграция данных сущности Если необходимо сделать разово принудительную полную миграцию данных для сущности, у нее должен быть реализована точка расширения IForceSyncHandler . Самый простой способ - вызвать метод хелпера в сценарии процесса. При этом достаточно реализовать элемент Сценарий с со следующим кодом и запустить отладку процесса на данном элементе: namespace EleWise.ELMA.Model.Scripts { public partial class P_StartE365Process_Scripts : EleWise.ELMA.Workflow.Scripts.ProcessScriptBase { /// /// Запустить полную миграцию справочника Валюта /// /// Контекст процесса public virtual void FullSync (Context context) { ServerHelper.E365FullSync(InterfaceActivator.TypeOf()); } } } В данном примере используется справочник Валюта. Если используется компонент ITino.ELMA.E365.CRM, то точка расширения IForceSyncHandler уже присутствует. Иначе ее нужно создать самостоятельно. Свойства сущности типа IEntity При реализации маппинга простых типов данных не требуется каких либо ухищрений. Однако, если в сущности есть свойства типа ссылки на справочник или документ, то необходимо реализовать дополнительную логику. Пример для сущности Контакт (исходный код урезан для простоты понимания): /// public class ContactDataItem : BaseDataItem { /// public override string Path => "app/_clients/_contacts"; /// /// [JsonProperty(PropertyName = "__name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } ... /// /// [JsonProperty(PropertyName = "_companies", NullValueHandling = NullValueHandling.Ignore)] public List Companies { get; set; } ... } Данная реализация позволяет подготовить пакет передачи в ELMA365 в нужном формате. Лисенер для регистрации изменений элемента сущности (исходный код урезан для простоты понимания): public static Pair SyncContact(IContact item, bool syncLink = false) { var data = new ContactDataItem(); if (item == null) return new Pair(data, item); ... E365DataItemManager.Instance.PushItem(typeof(ContactDataItem), item.Uid, data, item, SR.T("Контакт: {0}", item.Name)); if (!syncLink) return new Pair(data, item); var link = new LinkDataItem(data); link.Links.Add(new LinkImpl(ContractorListener.SyncContractor(item.Contractor), BaseDataItem.GetJsonPropertyName(x => x.Companies))); E365DataItemManager.Instance.PushItem(typeof(LinkDataItem), link.Uid, link, item, SR.T("Связи в контакте: {0}", data.Name)); return new Pair(data, item); } Свойства сущности типа BinaryFile Если в сущности есть свойства типа BinaryFile, то необходимо реализовать дополнительную логику. Лисенер для регистрации изменений элемента сущности (исходный код урезан для простоты понимания): public static Pair SyncContact(IContact item, bool syncLink = false) { var data = new ContactDataItem(); if (item == null) return new Pair(data, item); ... E365DataItemManager.Instance.PushItem(typeof(ContactDataItem), item.Uid, data, item, SR.T("Контакт: {0}", item.Name)); if (!syncLink) return new Pair(data, item); var link = new LinkDataItem(data); // vCard для Контакта имеем тип BinaryFile // contact_vCard - название свойтва в Приложении ELMA365 типа Файл link.Links.Add(new LinkImpl(FileDataItem.Create(item.vCard), "contact_vCard")); E365DataItemManager.Instance.PushItem(typeof(LinkDataItem), link.Uid, link, item, SR.T("Связи в контакте: {0}", data.Name)); return new Pair(data, item); } Реализация миграции для документа Счет исходящий Для передачи данных сущности из ELMA3/4 в ELMA365 в глобальном модуле необходимо определить класс маппинга. Пример для справочника Страна (с дополнительными полями в конфигурации ELMA3/4). В ELMA365 создаем Приложение (документ)  как предложено здесь: Элемент обмена данными (маппинг): using System; using ITino.ELMA.E365.Common.Models; using ITino.ELMA.Accounting.Documents.Models; namespace E365 { public class OutInvoiceDataItem : DocumentDataItem { public override string Path => "app/_clients/rmgOutInvoice"; public string Number { get; set; } public DateTime Date { get; set; } public OutInvoiceDataItem(DOOutInvoice document) : base(document) { } } } Лисенер для регистрации изменений элемента сущности: using EleWise.ELMA.ComponentModel; using EleWise.ELMA.Runtime.NH.Listeners; using NHibernate.Event; using ITino.ELMA.Accounting.Documents.Models; using EleWise.ELMA.Model.Common; using ITino.ELMA.E365.Common.Components; using EleWise.ELMA.Model.Entities; using ITino.ELMA.E365.Common.Managers; using EleWise.ELMA; namespace E365 { [Component] public class OutInvoiceListener : PostFlushEventListener { public override void OnPostInsert(PostInsertEvent @event) { SyncItem(@event.Entity as DOOutInvoice, true); } /// public override void OnPostUpdate(PostUpdateEvent @event) { SyncItem(@event.Entity as DOOutInvoice, true); } public static Pair SyncItem(DOOutInvoice item, bool syncLink = false) { var data = new OutInvoiceDataItem(item); if (item == null) return new Pair(data, item); data.Number = item.IEEDocNumber; data.Date = item.IEEDocDate; E365DataItemManager.Instance.PushItem(typeof(OutInvoiceDataItem), item.Uid, data, item, SR.T("Счет исходящий: {0}", item.Name)); data.PushVersion(); return new Pair(data, item); } } } Обработчик события полной синхронизации справочника. Так же используется для вызова принудительной синхронизации всех данных из хелпера: using System; using System.Linq; using EleWise.ELMA.ComponentModel; using EleWise.ELMA.Model.Services; using ITino.ELMA.E365.Common.Components; using EleWise.ELMA.Model.Managers; using ITino.ELMA.CRM.Models; namespace E365 { [Component] public class CountrySyncHandler : IForceSyncHandler { public Type Type => InterfaceActivator.TypeOf(); public bool Enabled => true; public void Process() { EntityManager.Instance.FindAll().ToList().ForEach(x => CountryListener.SyncItem(x)); } } }   Блоки в сущности Если в сущности есть блоки, то их можно мигрировать в ELMA365. Для этого нужно реализовать следующий код в точках расширения: Для реализации миграции блока необходимо определить его в коде, например: using System; using System.Collections.Generic; using ITino.ELMA.E365.Common.Models; using Newtonsoft.Json; namespace E365 { public class TestTablePartsDataItem : BaseDataItem { public override string Path => "app/_clients/testtableparts"; [JsonProperty(PropertyName = "__name")] public string Name { get; set; } [JsonIgnore] public TableDataItem TestTable { get; set;} public TestTablePartsDataItem() { TestTable = new TableDataItem("testtable", this); } } } Структура блока и его записи заполняются для передачи, например (при этом указываются простые свойства и справочники): using System; using System.Collections.Generic; using System.Linq; using EleWise.ELMA.API; using System.Text; using EleWise.ELMA.ComponentModel; using EleWise.ELMA.Runtime.NH.Listeners; using NHibernate.Event; using EleWise.ELMA.ConfigurationModel; using EleWise.ELMA.Model.Common; using ITino.ELMA.E365.Common.Components; using EleWise.ELMA.Model.Entities; using EleWise.ELMA.Extensions; using ITino.ELMA.E365.Common.Managers; using EleWise.ELMA; using ITino.ELMA.E365.Common.Models; using ITino.ELMA.E365.CRM.Listeners; using ITino.ELMA.E365.Common.Listeners; namespace E365 { [Component] public class TestTableParts_TestTableListener : PostFlushEventListener { public override void OnPostInsert(PostInsertEvent @event) { SyncItem(@event.Entity as ITestTableParts_TestTable, true); } public override void OnPostUpdate(PostUpdateEvent @event) { SyncItem(@event.Entity as ITestTableParts_TestTable, true); } public override void OnPostDelete(PostDeleteEvent @event) { SyncItem(@event.Entity as ITestTableParts_TestTable, true); } public static Pair SyncItem(ITestTableParts_TestTable item, bool syncLink = false) { return TestTablePartsListener.SyncItem(item?.Parent); } } [Component] public class TestTablePartsListener : PostFlushEventListener { public override void OnPostInsert(PostInsertEvent @event) { SyncItem(@event.Entity as ITestTableParts, true); } public override void OnPostUpdate(PostUpdateEvent @event) { SyncItem(@event.Entity as ITestTableParts, true); } public static Pair SyncItem(ITestTableParts item, bool syncLink = false) { var data = new TestTablePartsDataItem(); if (item == null) return new Pair(data, item); data.Uid = item.Uid; data.Name = item.Name; item.TestTable?.ForEach(tableItem => { var row = new RowDataItem(); row.SimpleContext.Add ("boolean", tableItem.Boolean); row.SimpleContext.Add ("string", tableItem.String); row.LinkedContext.Add ("currency", new List { new LinkImpl (CurrencyListener.SyncItem(tableItem.Currency)) }); row.LinkedContext.Add ("newuser", new List { new LinkImpl (UserListener.SyncItem(tableItem.NewUser)) }); row.SimpleContext.Add ("newnumber", tableItem.NewNumber); row.SimpleContext.Add ("date", tableItem.Data); data.TestTable.Rows.Add(row); }); E365DataItemManager.Instance.PushItem(typeof(TestTablePartsDataItem), item.Uid, data, item, SR.T("TestTableParts: {0}", item.Name)); return new Pair(data, item); } } }