F&O Environment Creation Script

Last modified: 

02/01/26




Below is an updated script for creating environments in PPAC. This will ask some basic questions for what you’d like to create and can provision development, sandbox, and production environments for F&O.

 

<#
Interactive Power Platform environment provisioning script

Goals:
– Collect inputs to New-AdminPowerAppEnvironment using values retrieved from Power Platform
– Localize choices to the end user’s OS (suggest defaults from OS culture/region)
– Show user-friendly language names 
– Make currency selection FAST (no per-row region scans)
– Build TemplateMetadata without the user editing JSON

Docs:
– Get-AdminPowerAppEnvironmentLocations: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseTemplates: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseLanguages: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseCurrencies: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– New-AdminPowerAppEnvironment: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
#>

Set-StrictMode -Version Latest
$ErrorActionPreference = “Stop”

# —————————-
# Helpers
# —————————-

function Install-RequiredModule {
    param([Parameter(Mandatory=$true)][string]$Name)

    if (-not (Get-Module -ListAvailable -Name $Name)) {
        Write-Host “Installing module ‘$Name’…”
        Install-Module -Name $Name -Force -Scope CurrentUser
    }
    Import-Module $Name -Force
}

function Read-YesNo {
    param(
        [Parameter(Mandatory=$true)][string]$Prompt,
        [bool]$DefaultYes = $true
    )
    $suffix = if ($DefaultYes) { “[Y/n]” } else { “[y/N]” }
    while ($true) {
        $raw = Read-Host “$Prompt $suffix”
        if ([string]::IsNullOrWhiteSpace($raw)) { return $DefaultYes }
        switch ($raw.Trim().ToLowerInvariant()) {
            “y” { return $true }
            “yes” { return $true }
            “n” { return $false }
            “no” { return $false }
            default { Write-Host “Please answer Y or N.” }
        }
    }
}

function Get-ValidateSetValues {
    param(
        [Parameter(Mandatory=$true)][string]$CommandName,
        [Parameter(Mandatory=$true)][string]$ParameterName
    )

    $cmd = Get-Command $CommandName -ErrorAction SilentlyContinue
    if (-not $cmd) { return @() }

    $p = $cmd.Parameters[$ParameterName]
    if (-not $p) { return @() }

    foreach ($attr in $p.Attributes) {
        if ($attr -is [System.Management.Automation.ValidateSetAttribute]) {
            return @($attr.ValidValues)
        }
    }
    return @()
}

function Get-PropValue {
    param(
        [Parameter(Mandatory=$true)]$Object,
        [Parameter(Mandatory=$true)][string[]]$Candidates
    )
    foreach ($c in $Candidates) {
        if ($null -ne $Object.PSObject.Properties[$c]) {
            $v = $Object.$c
            if ($null -ne $v -and “$v”.Length -gt 0) { return $v }
        }
    }
    return $null
}

function Select-FromList {
    param(
        [Parameter(Mandatory=$true)][string]$Title,
        [Parameter(Mandatory=$true)][array]$Items,
        [Parameter(Mandatory=$true)][scriptblock]$ToLabel,
        [int]$DefaultIndex = 0
    )

    if (-not $Items -or $Items.Count -eq 0) {
        throw “No items available for selection: $Title”
    }

    Write-Host “”
    Write-Host $Title
    Write-Host (“-” * $Title.Length)

    for ($i=0; $i -lt $Items.Count; $i++) {
        $label = & $ToLabel $Items[$i]
        $marker = if ($i -eq $DefaultIndex) { “*” } else { ” ” }
        Write-Host (“{0} [{1}] {2}” -f $marker, $i, $label)
    }

    while ($true) {
        $raw = Read-Host (“Choose an index (Enter for default {0})” -f $DefaultIndex)
        if ([string]::IsNullOrWhiteSpace($raw)) { return $Items[$DefaultIndex] }

        $n = 0
        if ([int]::TryParse($raw, [ref]$n) -and $n -ge 0 -and $n -lt $Items.Count) {
            return $Items[$n]
        }

        Write-Host “Invalid selection. Enter a number between 0 and $($Items.Count – 1).”
    }
}

