Разработка
15
Фев
1

Массовое обновление записей с помощью PowerShell

Не нравится стандартный импорт CRM? Правильно! 🙂 мы не ищем легких путей… И попробуем настроить массовое обновление с помощью PowerShell. К тому же это можно настроить один раз и быстро использовать повторно (в т.ч. автоматизировать).
Для начала под админом запустите PowerShwell ISE и админом выполните запрос:

Set-ExecutionPolicy Unrestricted

Это позволит в принципе выполнять PowerShell-скрипты на текущей машине.

Скопируйте из SDK сборки Microsoft.Crm.Sdk.Proxy.dll и Microsoft.Xrm.Sdk.dll в папку на локальном диске, в которой будет распологаться PowerShell-скрипт.

Обновление по GUID’у

Предположим у нас есть CSV файл в таком формате:

accountid;name;address1_city;telephone1;websiteurl;preferredcontactmethodcode
7e6e0424-0d32-e511-80d1-001dd8b721fe;Alfa;Veenendaal;(555) 123456;http://localhost/alfa;Email
1838263f-0d32-e511-80d1-001dd8b721fe;Beta;Arnhem;(555) 234567;http://localhost/beta;Phone
59a45311-0e32-e511-80d1-001dd8b721fe;Delta;Amsterdam;(555) 345678;http://localhost/delta;

И мы хотим обновить в CRM записи с соответствующими GUID’ами.

Поместите файл в той же папке что и скрипт и выполните его:

# Подключаем библиотеки
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.xrm.sdk.dll")
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.crm.sdk.proxy.dll")
[void][System.Reflection.Assembly]::LoadWithPartialName("system.servicemodel")

# Подключаемся к организации
$crmServiceUrl = "https://slko.crm4.dynamics.com/XRMServices/2011/Organization.svc"
$clientCredentials = new-object System.ServiceModel.Description.ClientCredentials
$clientCredentials.UserName.UserName = 'slko@slko.onmicrosoft.com'
$clientCredentials.UserName.Password = '1qaz@WSX'
$service = new-object Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy($crmServiceUrl, $null, $clientCredentials, $null)
$service.Timeout = new-object System.Timespan(0, 10, 0)

$request = new-object Microsoft.Crm.Sdk.Messages.WhoAmIRequest
$service.Execute($request)

# Считываем содержимое файла
$accounts = Import-Csv "accounts.txt" -Delimiter ";"
$accounts | Format-Table

# Возвращаем словарь со списком значений пиклиста
function GetOptionSet([string]$entityName, [string]$attributeName)
{
    # Формируем запрос к метаданным
    $metadataRequest = New-Object Microsoft.Xrm.Sdk.Messages.RetrieveAttributeRequest
    $metadataRequest.EntityLogicalName = $entityName
    $metadataRequest.LogicalName = $attributeName
    $metadataRequest.RetrieveAsIfPublished = $true
    $metadataResponse = $service.Execute($metadataRequest)

    # Помещаем каждое знаечние пиклиста в словарь
    $dict = @{ };
    $metadataResponse.AttributeMetadata.OptionSet.Options | ForEach-Object {
        $label = $_.Label.UserLocalizedLabel.Label
        $value = $_.Value
        $dict.Add($label, $value)
    }

    # Дефолтное значение помечяем пустой строкой
    $dict.Add("", $metadataResponse.AttributeMetadata.DefaultFormValue)

    return $dict
}

# Запрашиваем и выводим значения пиклиста
$contactmethods = GetOptionSet "account" "preferredcontactmethodcode"
$contactmethods


# Обновляем записи CRM
$accounts | Foreach-Object {
    $entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
    $entity.Id = [Guid]::Parse($_.accountid)
    $entity.Attributes["address1_city"] = $_.address1_city
    $entity.Attributes["telephone1"] = $_.telephone1
    $entity.Attributes["websiteurl"] = $_.websiteurl
    # Ищем в словаре соответствие для пиклиста
    if ($_.preferredcontactmethodcode)
    {
        $contactMethodId = $contactMethods[$_.preferredcontactmethodcode]
        if($contactMethodId)
        {    
            $contactMethodOption = new-object Microsoft.Xrm.Sdk.OptionSetValue($contactMethodId)
            $entity.Attributes["preferredcontactmethodcode"] = [Microsoft.Xrm.Sdk.OptionSetValue]$contactMethodOption
        }
    }
    Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
    $service.Update($entity)
}

Анализ:

  • Подключаем SDK-сборки.
  • Подключаемся к CRM.
    З.Ы. Для сценария IFD Вы должны определить свое имя пользователя и пароль в ClientCredentials, тогда как локальном развертывании Вам система по умолчанию автоматически подтянет текущие учетные данные.
  • Тестируем соединение посредством запроса WhoAmIRequest.
  • Загружаем CSV-файл в переменную и выводим в отформатированном виде.
  • Формируем промежуточную функцию, которая возвращает словарь, состоящий из текстового описания и кода пиклиста. Она необходим, чтобы иметь возможность указать в импортируемом файле отображаемое значение пиклиста и сопоставить его при импорте с кодом пиклиста. Если У Вас в импортируемом файле уже содержится корректное значение кода, то можете ее пропустить.
  • По очереди обновляем записи по их GUID’у.


