I successfully migrated Bookable Resource and Work Hour Template Calendar Rules between environments using the following approach:
Firstly, I inserted the connection details for both the source and target environments. Then, I utilized Microsoft functions, starting with ‘GetCalendar,’ which retrieved the Calendar ID from the Bookable Resource and Work Hour Template. Subsequently, I passed this Calendar ID into the ‘GetCalendarRules’ function, fetching the Calendar Rules associated with it. The final steps involved two functions that I created, namely ‘FormatCalendarJSON_WorkHourTemplate’ and ‘FormatCalendarJSON_BookableResource.’ These functions formatted the JSON data to be passed into the Microsoft ‘CalendarAction’ function, responsible for creating the Calendar Rules in the target environment.
The recommended way to implement the provided code is by migrating the ‘Work Hour Template’ and ‘Bookable Resource’ tables using the ‘Power Platform Export Data’ and ‘Power Platform Import Data’ build tools in Azure DevOps Pipelines. However, it is crucial to exclude the ‘calendarid’ field from the schema to avoid potential issues. The rationale behind omitting the ‘calendarid’ field is that the code generates a new Calendar and CalendarRule in the target environment.
The only drawback is the necessity to input static Start and End times into the ‘CalendarAction,’ which may require creating separate PowerShell scripts for each ‘Bookable Resource’ and ‘Work Hour Template.’ In the provided example, I exported the working hours of Israel from my Dev environment to the target environment.
I hope the code example below proves helpful in your migration efforts.
$DevUrl = (DEVURL)
$DevClientId = (DEVCLIENT)
$DevClientsecret = (DEVCLIENTSECRET)
$DevTenantId = (DEVTENANTID)
$DevConnectionString = "AuthType=ClientSecret;url=$DevUrl;ClientId=$DevClientId;ClientSecret=$DevClientsecret"
$TargetUrl = (TARGETURL)
$TargetClientId = (TARGETCLIENT)
$TargetClientsecret = (TARGETCLIENTSECRET)
$TargetTenantId = (TARGETTENANTID)
$TargetConnectionString = "AuthType=ClientSecret;url=$TargetUrl;ClientId=$TargetClientId;ClientSecret=$TargetClientsecret"
# Login to PowerApps for the Admin commands
Write-Host "Login to PowerApps for the Admin commands"
Write-Host " "
Install-Module Microsoft.PowerApps.Administration.PowerShell -RequiredVersion "2.0.105" -Force -Scope CurrentUser
# Login to PowerApps for the Xrm.Data commands
Write-Host "Login to PowerApps for the Xrm.Data commands"
Write-Host " "
Install-Module Microsoft.Xrm.Data.PowerShell -RequiredVersion "2.8.14" -Force -Scope CurrentUser
# Connect to DEV CRM
$DevConn = Get-CrmConnection -ConnectionString $DevConnectionString
Write-Host 'Connected to' $DevConn.ConnectedOrgFriendlyName
Write-Host " "
# Connect to TARGET CRM
$TargetConn = Get-CrmConnection -ConnectionString $TargetConnectionString
Write-Host 'Connected to' $TargetConn.ConnectedOrgFriendlyName
Write-Host " "
function GetCalendar {
param (
$PluralEntityLogicalName,
$EntityId
)
Write-Host "Entered GetCalendar"
Write-Host " "
# Define the URL for the GET request
$url = $DevUrl
$apiVersion = "v9.1"
$apiEndpoint = "/api/data/$apiVersion/$PluralEntityLogicalName($EntityId)"
# Define headers if needed (e.g., for authentication)
$headers = @{
"Authorization" = "Bearer " + $DevConn.CurrentAccessToken
"OData-MaxVersion" = "4.0"
"OData-Version" = "4.0"
"Accept" = "application/json"
}
# Make the GET request
$response = Invoke-RestMethod -Uri ($url + $apiEndpoint) -Method Get -Headers $headers
# Output the response (you can handle the data as needed)
return $response
}
function GetCalendarRules {
param (
$CalendarId
)
Write-Host "Entered GetCalendarRules"
Write-Host " "
# Define the URL for the GET request
$url = $DevUrl
$apiVersion = "v9.1"
$apiEndpoint = "/api/data/$apiVersion/calendars("+$CalendarId+")?"+"$"+"expand=calendar_calendar_rules"
# Define headers if needed (e.g., for authentication)
$headers = @{
"Authorization" = "Bearer " + $DevConn.CurrentAccessToken
"OData-MaxVersion" = "4.0"
"OData-Version" = "4.0"
"Accept" = "application/json"
}
Write-Host $apiEndpoint
Write-Host " "
# Make the GET request
$response = Invoke-RestMethod -Uri ($url + $apiEndpoint) -Method Get -Headers $headers
return $response
}
function FormatCalendarJSON_WorkHourTemplate {
param (
$Calendar,
[string]$EntityLogicalName,
$CalendarRules
)
Write-Host "Entered FormatCalendarJSON_WorkHourTemplate"
Write-Host " "
$data = @{
"CalendarEventInfo" = @"
{
"CalendarId": "$($Calendar.msdyn_calendarid)",
"EntityLogicalName": "$($EntityLogicalName)",
"TimeZoneCode": $($CalendarRules.calendar_calendar_rules[0].timezonecode),
"IsVaried": false,
"RulesAndRecurrences": [
{
"Rules": [
{
"StartTime": "2023-10-12T08:00:00Z",
"EndTime": "2023-10-12T18:00:00Z",
"WorkHourType": 0,
"Effort": 1
}
],
"RecurrencePattern": "$($CalendarRules.calendar_calendar_rules[0].pattern)"
}
]
}
"@
}
$jsonString = ($data | ConvertTo-Json -Compress -Depth 10).Replace('\r\n','')
$jsonString2 = $jsonString -replace " ",""
Write-Host "JSON String - " + $jsonString2
# Call the CalendarAction function with your action and data
CalendarAction "msdyn_SaveCalendar" $jsonString2
}
function FormatCalendarJSON_BookableResource {
param (
$Calendar,
[string]$EntityLogicalName,
$CalendarRules
)
$data = @{
"CalendarEventInfo" = @"
{
"CalendarId": "$($Calendar.calendarid_Property.value[0].Id)",
"EntityLogicalName": "$($EntityLogicalName)",
"TimeZoneCode": $($CalendarRules.calendar_calendar_rules[0].timezonecode),
"IsVaried": false,
"RulesAndRecurrences": [
{
"Rules": [
{
"StartTime": "2023-10-12T08:00:00Z",
"EndTime": "2023-10-12T18:00:00Z",
"WorkHourType": 0,
"Effort": 1
}
],
"RecurrencePattern": "$($CalendarRules.calendar_calendar_rules[0].pattern)"
}
]
}
"@
}
$jsonString = ($data | ConvertTo-Json -Compress -Depth 10).Replace('\r\n','')
$jsonString2 = $jsonString -replace " ",""
<#Write-Host $($response2.calendar_calendar_rules[0].pattern)#>
Write-Host "JSON String - " + $jsonString2
# Call the CalendarAction function with your action and data
CalendarAction "msdyn_SaveCalendar" $jsonString2
}
function CalendarAction {
param (
[string]$action,
[object]$data
)
$url = $TargetUrl+"api/data/v9.0/$action"
$headers = @{
"Authorization" = "Bearer " + $TargetConn.CurrentAccessToken
"OData-MaxVersion" = "4.0"
"OData-Version" = "4.0"
"Accept" = "application/json"
"Content-Type" = "application/json; charset=utf-8"
"Prefer" = 'odata.include-annotations="*"'
}
<#$jsonBody = "'",$data,"'"#>
$jsonBody = $data | ConvertTo-Json -Depth 10
try {
$response = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $jsonBody
if ($response) {
Write-Host "Response:"
Write-Host $response
Write-Host " "
} else {
Write-Error "Error occurred."
}
}
catch {
Write-Error $_.Exception.Message
}
}
## WORK HOUR TEMPLATE ###
Write-Host "Get Work Hour Templates Calendar IDs in Dev"
Write-Host " "
$fetchWorkHourTemplatesDev = @"
<fetch>
<entity name="msdyn_workhourtemplate">
<attribute name="msdyn_workhourtemplateid" />
<attribute name="msdyn_calendarid" />
<attribute name="msdyn_name" />
<filter>
<condition attribute="msdyn_name" operator="eq" value="Israel" />
</filter>
</entity>
</fetch>
"@
$workHourTemplatesDev = (Get-CrmRecordsByFetch -conn $DevConn -Fetch $fetchWorkHourTemplatesDev -Verbose).CrmRecords
if ($workHourTemplatesDev.Count -eq 0)
{
Write-Host "No workHourTemplates found."
}
else
{
foreach ($workHourTemplateDev in $workHourTemplatesDev) {
$WorkHourTemplate_Calendar = GetCalendar "msdyn_workhourtemplates" $workHourTemplateDev.msdyn_workhourtemplateid
Write-Host "WorkHourTemplate_Calendar " + $WorkHourTemplate_Calendar
Write-Host " "
$WorkHourTemplate_CalendarRules = GetCalendarRules $WorkHourTemplate_Calendar.msdyn_calendarid
Write-Host "calendar_calendar_rules" + $WorkHourTemplate_CalendarRules.calendar_calendar_rules
Write-Host " "
Write-Host "Get Work Hour Template Calendar IDs in Target"
Write-Host " "
$fetchWorkHourTemplatesTarget= @"
<fetch>
<entity name="msdyn_workhourtemplate">
<attribute name="msdyn_workhourtemplateid" />
<attribute name="msdyn_calendarid" />
<attribute name="msdyn_name" />
<filter>
<condition attribute="msdyn_name" operator="eq" value="Israel" />
</filter>
</entity>
</fetch>
"@
$workHourTemplatesTarget = (Get-CrmRecordsByFetch -conn $TargetConn -Fetch $fetchWorkHourTemplatesTarget -Verbose).CrmRecords
if ($workHourTemplatesTarget.Count -eq 0)
{
Write-Host "No Calendar IDs found."
}
else
{
foreach ($WorkHourTemplateTarget in $workHourTemplatesTarget) {
FormatCalendarJSON_WorkHourTemplate $workHourTemplatesTarget "msdyn_workhourtemplate" $WorkHourTemplate_CalendarRules
}
}
}
}
## BOOKABLE RESOURCE ###
Write-Host "Get Bookable Resource Calendar IDs in Dev"
$fetchBookableResourcesDev = @"
<fetch>
<entity name="bookableresource">
<attribute name="bookableresourceid" />
<attribute name="calendarid" />
<filter>
<condition attribute="resourcetype" operator="eq" value="1" />
<condition attribute="name" operator="eq" value="Calendar Template - Israel" />
</filter>
</entity>
</fetch>
"@
$bookableResourcesDev = (Get-CrmRecordsByFetch -conn $DevConn -Fetch $fetchBookableResourcesDev -Verbose).CrmRecords
if ($bookableResourcesDev.Count -eq 0)
{
Write-Host "No Calendar IDs found."
}
else
{
foreach ($bookableResourceDev in $bookableResourcesDev) {
$bookableResource_Calendar = GetCalendar "bookableresources" $bookableResourceDev.bookableresourceid
Write-Host "bookableResource_Calendar " + $bookableResource_Calendar
Write-Host " "
$bookableResource_CalendarRules = GetCalendarRules $bookableResource_Calendar._calendarid_value
Write-Host "calendar_calendar_rules" + $bookableResource_CalendarRules.calendar_calendar_rules
Write-Host " "
Write-Host "Get Bookable Resource Calendar IDs in Target"
$fetchBookableResourcesTarget = @"
<fetch>
<entity name="bookableresource">
<attribute name="bookableresourceid" />
<attribute name="calendarid" />
<filter>
<condition attribute="resourcetype" operator="eq" value="1" />
<condition attribute="name" operator="eq" value="Calendar Template - Israel" />
</filter>
</entity>
</fetch>
"@
$bookableResourcesTarget = (Get-CrmRecordsByFetch -conn $TargetConn -Fetch $fetchBookableResourcesTarget -Verbose).CrmRecords
if ($bookableResourcesTarget.Count -eq 0)
{
Write-Host "No Calendar IDs found."
}
else
{
foreach ($bookableResourceTarget in $bookableResourcesTarget) {
FormatCalendarJSON_BookableResource $bookableResourceTarget "bookableresource" $bookableResource_CalendarRules
}
}
}
}