Copy-SSPList.ps1

<#
    .Synopsis
    Copies a source site as a template and instantiates it at the destination.
    .Description
    This CmdLet gets a SiteTemplate from the source and instantiates it at the
    destination url. It attempts to download image files from the source in
    the Site Assets folder and upload them to the destination site.
    .Parameter Src
    This parameter contains the Url of the source site.
    .Parameter Dest
    This parameter contains the Url of the destination site.
    .Parameter SrcConnection
    This parameter contains the connection context for the source site. The default is the current connection
    returned by "Get-PnPConnection".
    .Parameter DestConnection
    This parameter contains the connection context for the destination site. The default is the current connection
    returned by "Get-PnPConnection".
    .Parameter Handlers
    This parameter contains a list of the handlers to invoke for the template.
#>


function Copy-SSPList {
    param(
        $listName,
        $srcConnection = (Get-PnPConnection),
        $destConnection = (Get-PnPConnection)
    )

    Write-Host "Copy $listName"
    try {
        $csvData = Get-SSPListData -ListName $listName -Connection $srcConnection
        if ($csvData -and $csvData.Length -gt 0)
        {
            Add-SSPListData -ListData $csvData -ListName $listName -Connection $destConnection
        }
    }
    catch [Exception]
    {
        $ErrorMessage = $_.Exception.Message
        Write-Host "Error: $ErrorMessage" -ForegroundColor Red
    }
}

function Get-SSPListData {
    param (
        [String] $listName,
        $connection = (Get-PnPConnection)
        )

    #Get fields to Update from the List - Skip Read only, hidden fields, content type and attachments
    $listFields = Get-PnPField -List $ListName -Connection $connection |
            Where { (-Not ($_.ReadOnlyField)) -and (-Not ($_.Hidden)) -and ($_.InternalName -ne  "ContentType") -and ($_.InternalName -ne  "Attachments") }

    $listFields = $listFields | Foreach-Object { $_.InternalName }

    $listItems=(Get-PnPListItem -List $listName -Connection $connection).FieldValues

    $csvData = @()

    foreach($listItem in $listItems)
    {
        $obj = New-Object PSObject
        $listItem.GetEnumerator() |
                Where-Object { $_.Key -in $listFields } |
                ForEach-Object{
                    $obj | Add-Member Noteproperty $_.Key $_.Value
                }
        $csvData += $obj
        $obj = $null
    }

    return $csvData
}

function getLookupID($ListName, $LookupFieldName, $LookupValue, $destConnection)
{
    #Get Parent Lookup List and Field from Child Lookup Field's Schema XML
    $lookupField =  Get-PnPField -List $ListName -Identity $LookupFieldName -Connection $destConnection
    [Xml]$schema = $lookupField.SchemaXml
    $parentListID = $schema.Field.Attributes["List"].'#text'
    $parentField  = $schema.field.Attributes["ShowField"].'#text'
    $parentLookupItem  = Get-PnPListItem -List $parentListID -Fields $parentField -Connection $destConnection |
            Where {$_[$parentField] -eq $LookupValue} | Select -First 1
    if ($parentLookupItem -ne $Null)  {
        return $parentLookupItem["ID"]
    }  else  {
        return $Null
    }
}

function Add-SSPListData {
    param (
        $listData,
        [String] $listName,
        $connection = (Get-PnPConnection)
    )
    $List = Get-PnPList -Identity $ListName -Connection $connection

    # Skip readonly, hidden fields, content type, and attachments.
    $listFields = Get-PnPField -List $ListName -Connection $connection |
            Where { (-Not ($_.ReadOnlyField)) -and (-Not ($_.Hidden)) -and ($_.InternalName -ne  "ContentType") -and ($_.InternalName -ne  "Attachments") }

    # Update the matching list item ID in each row.
    foreach($row in $listData)
    {
        $itemValue = @{}
        $csvFields = $row | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name
        # Map each field from row to target list
        foreach ($csvField in $csvFields)
        {
            $mappedField = $listFields | Where {$_.InternalName -eq $csvField}
            if ($mappedField -ne $Null)
            {
                $fieldName = $mappedField.InternalName
                if ($row.$csvField -ne $Null)
                {
                    $fieldType  = $mappedField.TypeAsString
                    if ($fieldType -eq "User" -or $fieldType -eq "UserMulti") #People Picker Field
                    {
                        $peoplePickerValues = $row.$fieldName.Split(",")
                        $itemValue.add($fieldName,$peoplePickerValues)
                    }
                    elseif ($fieldType -eq "Lookup" -or $fieldType -eq "LookupMulti") #Lookup Field
                    {
                        $LookupIDs = $row.$fieldName.Split(",") |
                                ForEach-Object { Get-LookupID -ListName $ListName -LookupFieldName $fieldName -LookupValue $_ }
                        $itemValue.Add($fieldName,$LookupIDs)
                    }
                    else
                    {
                        $itemValue.Add($fieldName,$row.$fieldName)
                    }
                }
            }
        }
        Write-Output $itemValue | Out-Host
        Add-PnPListItem -List $listName -Values $itemValue -Connection $connection | Out-Null
    }
}