Обновление по текстовому полю

Не часто при обновлении у нас имеется идентификатор записи. Чаще встречается случай, когда нам сначала нужно найти запись, а затем ее обновить. Предположим, что комбинация название организации + город уникальны и мы сначала запись по этой комбинации, а затем обновляем.

В этом случае CSV файл будет иметь такой вид:

name;address1_city;telephone1;websiteurl;preferredcontactmethodcode
Alfa;Veenendaal;(555) 123456;http://localhost/alfa;Email
Beta;Arnhem;(555) 234567;http://localhost/beta;Phone
Delta;Amsterdam;(555) 345678;http://localhost/delta;

Скрипт в этом случае будет таким:

Вариант #1

# Подключаем библиотеки
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.xrm.sdk.dll")
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.crm.sdk.proxy.dll")
[void][System.Reflection.Assembly]::LoadWithPartialName("system.servicemodel")

# Подключаемся к организации
$crmServiceUrl = "https://slko.crm4.dynamics.com/XRMServices/2011/Organization.svc"
$clientCredentials = new-object System.ServiceModel.Description.ClientCredentials
$clientCredentials.UserName.UserName = 'slko@slko.onmicrosoft.com'
$clientCredentials.UserName.Password = '1qaz@WSX'
$service = new-object Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy($crmServiceUrl, $null, $clientCredentials, $null)
$service.Timeout = new-object System.Timespan(0, 10, 0)

$request = new-object Microsoft.Crm.Sdk.Messages.WhoAmIRequest
$service.Execute($request)

# Считываем содержимое файла
$accounts = Import-Csv "accounts2.txt" -Delimiter ";"
$accounts | Format-Table

# Возвращаем записи Организации с двумя полями
$query = new-object Microsoft.Xrm.Sdk.Query.QueryExpression("account")
$query.ColumnSet = new-object Microsoft.Xrm.Sdk.Query.ColumnSet("name", "address1_city")

# RetrieveMultiple returns a maximum of 5000 records by default. 
# If you need more, use the response's PagingCookie.
$response = $service.RetrieveMultiple($query)

$idmap = @{ }
$response.Entities | ForEach-Object {
    $key = ("{0}|{1}" -f @($_.Attributes["name"], $_.Attributes["address1_city"]))
    $idmap.Add($key, $_.Id)
}

$accounts | Foreach-Object {
    # Lookup the accountid based on the Account's name and city.
    $key = ("{0}|{1}" -f @($_.name, $_.address1_city))
    $id = $idmap[$key]
    
    # Обновляем записи CRM
    if($id)
    {
        $entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
        $entity.Id = $id
        $entity.Attributes["telephone1"] = $_.telephone1
        $entity.Attributes["websiteurl"] = $_.websiteurl

        Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
        $service.Update($entity)
    }
    else
    {
        Write-Output ('Couldn''t determine id for "{0}" in "{1}", skipping it...' -f $_.name, $_.address1_city)
    }
} 

Анализ:

  • Как и в предыдущем примере сначала подключаем сборки, подключаемся к CRM и считываем содержимое файлика.
  • Далее запрашиваем все имеющиеся Организации в CRM (с двумя полями).
  • Затем, из возвращенного результата создаем нечувствительный к регистру словарь, который содержит соответствие между идентификатором записи и комбинацией название + город.
  • Затем проходимся по получившемуся словарю и ищем в нем соответствие (по комбинации название + город) записям из импортируемого файлика. Если соответствие найдено – обновляем по GUID’у запись в CRM.

Вариант #2

# Подключаем библиотеки
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.xrm.sdk.dll")
[void][System.Reflection.Assembly]::LoadFile("$pwd\microsoft.crm.sdk.proxy.dll")
[void][System.Reflection.Assembly]::LoadWithPartialName("system.servicemodel")

# Подключаемся к организации
$crmServiceUrl = "https://slko.crm4.dynamics.com/XRMServices/2011/Organization.svc"
$clientCredentials = new-object System.ServiceModel.Description.ClientCredentials
$clientCredentials.UserName.UserName = 'slko@slko.onmicrosoft.com'
$clientCredentials.UserName.Password = '1qaz@WSX'
$service = new-object Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy($crmServiceUrl, $null, $clientCredentials, $null)
$service.Timeout = new-object System.Timespan(0, 10, 0)

$request = new-object Microsoft.Crm.Sdk.Messages.WhoAmIRequest
$service.Execute($request)

# Считываем содержимое файла
$accounts = Import-Csv "accounts2.txt" -Delimiter ";"
$accounts | Format-Table