function Find-BestLocationDefaultIndex {
    param(
        [Parameter(Mandatory=$true)][array]$Locations,
        [Parameter(Mandatory=$true)][System.Globalization.RegionInfo]$Region
    )

    $iso2 = $Region.TwoLetterISORegionName.ToUpperInvariant()
    $regionEnglish = $Region.EnglishName

    $preferredNameMap = @{
        “US” = “unitedstates”
        “GB” = “unitedkingdom”
        “UK” = “unitedkingdom”
        “CA” = “canada”
        “AU” = “australia”
        “DE” = “germany”
        “FR” = “france”
        “NL” = “netherlands”
        “SE” = “sweden”
        “NO” = “norway”
        “DK” = “denmark”
        “IE” = “ireland”
        “JP” = “japan”
        “IN” = “india”
        “BR” = “brazil”
    }

    $locNameCandidate = $null
    if ($preferredNameMap.ContainsKey($iso2)) { $locNameCandidate = $preferredNameMap[$iso2] }

    for ($i=0; $i -lt $Locations.Count; $i++) {
        $ln = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationName”,”locationName”)
        if ($locNameCandidate -and $ln -and ($ln.ToString().ToLowerInvariant() -eq $locNameCandidate)) {
            return $i
        }
    }

    for ($i=0; $i -lt $Locations.Count; $i++) {
        $dn = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationDisplayName”,”DisplayName”,”locationDisplayName”,”name”)
        if ($dn -and $dn.ToString().IndexOf($regionEnglish, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
            return $i
        }
    }

    for ($i=0; $i -lt $Locations.Count; $i++) {
        $dn = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationDisplayName”,”DisplayName”,”locationDisplayName”,”name”)
        if ($dn -and $dn.ToString().IndexOf($iso2, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
            return $i
        }
    }

    return 0
}

function Find-DefaultIndexByPredicate {
    param(
        [Parameter(Mandatory=$true)][array]$Items,
        [Parameter(Mandatory=$true)][scriptblock]$Predicate
    )
    for ($i=0; $i -lt $Items.Count; $i++) {
        if (& $Predicate $Items[$i]) { return $i }
    }
    return 0
}

function Get-FriendlyCultureNameFromLcid {
    param([Parameter(Mandatory=$true)][int]$Lcid)
    try {
        $ci = [System.Globalization.CultureInfo]::GetCultureInfo($Lcid)
        return “{0} / {1} — LCID {2}” -f $ci.EnglishName, $ci.NativeName, $Lcid
    } catch {
        return “LCID $Lcid”
    }
}

function Get-LcidFromLanguageItem {
    param([Parameter(Mandatory=$true)]$Item)

    if ($Item -is [int]) { return $Item }

    $tmp = 0
    if ($Item -is [string] -and [int]::TryParse($Item, [ref]$tmp)) { return $tmp }

    $preferredProps = @(
        “LanguageCode”,”languageCode”,
        “Lcid”,”LCID”,
        “LocaleId”,”localeId”,
        “Code”,”code”,
        “Value”,”value”,
        “Id”,”id”
    )

    foreach ($p in $preferredProps) {
        if ($null -ne $Item.PSObject.Properties[$p]) {
            $v = $Item.$p
            if ($v -is [int]) { return $v }
            if ([int]::TryParse(“$v”, [ref]$tmp)) { return $tmp }
        }
    }

    foreach ($prop in $Item.PSObject.Properties) {
        $v = $prop.Value
        if ($v -is [int]) {
            if ($v -ge 1 -and $v -le 99999) { return $v }
        }
        if ([int]::TryParse(“$v”, [ref]$tmp)) {
            if ($tmp -ge 1 -and $tmp -le 99999) { return $tmp }
        }
    }

    return $null
}

