Licencias sin usar
Compara licencias compradas contra asignadas por SKU y lista a los usuarios deshabilitados que siguen con licencia. Te dice cuánto puedes reasignar antes de comprar más.
Ejemplo en México
Cadena de tiendas en León, 12 sucursales y 260 personas
Viene la renovación anual y Compras pide justificar las 300 licencias, mientras varias sucursales tuvieron recortes de personal.
.\get-licencias-sin-usar.ps1 -RutaCsv .\licencias.csvEncuentra 22 licencias sin asignar y 17 en usuarios deshabilitados. Reasigna lo que hace falta y renueva 39 licencias menos.
Cómo implementarlo
- Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
- Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lector global.
- Filtra el CSV por la columna Hallazgo: un tipo es el resumen de SKU y el otro son los usuarios deshabilitados.
- Quita las licencias que ya no se ocupen y ajusta la cantidad en la siguiente renovación.
Ver el código completo de get-licencias-sin-usar.ps1
<#
.SYNOPSIS
¿Cuántas licencias compraste contra cuántas asignaste, y qué usuarios deshabilitados siguen con licencia?
.DESCRIPTION
Compara las unidades compradas y asignadas de cada SKU y lista a los usuarios deshabilitados con licencia.
Te ayuda a reasignar lo que ya pagas antes de comprar más o a ajustar la renovación.
Solo lectura: no cambia nada en el tenant.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\get-licencias-sin-usar.ps1 -RutaCsv .\licencias.csv
.NOTES
Permisos: LicenseAssignment.Read.All, User.Read.All
Rol mínimo: Lector global (Global Reader)
Licencia: Microsoft Entra ID Free
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
[CmdletBinding()]
param(
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
$permisos = @('LicenseAssignment.Read.All', 'User.Read.All')
if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
Connect-MgGraph -Scopes $permisos -NoWelcome
function Get-TodasLasPaginas {
param([string]$Uri)
while ($Uri) {
$pagina = Invoke-MgGraphRequest -Method GET -Uri $Uri
$pagina.value
$Uri = $pagina.'@odata.nextLink'
}
}
$base = 'https://graph.microsoft.com/v1.0'
$skus = @(Get-TodasLasPaginas "$base/subscribedSkus")
$nombreSku = @{}
foreach ($s in $skus) { $nombreSku[$s.skuId] = $s.skuPartNumber }
$deshabilitados = Get-TodasLasPaginas "$base/users?`$filter=accountEnabled eq false&`$select=displayName,userPrincipalName,assignedLicenses&`$top=999"
$filas = @(
foreach ($s in $skus) {
$compradas = [int]$s.prepaidUnits.enabled
$asignadas = [int]$s.consumedUnits
[pscustomobject]@{
Hallazgo = 'Resumen de SKU'
Sku = $s.skuPartNumber
Compradas = $compradas
Asignadas = $asignadas
SinAsignar = $compradas - $asignadas
EstadoSku = $s.capabilityStatus
Nombre = $null
Cuenta = $null
}
}
foreach ($u in $deshabilitados) {
foreach ($l in $u.assignedLicenses) {
[pscustomobject]@{
Hallazgo = 'Usuario deshabilitado con licencia'
Sku = if ($nombreSku[$l.skuId]) { $nombreSku[$l.skuId] } else { $l.skuId }
Compradas = $null
Asignadas = $null
SinAsignar = $null
EstadoSku = $null
Nombre = $u.displayName
Cuenta = $u.userPrincipalName
}
}
}
)
$resumen = @($filas | Where-Object Hallazgo -eq 'Resumen de SKU')
Write-Host "SKU revisados: $($resumen.Count). Licencias sin asignar: $(($resumen | Where-Object SinAsignar -gt 0 | Measure-Object SinAsignar -Sum).Sum + 0). Licencias en usuarios deshabilitados: $($filas.Count - $resumen.Count)"
if ($RutaCsv) {
# Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
foreach ($fila in $filas) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } }
# UTF-8 con BOM para que Excel en español muestre bien los acentos.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
Write-Host "CSV guardado en $RutaCsv"
} else {
$filas | Format-Table -AutoSize
}