$accounts | Foreach-Object {
    # Возвращаем записи Организации с двумя полями
    $query = new-object Microsoft.Xrm.Sdk.Query.QueryExpression("account")
    $query.TopCount = 1
    $query.ColumnSet = new-object Microsoft.Xrm.Sdk.Query.ColumnSet("accountid")
    $query.Criteria.AddCondition("name", [Microsoft.Xrm.Sdk.Query.ConditionOperator]::Equal, $_.name)
    $query.Criteria.AddCondition("address1_city", [Microsoft.Xrm.Sdk.Query.ConditionOperator]::Equal, $_.address1_city)
   
    $response = $service.RetrieveMultiple($query)

    if ($response.Entities[0])
    {
        $entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
        $entity.Id = $response.Entities[0].Id
        $entity.Attributes["telephone1"] = $_.telephone1
        $entity.Attributes["websiteurl"] = $_.websiteurl

        Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
        $service.Update($entity)
    }
    else
    {
        Write-Output ('Couldn''t determine id for "{0}" in "{1}", skipping it...' -f $_.name, $_.address1_city)
    }
} 

Анализ: в отличие от первого варианта мы тут не формируем словарь со списком всех Организаций, а последовательно, для каждой строки импортируемого файла, запрашиваем соответствие ей в CRM. И если таковая найдена – обновляем ее.

И тот и другой вариант имеет право на существование. Первый дает меньшую нагрузку на систему из-за меньшего количества запросов, но может вызвать проблемы при большом количестве Организаций (всех их придется поместить в словарь). Второй меньше занимает оперативной памяти, но генерирует по два запроса на строку импортируемого файла.

Комментарии (1)
  • Maksim_Klyu 15.02.2017
    #Пример синхронизации с AD (on-premise)
    #https://github.com/seanmcne/Microsoft.Xrm.Data.PowerShell
    
    if ($conn -eq $null) {
        $OrganizationName = 'org1'
        $ServerUrl = "https://$($OrganizationName).crm.domain_test.ru"
        $conn = Connect-CrmOnPremDiscovery -ServerUrl $ServerUrl -OrganizationName $OrganizationName -Verbose
        if ($conn -eq $null) {
            break;
        }
        ################################
        #-SearchBase 'OU=CRM_Users,DC=test2,DC=domain_test,DC=ru'
        $ADUsers = Get-ADUser -Filter * -Properties GivenName, Initials, sn, Title, Manager #,thumbnailPhoto
        ################################
    } 
    Write-Host "[ii]" $conn.CrmConnectOrgUriActual
    
    $domain = 'domain_test'
    $CrmRecords = (Get-CrmRecords -conn $conn -EntityLogicalName systemuser -Fields domainname, parentsystemuserid -AllRows).CrmRecords #,entityimage,firstname,middlename,lastname,title
    
    foreach ($CrmRecord in $CrmRecords) {
        
        $systemuserid = $CrmRecord.systemuserid
    
        if($CrmRecord.domainname -ne '') {
    
            $ADUser = $ADUsers | Where-Object {$_.SamAccountName -eq $CrmRecord.domainname.ToLower().Replace("$domain\",'')}
    
            #$ADUser
            #$CrmRecord
    
            $Fields = @{}
            $CRMManager = $null
            $ADManager = $null
    
            if($ADUser -ne $null) {
    
            try {
                if ($ADUser.GivenName -ne $null) {
                    #$CrmRecord.firstname = $ADUser.GivenName
                    $Fields += @{'firstname' = $ADUser.GivenName}
                }
                if ($ADUser.Initials -ne $null) {
                    #$CrmRecord.middlename = $ADUser.Initials
                    $Fields += @{'middlename' = $ADUser.Initials[0] + '.'}
                }
                if ($ADUser.sn -ne $null) {
                    #$CrmRecord.lastname = $ADUser.sn
                    $Fields += @{'lastname' = $ADUser.sn}
                }
                if ($ADUser.Title -ne $null) {
                    #$CrmRecord.title = $ADUser.Title
                    $Fields += @{'title' = $ADUser.Title}
                }
                
    
                
    
                ###__SET
                if ($Fields.Count -gt 0) {
                    Set-CrmRecord -conn $conn -EntityLogicalName systemuser -Id $systemuserid.Guid -Fields $Fields -ErrorAction Continue
                }
    
                Write-Host "[OK] Sync:" $CrmRecord.domainname [$systemuserid] -ForegroundColor Green
    
                } catch {
                    Write-Host "[!!] Sync:" $CrmRecord.domainname [$systemuserid]"`n" $_.Exception -ForegroundColor Red
                    return
    
                }
                #REPORTS      
                foreach ($Field in $Fields.GetEnumerator()) {
                    Write-Host ("`t{0,-19} {1}" -f $Field.Name, $Field.Value)
                }
                
            } else {
                Write-Host "[??] Cannot find domainname in AD" $CrmRecord.domainname [$systemuserid] -ForegroundColor Magenta
            }
    
        } else {
            Write-Host "[??] Cannot find systemuserid in CRM " $systemuserid -ForegroundColor Yellow
        }
    }
    #EOF
    

*

code