Microsoft has announced the retirement of Business Voice licensing . If your tenant is still using Business Voice with Calling Plan or Business Voice without Calling Plan, you will need to switch to the new equivalent Teams Phone plan.

If you take a look at the Microsoft doc linked above, there are examples for how to update the licenses on bulk, however it is baffling Microsoft chose to demonstrate using the Azure AD PowerShell module, when the licensing portion of that module is slated to be retired as of today (6/30). I went ahead and created a script utilizing the Microsoft Graph API to update the licenses in bulk.

First, as a precaution, connect to Microsoft Teams with PowerShell and export a list of all users and their assigned numbers. If at any time the voice license feature is unassigned for a user, their phone number assigment will be removed immediately. Don’t ask me how I know.

# As a precaution, backup all users phone number assignments
Get-CsOnlineUser | where LineUri | select Displayname, UserPrincipalName, DialPlan, @{
	N = "Line"; e = { $_.lineuri -replace 'tel:', '' }
} | export-Csv TeamsUsers.csv -NoTypeInformation

Next we need to identify the license sku to remove and the sku to add in place of the old sku. Log into Azure AD and check your current licensed products. Use the table here to find the SkuPartID (center column)

In my example, I will replace Business Voice with Calling Plan, with Teams Phone with Calling Plan and Microsoft Teams Audio Conferencing with dial-out to USA/CAN

# Replace with appropriate sku names

Now we need to connect to Microsoft Graph and gather our tenants license info

# Get necessary permission scopes
$perms = 'User.Read.All', 'User.ReadWrite.All', 'Directory.Read.All'
Connect-MgGraph -Scopes $perms
Select-MgProfile beta

$Skus = Get-MgSubscribedSku # Get all skus in the tenant
$Users = Get-MGUser -All # Get all user accounts
$OldVoiceSku = $skus | where { $_.skupartnumber -eq "$OldSku" }
$NewVoiceSku = foreach ($Product in $NewSkus){
$s = $skus | where { $_.skupartnumber -like "$Product" }
@{ SkuID = $s.SkuId }

Finally, we need to process our users, gather their current license assignment, filter for the users who need updated, then update the license assignment.

$i = 0 # Increment variable
foreach ($user in $Users) # Gather license assigments for all users
	Write-Progress -Activity "Processing User License details" -Status "Working on $($user.displayname)" -PercentComplete (($i / $Users.Count) * 100)
	$user.LicenseDetails = Get-MgUserLicenseDetail -UserId $
	Start-Sleep -Milliseconds 200

$NeedsUpdated = $Users | where { $_.LicenseDetails.SkuPartNumber -like $OldSku }
foreach ($u in $NeedsUpdated)
	"Updating License assignment for {0}" -f $u.UserPrincipalName
	Set-MgUserLicense -UserId $ -AddLicenses $NewVoiceSku -RemoveLicenses @($OldVoiceSku.SkuId) #Add new license and remove old
	Start-Sleep -Milliseconds 200

Finally as a check, lets make sure our users still have their phone assigments.

Get-CsOnlineUser | select name,userprincipalname,lineuri

You should see no change if everything went according to plan. See the entire script on my github account