function Get-LanguageFriendlyLabel {
    param([Parameter(Mandatory=$true)]$Item)
    $lcid = Get-LcidFromLanguageItem -Item $Item
    if ($null -ne $lcid) {
        return (Get-FriendlyCultureNameFromLcid -Lcid $lcid)
    }

    $nameGuess = $null
    try {
        $nameGuess = Get-PropValue -Object $Item -Candidates @(“DisplayName”,”displayName”,”Name”,”name”,”LanguageName”,”languageName”)
    } catch {}
    if ($nameGuess) { return “$nameGuess” }

    return “Unknown language (unrecognized shape)”
}

function Build-IsoCurrencyToEnglishNameMap {
    $map = @{}

    $cultures = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures)
    foreach ($c in $cultures) {
        try {
            $r = [System.Globalization.RegionInfo]::new($c.Name)
            if (-not $r) { continue }

            $iso = $r.ISOCurrencySymbol
            if ([string]::IsNullOrWhiteSpace($iso)) { continue }

            if (-not $map.ContainsKey($iso)) {
                $map[$iso] = $r.CurrencyEnglishName
            }
        } catch {
            # ignore
        }
    }

    return $map
}

function Get-CurrencyCodeFromItem {
    param([Parameter(Mandatory=$true)]$Item)

    if ($Item -is [string]) { return $Item }

    return (Get-PropValue -Object $Item -Candidates @(
        “IsoCurrencyCode”,”isoCurrencyCode”,
        “CurrencyName”,”currencyName”,
        “Name”,”name”,”Code”,”code”
    ))
}

function Get-CurrencySymbolFromItem {
    param([Parameter(Mandatory=$true)]$Item)
    if ($Item -is [string]) { return $null }
    return (Get-PropValue -Object $Item -Candidates @(“Symbol”,”symbol”,”CurrencySymbol”,”currencySymbol”))
}

# —————————-
# Main
# —————————-

$culture   = Get-Culture
$uiCulture = [System.Globalization.CultureInfo]::CurrentUICulture
$region    = [System.Globalization.RegionInfo]::new($culture.Name)

Write-Host “Detected OS Culture: $($culture.Name) / UI: $($uiCulture.Name) / Region: $($region.EnglishName) ($($region.TwoLetterISORegionName))”
Write-Host “”

$displayName = Read-Host -Prompt “Input your environment display name (e.g., ‘Contoso Dev Sandbox’)”
if ([string]::IsNullOrWhiteSpace($displayName)) { throw “DisplayName cannot be empty.” }

Install-RequiredModule -Name “Microsoft.PowerApps.Administration.PowerShell”

Write-Host “Creating a session against the Power Platform API…”
Add-PowerAppsAccount -Endpoint prod

# —– Location —–
Write-Host “Loading supported environment locations from Power Platform…”
$locations = @(Get-AdminPowerAppEnvironmentLocations)

$defaultLocationIndex = Find-BestLocationDefaultIndex -Locations $locations -Region $region

$selectedLocation = Select-FromList `
    -Title “Select LocationName (default suggested from your OS region)” `
    -Items $locations `
    -DefaultIndex $defaultLocationIndex `
    -ToLabel {
        param($loc)
        $ln = Get-PropValue -Object $loc -Candidates @(“LocationName”,”locationName”)
        $ld = Get-PropValue -Object $loc -Candidates @(“LocationDisplayName”,”LocationDisplayname”,”locationDisplayName”,”DisplayName”,”name”)
        if ($ld) { “$ld ($ln)” } else { “$ln” }
    }

$locationName = (Get-PropValue -Object $selectedLocation -Candidates @(“LocationName”,”locationName”)).ToString()

# —– SKU —–
$skuValues = Get-ValidateSetValues -CommandName “New-AdminPowerAppEnvironment” -ParameterName “EnvironmentSku”
if (-not $skuValues -or $skuValues.Count -eq 0) {
    $skuValues = @(“Sandbox”,”Production”,”Trial”)
}

$defaultSkuIndex = 0
for ($i=0; $i -lt $skuValues.Count; $i++) {
    if ($skuValues[$i].ToString().Equals(“Sandbox”, [System.StringComparison]::OrdinalIgnoreCase)) {
        $defaultSkuIndex = $i; break
    }
}

