diff --git a/README.md b/README.md index cbe7f12..27b9c50 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Authentication: Org and metadata: - **[`sf-org-create` / `sf-org-create.ps1`](#sf-org-create)** - Smart scratch org creation -- **[`sf-org-lic`](#sf-org-lic)** - Salesforce org license utilization report +- **[`sf-org-lic` / `sf-org-lic.ps1`](#sf-org-lic--sf-org-licps1)** - Salesforce org license utilization report - **[`sf-org-info` / `sf-org-info.ps1`](#sf-org-info--sf-org-infops1)** - Quick org info, limits, and context - **[`sf-retrieve` / `sf-retrieve.ps1`](#sf-retrieve--sf-retrieveps1)** - Streamlined metadata retrieval (types, manifest, packages) @@ -176,7 +176,7 @@ sf-org-create -al TestOrg -dd 5 -nn --- -### [🏠](#salesforce-cli-wrapper-scripts) sf-org-lic +### [🏠](#salesforce-cli-wrapper-scripts) sf-org-lic / sf-org-lic.ps1 Generate comprehensive Salesforce license utilization reports. @@ -184,6 +184,9 @@ Generate comprehensive Salesforce license utilization reports. ```bash sf-org-lic -to ORG [-hp] ``` +```powershell +sf-org-lic.ps1 -to "PROD-ORG" +``` **Options:** - `-to` - Target org alias or username (required) diff --git a/sf-org-lic.ps1 b/sf-org-lic.ps1 new file mode 100644 index 0000000..54cb066 --- /dev/null +++ b/sf-org-lic.ps1 @@ -0,0 +1,249 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Salesforce org license utilization reporting tool + +.DESCRIPTION + Generate comprehensive Salesforce license utilization reports for an org. + Reports on User Licenses and Permission Set Licenses with detailed totals + and professional formatting suitable for license audits. + +.PARAMETER to + Target org alias or username (required) + +.PARAMETER hp + Show this help message + +.EXAMPLE + .\sf-org-lic.ps1 -to "PROD-ORG" + Generate license report for production org + +.EXAMPLE + .\sf-org-lic.ps1 -to "admin@company.com" + Generate report for specific user + +.EXAMPLE + .\sf-org-lic.ps1 -hp + Show help message + +.NOTES + This script requires: + - Salesforce CLI (sf) + - PowerShell 5.1+ or PowerShell Core + - Valid authentication to the target org + + Features: + - User Licenses - Core Salesforce user license utilization + - Permission Set Licenses - Add-on feature license usage + - Comprehensive totals - Overall usage summary with remaining capacity + - Clean formatting - Professional tabular output + - Error handling - Clear messages for invalid orgs with suggestions +#> + +param( + [Parameter(Mandatory=$false)] + [string]$to = "", + [switch]$hp +) + +function Show-Help { + Get-Help $MyInvocation.MyCommand.Path -Detailed +} + +function Write-Error-And-Exit { + param([string]$Message, [int]$ExitCode = 1) + Write-Host "Error: $Message" -ForegroundColor Red + exit $ExitCode +} + +# Show help if requested or if no org specified +if ($hp) { + Show-Help + exit 0 +} + +if ($to -eq "") { + Write-Host "Usage: .\sf-org-lic.ps1 -to " -ForegroundColor Yellow + Write-Host "" + Write-Host "Generate Salesforce license utilization report for an org" + Write-Host "" + Write-Host "Examples:" + Write-Host " .\sf-org-lic.ps1 -to PROD-ORG" + Write-Host " .\sf-org-lic.ps1 -to dev@company.com" + Write-Host " .\sf-org-lic.ps1 -hp" + exit 1 +} + +# Check for required dependencies +try { + Get-Command sf -ErrorAction Stop | Out-Null +} +catch { + Write-Error-And-Exit "'sf' CLI is required but not found. Please install Salesforce CLI." 1 +} + +# Validate org exists and is authorized +Write-Host "Validating org: $to..." -ForegroundColor Yellow +try { + $orgCheckResult = & sf org display --target-org $to --json 2>&1 + if ($LASTEXITCODE -ne 0) { + # Try to get available orgs for suggestions + try { + $orgListResult = & sf org list --json 2>$null + if ($LASTEXITCODE -eq 0) { + $orgList = $orgListResult | ConvertFrom-Json + $availableOrgs = $orgList.result.other | ForEach-Object { $_.alias } | Where-Object { $_ -ne $null } | Sort-Object + $orgSuggestions = $availableOrgs -join ", " + if ($orgSuggestions -eq "") { $orgSuggestions = "none" } + } + else { + $orgSuggestions = "none" + } + } + catch { + $orgSuggestions = "none" + } + Write-Error-And-Exit "org alias '$to' not found or not authorized. Available orgs: $orgSuggestions" 2 + } +} +catch { + Write-Error-And-Exit "Failed to validate org: $_" 2 +} + +# Helper function to run SOQL queries with error handling +function Invoke-SafeSOQLQuery { + param([string]$Query) + + try { + $result = & sf data query --target-org $to --json --query $Query 2>&1 + if ($LASTEXITCODE -ne 0) { + return $null + } + + $jsonResult = $result | ConvertFrom-Json + + # Check if result contains error information + if ($jsonResult.name -or $jsonResult.error -or ($jsonResult.message -and ($jsonResult.commandName -or $jsonResult.status))) { + $errorMsg = $jsonResult.message -or $jsonResult.error -or "Unknown error" + + # Check if it's a "not supported" error (non-fatal) + if ($errorMsg -like "*not supported*" -or $errorMsg -like "*does not exist*" -or $errorMsg -like "*INVALID_TYPE*") { + return "NOT_AVAILABLE" + } + + Write-Error-And-Exit "SOQL query failed: $errorMsg" 4 + } + + return $jsonResult + } + catch { + return $null + } +} + +# Helper function to format table output +function Format-LicenseTable { + param( + [object]$JsonData, + [string[]]$Headers, + [string]$LicenseType + ) + + $records = $JsonData.result.records + if (-not $records) { $records = $JsonData.records } + if (-not $records) { $records = @() } + + # Print headers + Write-Host ($Headers -join "`t") -ForegroundColor Cyan + Write-Host ("-" * ($Headers -join "`t").Length) -ForegroundColor Cyan + + if ($records.Count -eq 0) { + Write-Host "(no rows)" + Write-Host "" + return @{ Total = 0; Used = 0 } + } + + $totalLicenses = 0 + $totalUsed = 0 + + foreach ($record in $records) { + if ($LicenseType -eq "User") { + $total = [int]($record.TotalLicenses -or 0) + $used = [int]($record.UsedLicenses -or 0) + $remaining = $total - $used + Write-Host "$($record.Name)`t$total`t$used`t$remaining" + } + elseif ($LicenseType -eq "PSL") { + $total = [int]($record.TotalLicenses -or 0) + $used = [int]($record.UsedLicenses -or 0) + $remaining = $total - $used + Write-Host "$($record.MasterLabel)`t$($record.DeveloperName)`t$total`t$used`t$remaining" + } + + $totalLicenses += $total + $totalUsed += $used + } + + Write-Host "" + $remaining = $totalLicenses - $totalUsed + Write-Host "Totals: Total=$totalLicenses Used=$totalUsed Remaining=$remaining" -ForegroundColor Green + Write-Host "" + + return @{ Total = $totalLicenses; Used = $totalUsed } +} + +# Main execution +Write-Host "" +Write-Host "Salesforce License Utilization — $to" -ForegroundColor White -BackgroundColor Blue +Write-Host "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Gray +Write-Host "" + +# SOQL queries for the two main license types +$userLicenseQuery = "SELECT Id, Name, TotalLicenses, UsedLicenses FROM UserLicense ORDER BY Name LIMIT 200" +$pslQuery = "SELECT Id, MasterLabel, DeveloperName, TotalLicenses, UsedLicenses FROM PermissionSetLicense ORDER BY MasterLabel LIMIT 200" + +$grandTotalLicenses = 0 +$grandTotalUsed = 0 + +# 1) User Licenses +Write-Host "User Licenses" -ForegroundColor White -BackgroundColor DarkBlue +Write-Host "-------------" -ForegroundColor White -BackgroundColor DarkBlue + +$userLicenseResult = Invoke-SafeSOQLQuery -Query $userLicenseQuery +if ($userLicenseResult -eq "NOT_AVAILABLE") { + Write-Host "UserLicense object not available in this org." + Write-Host "" +} +elseif ($userLicenseResult -ne $null) { + $userTotals = Format-LicenseTable -JsonData $userLicenseResult -Headers @("Name", "Total", "Used", "Remaining") -LicenseType "User" + $grandTotalLicenses += $userTotals.Total + $grandTotalUsed += $userTotals.Used +} +else { + Write-Host "Failed to query UserLicense." + Write-Host "" +} + +# 2) Permission Set Licenses +Write-Host "Permission Set Licenses" -ForegroundColor White -BackgroundColor DarkBlue +Write-Host "-----------------------" -ForegroundColor White -BackgroundColor DarkBlue + +$pslResult = Invoke-SafeSOQLQuery -Query $pslQuery +if ($pslResult -eq "NOT_AVAILABLE") { + Write-Host "PermissionSetLicense object not available in this org." + Write-Host "" +} +elseif ($pslResult -ne $null) { + $pslTotals = Format-LicenseTable -JsonData $pslResult -Headers @("MasterLabel", "DeveloperName", "Total", "Used", "Remaining") -LicenseType "PSL" + $grandTotalLicenses += $pslTotals.Total + $grandTotalUsed += $pslTotals.Used +} +else { + Write-Host "Failed to query PermissionSetLicense." + Write-Host "" +} + +# Summary +Write-Host "Note: This report covers the main Salesforce license types available in most orgs." -ForegroundColor Gray +Write-Host "FeatureLicense objects are not commonly available and have been excluded." -ForegroundColor Gray