...

Миграция данных из Creatio (Terrasoft) в ELMA365

Тема в разделе "Данные: импорт, экспорт, миграция", создана пользователем ava_var, 29 апр 2022.

  1. ava_var

    ava_var Активный участник

    В этой статье рассмотрим, как мигрировать исторические данные из продукта Creatio (решение от Terrasoft).
    Для интеграции двух систем будем использовать протокол OData.
    Настроим Creatio
    Для интеграции будет использовать протокол OData
    Назначение протокола OData — выполнение запросов от внешних приложений к серверу баз данных Creatio.
    Приложение Creatio поддерживает протоколы OData 4 и OData 3. OData 4 предоставляет больше возможностей, чем OData 3. Основное отличие протоколов — ответ на запрос, возвращаемый сервером, имеет разный формат данных. Различия протоколов OData 3 и OData 4 описаны в официальной документации OData. При планировании интеграции с Creatio по протоколу OData необходимо использовать протокол версии 4.
    Ограничения при использовании протокола OData
    При использовании протокола OData необходимо учитывать следующие ограничения:
    • Невозможно создать системных пользователей.
    • Невозможно задать культуру возвращаемых данных. Культура определяется культурой пользователя от имени которого выполняется запрос.
    • Тело ответа на запрос может содержать максимум 20 000 строк.
    • Пакетный запрос может содержать максимум 100 подзапросов.
    • Максимальный размер файла, который можно загрузить с помощью запроса, задается системной настройкой [ MaxFileSize ] (по умолчанию — 10 Мб).

    Авторизация в Creatio
    Все внешние запросы к приложению Creatio должны быть аутентифицированы.
    Запросы должны выполняться под учетной записью, у которой есть права на OData.
    Права можно выставить следующим образом: перейти в настройки через правую панель.
    Затем пройти по пути Open System designer –> Users and administration –> Operation permissions –> Access to OData.
    Код:
    
    async function auththorize(): Promise<void> {
        try {
            const 
    resp = await fetch(`${baseUrl}/ServiceModel/AuthService.svc/Login`, {
                
    method: 'POST',
                
    headers: {
                    
    "ForceUseSession": "true",
                    
    "Content-Type": "application/json"
                
    },
                
    body: JSON.stringify({
                    
    "UserName": login,
                    
    "UserPassword": password
                
    })
            });
            const 
    res = await resp.json();
            if (
    res.Code !== 0) {
                throw new 
    Error("authorization error: " + res.Message);
            }
            const 
    cookieHeader = resp.headers.get("set-cookie");
            if (!
    cookieHeader) {
                throw new 
    Error("auth headers not found");
            }
            
    await Namespace.storage.setItem("BPMCSRF_cookie", cookieHeader);
            const 
    start = cookieHeader.indexOf("BPMCSRF");
            const 
    end = cookieHeader.indexOf(";", start);
            const 
    csrf = cookieHeader.substring(start + 8, end);
            
    await Namespace.storage.setItem("BPMCSRF", csrf);
        } catch (
    e) {
            throw 
    e;
        }
    }
    Рекомендуемым способом для запросов по протоколу OData является аутентификация на основе cookies (Forms-аутентификация), которая реализована с помощью веб-сервиса AuthService.svc.
    Первым запросом получаем cookie, затем добавляем в заголовки при последующих запросах
    Доступ к данным в Creatio
    Все доступные сущности Creatio можно получить по эндпоинту: ${baseUrl}/odata/
    Запрос к конкретной таблице формирует по протоколу OData, например к таблице Account: ${baseUrl}/odata/Account?$expand=Industry($select=Name),Country($select=Name),City($select=Name)&$top=50&$skip=0 - дополнительно к ответу с помощью $expand добавляются данные из таблиц Industry, Country и City, $select ограничивает список начитываемых полей.
    Код:
    
    async function getData(schema: string): Promise<Record<string, any>[] | undefined> {
        const 
    url = `${baseUrl}/odata/${schema}`;
        const 
    data = {
            
    headers: {
                
    "Accept": "application/json",
                
    "Content-Type": "application/json;odata=verbose",
                
    "ForceUseSession": "true",
                
    "BPMCSRF": await Namespace.storage.getItem("BPMCSRF"),
                
    "Cookie": await Namespace.storage.getItem("BPMCSRF_cookie")
            },
        };
        try {
            const 
    resp = await fetch(url, data as FetchRequest);
            if (
    resp.status === 500) {
                if (!
    Context.data.is500err) {
                    
    Context.data.is500err = true;
                    return;
                } else {
                    throw new 
    Error(resp.status + ": " + resp.statusText)
                }
            }
            if (
    resp.status !== 200) {
                throw new 
    Error(resp.status + ": " + resp.statusText)
            }
            
    Context.data.is500err = false;
            const 
    res = await resp.json();
            
    Context.data.createdCount! += res.value.length;
            return 
    res.value;
        } catch (
    e) {
            throw 
    e;
        }
    }
    Особенности при работе c Creatio
    1. Если получение данных пакетами выполняется достаточно долго, то велика вероятность, что cookie станут невалидными, и вы получите ошибку 500. Отслеживайте это и выполняйте повторно команду авторизации.
    2. Если таблица содержит файл, при запросе к ней в ответе будет поле с названием типа Data@odata.mediaReadLink, в котором будет ссылка на файл, который можно получить по пути ${baseUrl}/odata/${row[‘Data@odata.mediaReadLink’]} (тоже с добавлением BPMCSRF и Cookie в заголовок).
    Код:
    
    const resp = await fetch(`${baseUrl}/odata/${row["Data@odata.mediaReadLink"]}`, {
      
    headers: {
        
    "ForceUseSession": "true",
        
    "BPMCSRF": await Namespace.storage.getItem("BPMCSRF"),
        
    "Cookie": await Namespace.storage.getItem("BPMCSRF_cookie")
       }
    } as 
    FetchRequest);
    const 
    file = await Context.fields.files.create(row.Name, await resp.arrayBuffer());
    Пример миграции контактов и компаний с сохранением связей.
    В Creatio контакт имеет ссылку на компанию AccountId. Поэтому мы сначала считаем данные о компаниях, затем уже о контактах.
    Во время получения данных о компаниях в объект creatioAccountsIdsToElmaId будем сохранять сопоставления id компании в Creatio и id компании в ELMA365. Аналогично сделаем для сохранения связи между компаниями и отраслями в объекте industriesCreatioToElma.
    Получение компаний и контактов идет пакетами, так как в системе ELMA365 есть ограничение на время выполнения сценария. Будем ходить по циклу Шлюз – Блок transferAccounts до тех пор, пока контекстная переменная createdEnd не станет равна true.


    [​IMG]

    Листинг сценария бизнес-процесса
    Код:
    
    const baseUrl = 'http://192.168.10.210:5389';
    const 
    login = "Supervisor";
    const 
    password = "Supervisor";
    let creatioAccountsIdsToElmaId: Record<string, string>;
    let industriesCreatioToElma: Record<string, string>;
    // TYPES..
    // Размеры пакетов настраиваются эмпирическим путем
    const accountBatchSize = 60;
    const 
    contactBatchSize = 70;
    function 
    initMaps() {
        
    creatioAccountsIdsToElmaId = Context.data.creatioAccountsIdsToElmaStr ? JSON.parse(Context.data.creatioAccountsIdsToElmaStr) : {};
        
    industriesCreatioToElma = Context.data.industriesCreatioToElmaStr ? JSON.parse(Context.data.industriesCreatioToElmaStr) : {};
    }
    async function transferAccounts(): Promise<void> {
        const 
    accountsSchema = `Account?$expand=Industry($select=Name),Country($select=Name),City($select=Name)&$top=${accountBatchSize}&$skip=${Context.data.createdCount}`;
        try {
            
    Context.data.iterationsCounter!++;
            
    initMaps();
            const 
    accounts = await getData(accountsSchema);
            
    // при 500 попробуем еще раз
            
    if (!Context.data.is500err) {
                if (
    accounts && accounts.length) {
                    
    await parseAccountsToElma(accounts as Account[]);
                } else {
                    
    Context.data.createdEnd = true;
                }
                
    Context.data.creatioAccountsIdsToElmaStr = JSON.stringify(creatioAccountsIdsToElmaId);
                
    Context.data.industriesCreatioToElmaStr = JSON.stringify(industriesCreatioToElma);
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Миграция контактов
    async function transferContacts(): Promise<void> {
        const 
    contactsSchema = `Contact?$top=${contactBatchSize}&$skip=${Context.data.createdCount}`;
        try {
            
    Context.data.iterationsCounter!++;
            
    initMaps();
            const 
    contacts = await getData(contactsSchema);
            if (!
    Context.data.is500err) {
                if (
    contacts && contacts.length) {
                    
    await parseContactsToElma(contacts as Contact[]);
                } else {
                    
    Context.data.createdEnd = true;
                }
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Конвертируем контакты из Creatio в ELMA365
    async function parseContactsToElma(rows: Contact[]): Promise<void> {
        try {
            for (const 
    row of rows) {
                const 
    contact = Context.fields.contact.app.create();
                
    contact.data.__name = row.Name;
                
    contact.data._account = [];
                if (
    row.Facebook) {
                    
    contact.data._account.push({ login: row.Facebook } as TAccount<AccountType.Facebook>);
                }
                if (
    row.LinkedIn) {
                    
    contact.data._account.push({ login: row.LinkedIn } as TAccount<AccountType.Other>);
                }
                if (
    row.Twitter) {
                    
    contact.data._account.push({ login: row.Twitter } as TAccount<AccountType.Other>);
                }
                if (
    row.Skype) {
                    
    contact.data._account.push({ login: row.Skype } as TAccount<AccountType.Skype>);
                    
    contact.data._skype = row.Skype;
                }
                if (
    row.Email) {
                    
    contact.data._email = { email: row.Email, type: EmailType.Work };
                }
                
    contact.data._fullname = { firstname: row.Name, lastname: row.Surname, middlename: row.MiddleName };
                
    contact.data._phone = [];
                if (
    row.Phone) {
                    
    contact.data._phone.push({ tel: row.Phone.replace(/-|\s/g, ""), type: PhoneType.Main });
                }
                if (
    row.MobilePhone) {
                    
    contact.data._phone.push({ tel: row.MobilePhone.replace(/-|\s/g, ""), type: PhoneType.Mobile });
                }
                if (
    row.HomePhone) {
                    
    contact.data._phone.push({ tel: row.HomePhone.replace(/-|\s/g, ""), type: PhoneType.Home });
                }
                
    contact.data._position = row.JobTitle;
                if (
    row.AccountId !== "") {
                    const 
    elmaCompanyId = creatioAccountsIdsToElmaId[row.AccountId];
                    if (
    elmaCompanyId) {
                        const 
    elmaCompany = await Context.fields.company.app.search().where(f => f.__id.eq(elmaCompanyId)).first();
                        if (
    elmaCompany) {
                            
    contact.data._companies = elmaCompany;
                        }
                    }
                }
                
    await contact.save();
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Конвертируем компании из Creatio в ELMA365
    async function parseAccountsToElma(rows: Account[]): Promise<void> {
        try {
            for (const 
    row of rows) {
                const 
    company = Context.fields.company.app.create();
                
    company.data.name = row.Name;
                if (
    row.IndustryId !== "") {
                    const 
    id = industriesCreatioToElma[row.IndustryId];
                    
    let industry: ApplicationItem<Application$_clients$otrasl$Data, Application$_clients$otrasl$Params> | undefined = undefined;
                    if (
    id) {
                        
    industry = await Context.fields.otrasl.app.search().where(f => f.__id.eq(id)).first();
                    }
                    if (!
    industry) {
                        
    industry = Context.fields.otrasl.app.create();
                        
    industry.data.__name = row.Industry.Name;
                        
    await industry.save();
                    }
                    
    company.data.otrasl = industry;
                    
    industriesCreatioToElma[row.IndustryId] = industry.id;
                }
                
    company.data._address = "";
                if (
    row.Country) {
                    
    company.data._address += `Country: ${row.Country.Name}; `;
                }
                if (
    row.City) {
                    
    company.data._address += `City: ${row.City.Name}; `;
                }
                if (
    row.Address) {
                    
    company.data._address += `Address: ${row.Address}; `;
                }
                if (
    row.Phone) {
                    
    company.data._phone = { tel: row.Phone.replace(/-|\s/g, ""), type: PhoneType.Work };
                }
                
    company.data._website = row.Web;
                
    await company.save();
                
    creatioAccountsIdsToElmaId[row.Id] = company.id;
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Команда авторизации в Creation
    async function auththorize(): Promise<void> {
        try {
            const 
    resp = await fetch(`${baseUrl}/ServiceModel/AuthService.svc/Login`, {
                
    method: 'POST',
                
    headers: {
                    
    "ForceUseSession": "true",
                    
    "Content-Type": "application/json"
                
    },
                
    body: JSON.stringify({
                    
    "UserName": login,
                    
    "UserPassword": password
                
    })
            });
            const 
    res = await resp.json();
            if (
    res.Code !== 0) {
                throw new 
    Error("authorization error: " + res.Message);
            }
            const 
    cookieHeader = resp.headers.get("set-cookie");
            if (!
    cookieHeader) {
                throw new 
    Error("auth headers not found");
            }
            
    // Сохраняем куки, чтоб использовать их в след.запросах 
            
    await Namespace.storage.setItem("BPMCSRF_cookie", cookieHeader);
            const 
    start = cookieHeader.indexOf("BPMCSRF");
            const 
    end = cookieHeader.indexOf(";", start);
            const 
    csrf = cookieHeader.substring(start + 8, end);
            
    await Namespace.storage.setItem("BPMCSRF", csrf);
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Универсальный метод, чтоб получать данные из Creatio
    // настройки берутся из схемы
    async function getData(schema: string): Promise<Record<string, any>[] | undefined> {
        const 
    url = `${baseUrl}/odata/${schema}`;
        const 
    data = {
            
    headers: {
                
    "Accept": "application/json",
                
    "Content-Type": "application/json;odata=verbose",
                
    "ForceUseSession": "true",
                
    "BPMCSRF": await Namespace.storage.getItem("BPMCSRF"),
                
    "Cookie": await Namespace.storage.getItem("BPMCSRF_cookie")
            },
        };
        try {
            const 
    resp = await fetch(url, data as FetchRequest);
            if (
    resp.status === 500) {
                if (!
    Context.data.is500err) {
                    
    Context.data.is500err = true;
                    return;
                } else {
                    throw new 
    Error(resp.status + ": " + resp.statusText)
                }
            }
            if (
    resp.status !== 200) {
                throw new 
    Error(resp.status + ": " + resp.statusText)
            }
            
    Context.data.is500err = false;
            const 
    res = await resp.json();
            
    Context.data.createdCount! += res.value.length;
            return 
    res.value;
        } catch (
    e) {
            throw 
    e;
        }
    }
    async function resetAllParams(): Promise<void> {
        
    Context.data.createdCount = 0;
        
    Context.data.createdEnd = false;
        
    Context.data.iterationsCounter = 0;
        
    Context.data.is500err = false;
    }
    async function resetCounter(): Promise<void> {
        
    Context.data.iterationsCounter = 0;
    }
    Последнее редактирование модератором: 30 июл 2025
  2. dev

    dev Новичок

    Здравствуйте, спасибо вам за статью, а вы случайно не разобрались как получить список System Users из Creatio? А так же вложения к Accounts?