# Generates an MSOL User License Report

Function Get-MGUserLicenseReport {
    Generates a comprehensive license report.
    Generates a license report on a all users or a specified group of users.
    By Default it will generate a new report file each time it is run.
    Report includes the following:
    * All licenses assigned to each user provided.
    * State of all plans inside of each license assignment
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
    Log file showing all actions taken by the function $env:LOCALAPPDATA\MGLicenseManagement.log
    CSV file named $env:LOCALAPPDATA\License_Report_YYYYMMDD_X.csv that contains the report.
    Creates a new License_Report_YYYYMMDD_X.csv file with the license report in the $env:LOCALAPPDATA\ directory for all users.
    Get-MGUserLicenseReport -Users $SalesUsers -Overwrite
    OverWrites the existing $env:LOCALAPPDATA\\License_Report_YYYYMMDD.csv file with a license report for all users in $SalesUsers.

        [switch]$OverWrite = $false

    # Make sure we have the connection to MSOL
    Write-SimpleLogFile "Generating Sku and Plan Report" -OutHost

    # Get all of the availible SKUs
    $AllSku = Get-MgSubscribedSku 2>%1
    if ($AllSku.count -le 0) {
        Write-Error ("No SKU found! Do you have permissions to run Get-MGSubscribedSKU? `nSuggested Command: Connect-MGGraph -scopes Organization.Read.All, Directory.Read.All, Organization.ReadWrite.All, Directory.ReadWrite.All")
    } else {
        Write-SimpleLogFile ("Found " + $AllSku.count + " SKUs in the tenant") -OutHost
    # Make sure our plan array is null
    [array]$Plans = $null

    # Build a list of all of the plans from all of the SKUs
    foreach ($Sku in $AllSku) {
        $SKU.ServicePlans.ServicePlanName | ForEach-Object { [array]$Plans = $Plans + $_ }

    # Need just the unique plans
    $Plans = $Plans | Select-Object -Unique | Sort-Object
    Write-SimpleLogFile ("Found " + $Plans.count + " Unique plans in the tenant") -OutHost

    # Make sure the output array is null
    $Output = $null

    # Create a false SKU object so we populate the first entry in the array with all needed values so we get a good CSV export
    # Basically a cheat so we can easily use export-csv we will remove this entry from the actual output
    $Object = $Null
    $Object = New-Object -TypeName PSobject
    $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value "BASELINE_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value ""
    $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value "BASELINELOCATION_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -Value "BASELINEDELETED_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "BASELINESKU_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "BASELINEINHERIT_IGNORE"

    # Populate a value into all of the plan names
    foreach ($value in $Plans) {

        $Object | Add-Member -MemberType NoteProperty -Name $value -Value "---"

    # Create the output list arrays
    $Output = New-Object System.Collections.ArrayList
    $UserToProcess = New-Object System.Collections.ArrayList

    # Populate in the generic object to the list array
    $Output.Add($Object) | Out-Null

    # See if our user array is null and pull all users if needed
    if ($null -eq $Users) {

        Write-SimpleLogFile "Getting all users in the tenant." -OutHost
        Write-SimpleLogFile "This can take some time." -OutHost

        # Get all of the users in the tenant
        $UserToProcess = Get-MgUser -All
        Write-SimpleLogFile ("Found " + $UserToProcess.count + " users in the tenant") -OutHost

    # Gather just the users provided
    else {
        # Make user our Users object is valid
        [array]$Users = Test-MGUserObject -ToTest $Users

        Write-SimpleLogFile "Gathering License information for provided users" -OutHost
        $i = 0
        # Get the data for each user to use in generating the report
        foreach ($account in $Users) {
            Update-MGProgress -CurrentCount $i -MaxCount $users.count -Message "Gathering License information for provided users"
            ### TODO: Need some error handling here for "bad inputs"
            $UserToProcess.Add((get-mguser -ConsistencyLevel eventual -Search ("Userprincipalname:" + $account.userprincipalname)))            
            if (!($i % 100)) { Write-SimpleLogFile ("Gathered Data for " + $i + " Users") }

        Write-SimpleLogFile ("Found " + $UserToProcess.count + " users to report on")

    ### Now that we have all of the user objects we need to start processing them and building our output object ###

    # Setup a counter for informing the user of progress
    $i = 0
    # Process each user
    Write-SimpleLogFile "Generating Report" -OutHost
    foreach ($UserObject in $UserToProcess) {
        # Increase the counter
        Update-MGProgress -CurrentCount $i -MaxCount $UserToProcess.count -Message "Generating Report"
        # Output every 100 users for progress
        if (!($i % 100)) { Write-SimpleLogFile ("Finished " + $i + " Users") }

        # Get the license assignments for the user
        $licenseOptions = Get-MgUserLicenseDetail -UserId $UserObject.Id
        # If no license is assigned we need to create that object
        if ($licenseOptions.count -eq 0) {
            $Object = $null
            # Create our object and populate in our values
            $Object = New-Object -TypeName PSobject
            $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
            $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
            $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
            $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -value ([bool]$UserObject.SoftDeletionTimestamp)
            $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "UNASSIGNED"
            $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit"
            # Add the object to the output array
            $Output.Add($Object) | Out-Null
        #If we have a license then add that information
        else {
            # Get how the licenses are assigned
            $UserLicenseAssignment = Get-MGUserLicenseAssignmentState -UserId $UserObject.Id
            # We can have more than one license on a user so we need to do each one seperatly
            foreach ($license in $licenseOptions) {
                $Object = $null

                # Determine how the license is assigned
                $assignment = Get-MGLicenseAssignmentMethod -UserAssignementArray $UserLicenseAssignment -SkuToResolve $license.SkuId
                # Create our object and populate in our values
                $Object = New-Object -TypeName PSobject
                $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
                $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
                $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
                $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -value ([bool]$UserObject.SoftDeletionTimestamp)
                $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value $license.SKUPartNumber
                $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value $assignment

                # Add each of the plans for the license in along with its status
                foreach ($value in $license.ServicePlans) {
                    $Object | Add-Member -MemberType NoteProperty -Name $value.ServicePlanName -Value $value.ProvisioningStatus
                # Add the object created from the user information to the output array
                #[array]$Output = $Output + $Object
                $Output.Add($Object) | Out-Null
    # Now that we have all of our output objects in an array we need to output them to a file
    # If overwrite has been set then just take the file name as the date and force overwrite the file
    if ($OverWrite) {
        # Build our file name
        $path = Join-path $env:LOCALAPPDATA ("License_Report_" + [string](Get-Date -UFormat %Y%m%d) + ".csv")
    # Default behavior will be to create a new incremental file name
    else {
        # Build our file name
        $RootPath = Join-path $env:LOCALAPPDATA ("License_Report_" + [string](Get-Date -UFormat %Y%m%d))
        $TryPath = $RootPath + "*"
        # Find any existing files that start with our planed file name
        $FilesInPath = Get-ChildItem $TryPath

        # Found files so we need to increment our file name with _X
        if ($FilesInPath.count -gt 0) {
            $Path = $RootPath + "_" + ($FilesInPath.count) + ".csv"
        # Didn't find anything so we are good with the base file name
        else {
            $Path = $RootPath + ".csv"

    Write-SimpleLogFile ("Exporting report to " + $path) -OutHost

    # Export everything to the CSV file
    $Output | Export-Csv $path -Force -NoTypeInformation
    # Pull it back in so we can remove our fake object
    $Temp = Import-Csv $path
    $Temp | Where-Object { $_.UserPrincipalName -ne "" } | Export-Csv $path -Force -NoTypeInformation
    Write-SimpleLogFile "Report generation finished" -OutHost