$selectedSku = Select-FromList `
    -Title “Select EnvironmentSku” `
    -Items $skuValues `
    -DefaultIndex $defaultSkuIndex `
    -ToLabel { param($s) $s.ToString() }

$environmentSku = $selectedSku.ToString()

# —– Dataverse —–
$provisionDatabase = Read-YesNo -Prompt “Provision Dataverse database?” -DefaultYes $true

$templateUniqueName = $null
$templateMetadata   = $null
$languageName       = $null
$currencyName       = $null

if ($provisionDatabase) {

    # Templates
    Write-Host “Loading Dataverse templates for location ‘$locationName’…”
    $templates = @(Get-AdminPowerAppCdsDatabaseTemplates -LocationName $locationName)

    $defaultTemplateIndex = Find-DefaultIndexByPredicate -Items $templates -Predicate {
        param($t)
        $tCode = Get-PropValue -Object $t -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)
        return ($tCode -and $tCode.ToString().Equals(“D365_FinOps_Finance”, [System.StringComparison]::OrdinalIgnoreCase))
    }

    $selectedTemplate = Select-FromList `
        -Title “Select Dataverse Template (Templates parameter)” `
        -Items $templates `
        -DefaultIndex $defaultTemplateIndex `
        -ToLabel {
            param($t)
            $tName = Get-PropValue -Object $t -Candidates @(“DisplayName”,”displayName”,”FriendlyName”,”friendlyName”)
            $tCode = Get-PropValue -Object $t -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)
            if ($tName -and $tCode -and ($tName.ToString() -ne $tCode.ToString())) { “$tName ($tCode)” }
            else { “$tCode” }
        }

    $templateUniqueName = (Get-PropValue -Object $selectedTemplate -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)).ToString()

    # Languages (friendly + robust extraction)
    Write-Host “Loading Dataverse languages for location ‘$locationName’…”
    $languages = @(Get-AdminPowerAppCdsDatabaseLanguages -LocationName $locationName)

    $osLcid = $culture.LCID
    $defaultLangIndex = Find-DefaultIndexByPredicate -Items $languages -Predicate {
        param($l)
        $lcid = Get-LcidFromLanguageItem -Item $l
        return ($null -ne $lcid -and $lcid -eq $osLcid)
    }

    $selectedLanguage = Select-FromList `
        -Title “Select Dataverse Language (friendly name shown; LCID used under the hood)” `
        -Items $languages `
        -DefaultIndex $defaultLangIndex `
        -ToLabel { param($l) Get-LanguageFriendlyLabel -Item $l }

    $selectedLcid = Get-LcidFromLanguageItem -Item $selectedLanguage
    if ($null -eq $selectedLcid) {
        throw “Could not determine LCID for selected language. Run: Get-AdminPowerAppCdsDatabaseLanguages -LocationName $locationName | Select -First 1 | Format-List *”
    }
    $languageName = $selectedLcid.ToString()

    # Currencies (FAST: one PP call + one local map build + one friendly list build)
    Write-Host “Loading Dataverse currencies for location ‘$locationName’…”
    $currenciesRaw = @(Get-AdminPowerAppCdsDatabaseCurrencies -LocationName $locationName)

    $isoNameMap = Build-IsoCurrencyToEnglishNameMap

    $currencyChoices = @()
    foreach ($c in $currenciesRaw) {
        $code = (Get-CurrencyCodeFromItem -Item $c)
        if ([string]::IsNullOrWhiteSpace($code)) { continue }

        $symbol = Get-CurrencySymbolFromItem -Item $c
        $englishName = $null
        if ($isoNameMap.ContainsKey($code)) { $englishName = $isoNameMap[$code] }

        $parts = @($code)
        if (-not [string]::IsNullOrWhiteSpace($symbol)) { $parts += $symbol }
        if (-not [string]::IsNullOrWhiteSpace($englishName)) { $parts += $englishName }

        $currencyChoices += [pscustomobject]@{
            Code  = $code
            Label = ($parts -join ” — “)
            Raw   = $c
        }
    }

    $desiredIsoCurrency = $region.ISOCurrencySymbol
    $defaultCurrencyIndex = 0
    for ($i=0; $i -lt $currencyChoices.Count; $i++) {
        if ($currencyChoices[$i].Code.Equals($desiredIsoCurrency, [System.StringComparison]::OrdinalIgnoreCase)) {
            $defaultCurrencyIndex = $i
            break
        }
    }

    $selectedCurrencyChoice = Select-FromList `
        -Title “Select Dataverse Currency (default suggested from your OS region)” `
        -Items $currencyChoices `
        -DefaultIndex $defaultCurrencyIndex `
        -ToLabel { param($x) $x.Label }

    $currencyName = $selectedCurrencyChoice.Code

    # TemplateMetadata (FinOps post-provisioning packages)
    $includeFinOpsMetadata = Read-YesNo -Prompt “Include FinOps post-provisioning package metadata (DevTools/DemoData)?” -DefaultYes $true
    if ($includeFinOpsMetadata) {
        $defaultDevTools = $environmentSku.Equals(“Sandbox”, [System.StringComparison]::OrdinalIgnoreCase)

        $devToolsEnabled = Read-YesNo -Prompt “Enable DevToolsEnabled?” -DefaultYes $defaultDevTools
        $demoDataEnabled = Read-YesNo -Prompt “Enable DemoDataEnabled?” -DefaultYes $false

        $paramString = @(
            “DevToolsEnabled=$($devToolsEnabled.ToString().ToLowerInvariant())”,
            “DemoDataEnabled=$($demoDataEnabled.ToString().ToLowerInvariant())”
        ) -join “|”

        $templateMetadataObj = @{
            PostProvisioningPackages = @(
                @{
                    applicationUniqueName = “msdyn_FinanceAndOperationsProvisioningAppAnchor”
                    parameters            = $paramString
                }
            )
        }

        $templateMetadata = ($templateMetadataObj | ConvertTo-Json -Depth 10) | ConvertFrom-Json
    }
}

