Copying Smart List Items Through PnP PowerShell

Here is a cool script to run so that we can copy all the list items from a Source List to a Target List within the same collection.
 
This is a smart modern way of copying list items without losing any data from any column values. 
 
The amazing part of this PS script is it can handle all types of list columns like look ups, metadata, other data types, and atachments.
 
Here is the PnP script:
  1. Function Copy - SPOAttachments($SourceItem, $TargetItem) {  
  2.         Try {  
  3.             #Get All Attachments from Source  
  4.             $Attachments = Get - PnPProperty - ClientObject $SourceItem - Property "AttachmentFiles"  
  5.             $Attachments | ForEach - Object {  
  6.                 #Download the Attachment to Temp  
  7.                 $File = Get - PnPFile - Url $_.ServerRelativeUrl - FileName $_.FileName - Path $env: TEMP - AsFile - force  
  8.                 #Add Attachment to Target List Item  
  9.                 $FileStream = New - Object IO.FileStream(($env: TEMP + "\"+$_.FileName),[System.IO.FileMode]::Open)  
  10.                         $AttachmentInfo = New - Object - TypeName Microsoft.SharePoint.Client.AttachmentCreationInformation $AttachmentInfo.FileName = $_.FileName $AttachmentInfo.ContentStream = $FileStream $AttachFile = $TargetItem.AttachmentFiles.add($AttachmentInfo) $Context.ExecuteQuery() #Delete the Temporary File Remove - Item - Path $env: TEMP\ $($_.FileName) - Force  
  11.                     }  
  12.                 }  
  13.                 Catch {  
  14.                     write - host - f Red "Error Copying Attachments:"  
  15.                     $_.Exception.Message  
  16.                 }  
  17.             }  
  18.             #Function to list items from one list to another  
  19.             Function Copy - SPOListItems() {  
  20.                 param(  
  21.                     [Parameter(Mandatory = $true)][string] $SourceListName,  
  22.                     [Parameter(Mandatory = $true)][string] $TargetListName)  
  23.                 Try {  
  24.                     #Get All Items from the Source List in batches  
  25.                     Write - Progress - Activity "Reading Source..." - Status "Getting Items from Source List. Please wait..."  
  26.                     $SourceListItems = Get - PnPListItem - List $SourceListName - PageSize 500  
  27.                     $SourceListItemsCount = $SourceListItems.count  
  28.                     Write - host "Total Number of Items Found:"  
  29.                     $SourceListItemsCount  
  30.                     #Get fields to Update from the Source List - Skip Read only, hidden fields, content type and attachments  
  31.                     $SourceListFields = Get - PnPField - List $SourceListName | Where {  
  32.                         (-Not($_.ReadOnlyField)) - and(-Not($_.Hidden)) - and($_.InternalName - ne "ContentType") - and($_.InternalName - ne "Attachments")  
  33.                     }  
  34.                     #Loop through each item in the source and Get column values, add them to target[int] $Counter = 1  
  35.                     ForEach($SourceItem in $SourceListItems) {  
  36.                         $ItemValue = @ {}  
  37.                         #Map each field from source list to target list  
  38.                         Foreach($SourceField in $SourceListFields) {  
  39.                             #Check  
  40.                             if the Field value is not Null  
  41.                             If($SourceItem[$SourceField.InternalName] - ne $Null) {  
  42.                                 #Handle Special Fields  
  43.                                 $FieldType = $SourceField.TypeAsString  
  44.                                 If($FieldType - eq "User" - or $FieldType - eq "UserMulti" - or $FieldType - eq "Lookup" - or $FieldType - eq "LookupMulti") #People Picker or Lookup Field {  
  45.                                     $LookupIDs = $SourceItem[$SourceField.InternalName] | ForEach - Object {  
  46.                                         $_.LookupID.ToString()  
  47.                                     }  
  48.                                     $ItemValue.add($SourceField.InternalName, $LookupIDs)  
  49.                                 }  
  50.                                 ElseIf($FieldType - eq "URL") #Hyperlink {  
  51.                                     $URL = $SourceItem[$SourceField.InternalName].URL  
  52.                                     $Description = $SourceItem[$SourceField.InternalName].Description  
  53.                                     $ItemValue.add($SourceField.InternalName, "$URL, $Description")  
  54.                                 }  
  55.                                 ElseIf($FieldType - eq "TaxonomyFieldType" - or $FieldType - eq "TaxonomyFieldTypeMulti") #MMS {  
  56.                                     $TermGUIDs = $SourceItem[$SourceField.InternalName] | ForEach - Object {  
  57.                                         $_.TermGuid.ToString()  
  58.                                     }  
  59.                                     $ItemValue.add($SourceField.InternalName, $TermGUIDs)  
  60.                                 }  
  61.                                 Else {  
  62.                                     #Get Source Field Value and add to Hashtable  
  63.                                     $ItemValue.add($SourceField.InternalName, $SourceItem[$SourceField.InternalName])  
  64.                                 }  
  65.                             }  
  66.                         }  
  67.                         Write - Progress - Activity "Copying List Items:" - Status "Copying Item ID '$($SourceItem.Id)' from Source List ($($Counter) of $($SourceListItemsCount))" - PercentComplete(($Counter / $SourceListItemsCount) * 100)  
  68.                         #Copy column value from source to target  
  69.                         $NewItem = Add - PnPListItem - List $TargetListName - Values $ItemValue  
  70.                         #Copy Attachments  
  71.                         Copy - SPOAttachments - SourceItem $SourceItem - TargetItem $NewItem  
  72.                         Write - Host "Copied Item ID from Source to Target List:$($SourceItem.Id) ($($Counter) of $($SourceListItemsCount))"  
  73.                         $Counter++  
  74.                     }  
  75.                 }  
  76.                 Catch {  
  77.                     Write - host - f Red "Error:"  
  78.                     $_.Exception.Message  
  79.                 }  
  80.             }  
  81.             #Connect to PnP Online  
  82.             Connect - PnPOnline - Url "https://abcd.sharepoint.com/sites/ArchivalDevelopmentTest" - useweblogin  
  83.             $Context = Get - PnPContext  
  84.             #Call the Function to Copy List Items between Lists  
  85.             Copy - SPOListItems - SourceListName "CountriesArchive" - TargetListName "CountriesArchive2"  
Just configure the above highlighted areas as per your requirement. 
 
Advantages
 
You can use the above script for a cool, smooth Archival process; i.e., Copy + Remove, as you know how to remove list items batchwise using PowerShell once the above copying is finished.  
 
This is applicable for both the Modern and Classic experience. 
 
Drawbacks
 
It will copy duplicate values if we run it mutliple times.
 
We can copy ListItems to another List within the same Site Collection only, as SP Context can read one Site Call at a time.
 
CrossSites are not supported.
 
Some other ideas - Go with Migration Tools if you want to go with Site Collections from different tenants on SP Online.
 
Cheers