# —————————-
# Execute provisioning
# —————————-

Write-Host “”
Write-Host “Provisioning new environment…”
Write-Host ”  DisplayName        : $displayName”
Write-Host ”  LocationName       : $locationName”
Write-Host ”  EnvironmentSku     : $environmentSku”
Write-Host ”  ProvisionDatabase  : $provisionDatabase”
if ($provisionDatabase) {
    Write-Host ”  Templates          : $templateUniqueName”
    Write-Host ”  Language (LCID)    : $languageName”
    Write-Host ”  Currency           : $currencyName”
    Write-Host (”  TemplateMetadata   : {0}” -f ($(if ($templateMetadata) { “included” } else { “none” })))
}

$cmdParams = @{
    DisplayName    = $displayName
    EnvironmentSku = $environmentSku
    LocationName   = $locationName
}

if ($provisionDatabase) {
    $cmdParams[“ProvisionDatabase”] = $true
    $cmdParams[“LanguageName”]      = $languageName   # LCID like 1033
    $cmdParams[“CurrencyName”]      = $currencyName   # ISO like USD

    if ($templateUniqueName) { $cmdParams[“Templates”] = @($templateUniqueName) }
    if ($templateMetadata)   { $cmdParams[“TemplateMetadata”] = $templateMetadata }
}

try {
    $result = New-AdminPowerAppEnvironment -WaitUntilFinished 0

    Write-Host “”
    Write-Host “Environment provisioning initiated successfully!”
    if ($null -ne $result) {
        Write-Host “Result:”
        $result | Format-List * | Out-String | Write-Host
    }
}
catch {
    Write-Host “”
    Write-Host “Provisioning failed:”
    Write-Host $_.Exception.Message
    throw
}
 

Original Post https://www.atomicax.com/article/fo-environment-creation-script

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Follow
Search
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...

Discover more from 365 Community Online

Subscribe now to keep reading and get access to the full archive.

Continue reading