Challenges in the Hybrid sourcing model

Overcoming cultural challenges in the Hybrid sourcing model

The focus of this article is how team members within offshore organizations can be more effective in the USA working within a hybrid model. Without active efforts to address the cultural differences leads to eroded USA client satisfaction and general frustration.

While the cultural differences in a fully offshore model are commonly understood, the differences could be mare stark and even destructive in the hybrid model, where staff are present both on and off shore.

It is understood that avoiding cultural miscues requires at times going against instinct, and doing what feels unnatural. However success depends on closing the cultural gap, so it is a goal worthy of effort.

In this article I try to offer specific guidance for the Indian audience that can be put to immediate use to improve effectiveness when working in the USA.

Punctuality

In USA, being late for a meeting can be fatal to a relationship. People are expected to arrive not only on-time, but preferably slightly early. Not only to accommodate unanticipated delays, but also to get through security and be fresh and ready for the meeting. In USA, meetings can run over, but generally start on time.

Language

While Indian written and spoke English is generally excellent, Americans often have a hard time parsing English spoken by Indians. This seems to be a greater problem over the phone. Note the human ear adjusts to accents, so over time, this issue is reduced. For example, many American companies use Philippine-based offshore call centers, as that accent is considered easier for Americans to understand.
Lastly, some expressions are unknown in the West, and cause puzzlement, and should be avoided in conversation. These include:
• “Kindly Revert”
Try instead “Please get back to me”
In general, outside of mathematics and programming, “Revert” is rarely ever used in spoken American English.

  • “Do the needful”
    Try instead “Please do ‘x’”
  • “Discuss about”
    Try instead “Can we discuss it?”
  • “Veg”
    That acronym is unknown in the west. Instead refer to it as “Vegetarian” and “Non-Vegetarian”
  • “Holiday”
    Use the word “Vacation” instead.
  • “Rest is fine”
    Instead, use the phrase “All the rest is fine”

Suggestions for success:

  • Visit clients in person when possible
  • Use Skype over telephone, as that connection is often clearer
  • Avoid some phrases commonly used by Indians
  • Add in “Please” and “Thank you” in verbal and written correspondence

Being direct

The nature of client/service provider puts the USA in the driver’s seat. The perception in India is that the client is generally right, and it would be rude to question the client. However in the USA, the expectation is that the service provider should challenge guidance, and offer insights, alternatives, possible improvements and even critique of the guidance. The problem becomes more subtle and insidious with delivering bad news. There are two general approaches to managing the delivery of bad news in the USA:

  1. Try to deliver all the bad news at once
    Better to declare a delivery date slippage of a full week, than to slip a day each day of the week.
  2. Deliver the news as early as possible
    Using the scheduling delay issue above, a top British architect once told me “If a project is declared late on the delivery date, I fire the staffer for one of two reasons: Either he is incompetent and did not know it was late until the last day, or he hid the information from me, which is dishonest”
    In short, it’s almost always better to be up-front. Being blunt on advice is considered especially refreshing in the USA, and is a key factor in promotions, event when the communication is skipping reporting layers.

Facing things head-on

Similar to being direct, in USA avoiding something uncomfortable, is frowned upon, and even downright confusing to Americans. If you cannot do what is asked, it is expected you will push back directly, and say you can’t. The best advice is to offer alternatives, rather than a blanket refusal.
How to succeed: If you can’t do something, offer guidance on who can, or a date when you can, or an alternative approach that you can deliver on.

Initiative

In the USA, guidance given is often general and not specific and indirect. In contrast, the expectation from Indian staff is to be given precise directives. USA staff are used to giving vague and summary guidance, and only general goals.

This cultural gap leads to stress for Indian staff and dissatisfaction on the part of USA clients.

This cultural gap feeds the perception in USA is that Indian staff do not show initiative. This perception is grounded in experience where USA organizations expect staff to apply common sense and promote ideas, alternatives, and make unrequested changes.

In USA there is an expression “The Customer is King”. So if the client asks for something small (doable), the common response expected should be “Yes” or a qualified yes, with a promise to get back. It is less acceptable to say one needs to climb the management chain for approval. Staff are assumed to take responsibility. Certainly this has to be balanced with managing scope/costs caused by commitments.

Email confirmation

Americans expect email confirmation when sending an email. This may sound simpler and even petty, but has been an issue, especially exacerbated by the time zone differences. Emails are discussions in America, and Americans expect confirmation or a reply with questions, ideas or suggestions.

I’ve seen this simple expectation result in significant consternation when an American manager emails guidance, and doesn’t get questions or acknowledgement back in response.

Americans are generally happy to coordinate and navigate even complex business issues in writing.

Suggestions for success:

  • Acknowledge receipt of guidance along with stating that it was understood
  • Ask any questions or clarifications in response
  • Feel free to suggest improvements or enhancements or even critique

Summary

The cultural differences encountered in business can be a cause for delight, or a source for frustration and even failed business relationships. Closing the cultural gap starts with an understanding of differences, and effort to change behavior and reduce the impact of such differences.

Excel corruption writing DIF files

When Excel writes a file in the DIF format, SAS is unable to read the file.  Here’s a valid DIF that has 24 columns, and 446,213 rows:

TABLE
0,1
""
VECTORS
0,24      
""
TUPLES
0,446213  

Note that “Tuples” in mathematical formulas are equivalent to “Rows”. A VECTOR is like a dimension, or field. In the case of Excel, it refers to columns. So far so good. However here is how such a file would be saved by Excel 2010:

TABLE
0,1
"EXCEL"
VECTORS
0,446213
""
TUPLES
0,24

 

Excel has no problem reading the above file format, as it ignores tuples/vectors internally. However SAS cannot handle this not-so-standard deviation.

Below is VBA code that after saving a DIF file “fixes” the header by opening the file in binary mode and corrects the issue. Note FName contains both path and filename:

 
 'Fixup TUPLES and VECTORS
 
Dim filenum As Integer
Dim strData As String
Dim VectorIdx As Integer
Dim TuplesIdx As Integer
Dim VectorStr As String
Dim TuplesStr As String
Const CharsToProcess = 60
Dim outStr
Dim CRLF As String
Dim DoubleQuote As String
Dim Fname As String
 
CRLF = Chr(13) & Chr(10)
DoubleQuote = Chr(34)
 
Fname = saveString
 
filenum = FreeFile
Open Fname For Binary Access Read As filenum
 
strData = String$(CharsToProcess, " ")
Get #filenum, , strData
Close #filenum
 
VectorIdx = InStr(strData, "VECTORS")
TuplesIdx = InStr(strData, "TUPLES")
VectorStr = Mid(strData, VectorIdx + 9, 14) 'overly generous portion of chars
TuplesStr = Mid(strData, TuplesIdx + 8, 14)
 
If InStr(TuplesStr, Chr(13)) > 0 Then 'trim CR LF
  TuplesStr = Left(TuplesStr, InStr(TuplesStr, Chr(13)) - 1)
End If
 
If InStr(VectorStr, Chr(13)) > 0 Then 'trim CR LF
  VectorStr = Left(VectorStr, InStr(VectorStr, Chr(13)) - 1)
End If
 
outStr = "VECTORS" & CRLF & TuplesStr & CRLF & DoubleQuote & DoubleQuote & CRLF & "TUPLES" & CRLF & VectorStr
 
filenum = FreeFile
Open Fname For Binary Access Write As filenum
Put #filenum, VectorIdx, outStr
Close #filenum

SharePoint Group Management

Managing SharePoint Groups in PowerShell

SharePoint Groups are a great mechanism for managing user permissions; however, they exist within a single site collection. What if you have hundreds of site collections? We can easily script a range of common operations.

I prefer to use a CSV-fed approach to manage groups and users. I create a CSV with the name of the group and the users, which I list in pipe-separated format (commas are already being used for the CSV). To read in a CSV, use:

Import-Csv "L:PowerShellAD and SP group mapping.csv"

Let’s get the Site, Root Web, as well as an SPUser for the group owner, and get the groups object:

$Site = New-Object Microsoft.SharePoint.SPSite($SiteName)
write-host $site.Url
$rootWeb = $site.RootWeb;
$Owner = $rootWeb.EnsureUser($OwnerName)
$Groups = $rootWeb.SiteGroups;

Here’s how to add a Group:

 
$Groups.Add($SPGroupName, $Owner, $web.Site.Owner, “SharePoint Group to hold AD group for Members")

Here’s how to give the group Read access, for example:

 
$GroupToAddRoleTo = $Groups[$SPGroupName]
if ($GroupToAddRoleTo) #if group exists
{
   $MyAcctassignment = New-Object Microsoft.SharePoint.SPRoleAssignment($GroupToAddRoleTo)
   $MyAcctrole = $RootWeb.RoleDefinitions["Read"]
   $MyAcctassignment.RoleDefinitionBindings.Add($MyAcctrole)
   $RootWeb.RoleAssignments.Add($MyAcctassignment)
}

Here’s how to add a Member to a Group:

$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj) #if it exists
{
   $GroupToAddTo.addUser($UserObj)  
}

Note that a duplicate addition of a member is a null-op, throwing no errors.

Here’s how to remove a member:

$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj)
{
   $GroupToAddTo.RemoveUser($UserObj)  
}

Here’s how to remove all the members from a given group. This wipes the users from the whole site collection, so use this approach with care and consideration:

$user1 = $RootWeb.EnsureUser($MyUser)
try
{
   $RootWeb.SiteUsers.Remove($MyUser)
   $RootWeb.update()
}

Here’s the full script, with flags to setting the specific actions described above:

Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue
# uses feedfile to load and create set of SharePoint Groups.
$mylogfile="L:PowerShellongoinglogfile.txt"
$ADMap= Import-Csv "L:PowerShellAD and SP group mapping.csv"
$OwnerName = "DOMAIN\sp2013farm"
$AddGroups = $false;
$AddMembers = $false;  # optionally populates those groups, Comma separated list
$GrantGroupsRead = $true; #grants read at top rootweb level
$RemoveMembers = $false; # optionally  removes Comma separated list of users from the associated group
$WipeMembers = $false;  # wipes the groups clean        
$WipeUsersOutOfSite = $false;  #The Nuclear option. Useful to eliminate AD groups used directly as groups
 
 
 #we do not need a hashtable for this work, but let's load it for extensibility
$MyMap=@{}  #load CSV contents into HashTable
for ($i=0; $i -lt $AD.Count; $i++)
{
    $MyMap[$ADMap[$i].SharePointGroup] = $ADMap[$i].ADGroup;
}
 
# Script changes the letter heading for each site collection
$envrun="Dev"           # selects environment to run in
 
if ($envrun -eq "Dev")
{
$siteUrl = "h ttp://DevServer/sites/"
$mylogfile="L:PowerShellongoinglogfile.txt"
$LoopString = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z"
$LoopStringArr = $LoopString.Split(“,”)
 
}
elseif ($envrun -eq "Prod")
{
$siteUrl = "ht tp://SharePoint/sites/"
$mylogfile="L:PowerShellongoinglogfile.txt"
$LoopString = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z"
$LoopStringArr = $LoopString.Split(“,”)
}
else
{
Write-Host "ENVIRONMENT SETTING NOT VALID: script terminating..."
$siteUrl =  $null;
return;
}
 
Write-Host "script starting"
 
$myheader = "STARTING: $(get-date)"
 
foreach ($letter in $LoopStringArr)
{
    $SiteName=$siteurl+$letter
    $Site = New-Object Microsoft.SharePoint.SPSite($SiteName)
 
    write-host $site.Url
    $rootWeb = $site.RootWeb;
    $Owner = $rootWeb.EnsureUser($OwnerName)
    $Groups = $rootWeb.SiteGroups;
 
    for ($ADi = 0; $ADi -lt $ADMap.count; $ADi++)
    {
        $SPGroupName = $ADMap[$ADi].SharePoint Group;
         
        if ($AddGroups)
        {
            if (!$Groups[$SPGroupName]) #no exist, so create
            {
                try
                {
                    $Groups.Add($SPGroupName, $Owner, $web.Site.Owner, “SharePoint Group to hold AD group members")
                }
                catch
                {
                    Write-Host -ForegroundColor DarkRed "Ouch, could not create $($SPgroupName)"
                }
            }
            else
            {
                    Write-Host -ForegroundColor DarkGreen "Already exists: $($SPgroupName)"
            }
        } #endif Add Groups
     
            if ($GrantGroupsRead)
        {
            $GroupToAddRoleTo = $Groups[$SPGroupName]
            if ($GroupToAddRoleTo) #if group exists
            {
                 
                $MyAcctassignment = New-Object Microsoft.SharePoint.SPRoleAssignment($GroupToAddRoleTo)
                $MyAcctrole = $RootWeb.RoleDefinitions["Read"]
                $MyAcctassignment.RoleDefinitionBindings.Add($MyAcctrole)
                $RootWeb.RoleAssignments.Add($MyAcctassignment)
            } #if the group exists in the first place
        } #ActionFlagTrue
     
        if ($AddMembers)
        {
            $GroupToAddTo = $Groups[$SPGroupName]
            if ($GroupToAddTo) #if group exists
            {
                $usersToAdd = $ADMap[$ADi].ADGroup;
                 
                if ($usersToAdd.length -gt 0) #if no users to add, skip
                {
                    $usersToAddArr = $usersToAdd.split("|")
                    foreach ($userName in $usersToAddArr)
                    {
                        try
                        {
                            $UserObj = $rootWeb.EnsureUser($userName);
                            if ($UserObj)
                            {
                                $GroupToAddTo.addUser($UserObj)  #dup adds are a null-op, throwing no errors
                            }
                        }
                        catch
                        {
                        Write-Host -ForegroundColor DarkRed "cannot add user ($($userName) to $($GroupToAddTo)"
                        }
 
                    }
                } #users to add
            } #if the group exists in the first place
        } #ActionFlagTrue
         
        if ($RemoveMembers)
        {
            $GroupToAddTo = $Groups[$SPGroupName]
            if ($GroupToAddTo) #if group exists
            {
                $usersToAdd = $ADMap[$ADi].SharePoint Group;
                 
                if ($usersToAdd.length -gt 0) #if no users to add, skip
                {
                    $usersToAddArr = $usersToAdd.split("|")
                    foreach ($userName in $usersToAddArr)
                    {
                        try
                        {
                            $UserObj = $rootWeb.EnsureUser($userName);
                            if ($UserObj)
                            {
                                $GroupToAddTo.RemoveUser($UserObj)  #dup adds are a null-op, throwing no errors
                            }
                        }
                        catch
                        {
                        Write-Host -ForegroundColor DarkRed "cannot add user ($($userName) to $($GroupToAddTo)"
                        }
 
                    }
                } #users to add
            } #if the group exists in the first place
        } #ActionFlagTrue
         
        if ($WipeMembers)  #Nukes all users in the group
        {
            $GroupToAddTo = $Groups[$SPGroupName]
            if ($GroupToAddTo) #if group exists
            {
                    foreach ($userName in $GroupToAddTo.Users)
                    {
                        try
                        {
                            $UserObj = $rootWeb.EnsureUser($userName);
                            if ($UserObj)
                            {
                                $GroupToAddTo.RemoveUser($UserObj)  #dup adds are a null-op, throwing no errors
                            }
                        }
                        catch
                        {
                        Write-Host -ForegroundColor DarkRed "cannot remove user ($($userName) to $($GroupToAddTo)"
                        }
 
                    }
                 
            } #if the group exists in the first place
        } #ActionFlagTrue
 
if ($WipeUsersOutOfSite)  #Nukes all users in the group
        {
        $usersToNuke = $ADMap[$ADi].ADGroup;
         
        if ($usersToNuke.length -gt 0) #if no users to add, skip
                {
                    $usersToNukeArr = $usersToNuke.split("|")
                    foreach ($MyUser in $usersToNukeArr)
                    {
                        try
                            {
                                try
                                {
                                    $user1 = $RootWeb.EnsureUser($MyUser)
                                }
                                catch
                                {
                                    Write-Host "x1: Failed to ensure user $($MyUser) in $($Site.url)"
                                }
                                 
                                try
                                {
                                    $RootWeb.SiteUsers.Remove($MyUser)
                                    $RootWeb.update()
                                }
                                catch
                                {
                                    Write-Host "x2: Failed to remove $($MyUser) from all users in $($Site.url)"
                                }
                           }
                           catch
                           {
                                Write-Host "x4: other failure for $($MyUser) in $($Site.url)"
                           }
                } #if user is not null
            } #foreach user to nuke
        } #ActionFlagTrue
         
    }
     
     
    $rootWeb.dispose()
    $site.dispose()
     
} #foreach site

Restore SharePoint document timestamp and author from feedfile

Often an administrator during maintenance or checking in a document for a user, “stomps” on a timestamp and who edited the document. In a perfect world we take the time to restore authorship and timestamp. Here’s a script that reads in a CSV of the URL, timestamp and user of any number of documents to correct. it will also try to remove the previous incorrect version, if possible.

 
$actionlist= Import-Csv "C:scriptsNameDateTag.csv"
 
for ($Ai=0; $Ai -lt $actionlist.Count; $Ai++)
    {
    $ActionRow=$ActionList[$Ai]
    $docurl=$ActionRow.DocURL;
    $site = New-Object Microsoft.SharePoint Online.SPSite($docurl)
    $web = $site.OpenWeb()
    $item = $web.GetListItem($docurl)
    $list = $item.ParentList
     
    [System.DateTime] $dat = Get-Date $ActionRow.Timestamp
    $usr = $web.ensureuser($ActionRow.Editor)
     
     $item["Modified"] = $dat;
     $item["Editor"] = $usr;
        $item.Update()
     try { $item.Versions[1].delete() } catch {write-host -foregroundcolor red "Error (1) could not delete old version of $($item['Name'])"}
    }

Use PowerShell to Automate Migration of FTP files to a File Share

A common business process to automate is moving files from an FTP server. To copy the files from an FTP server we first need to get a file listing. The following routine serves nicely:

function Get-FtpDir ($url,$credentials) {
 
$request = [Net.WebRequest]::Create($url)
 
$request.Method = [System.Net.WebRequestMethods+FTP]::ListDirectory
 
if ($credentials)
 
{
 
$request.Credentials = $credentials
 
}
 
$response = $request.GetResponse()
 
$reader = New-Object IO.StreamReader $response.GetResponseStream()
 
$reader.ReadToEnd()
 
$reader.Close()
 
$response.Close()
 
}

Let’s set some basic parameters for the file migration:

$url = 'ftp://sftp.SomeDomain.com';
$user = 'UserID';
$pass = 'Password'  #single quotes recommended if unusual characters are in use
$DeleteSource = $true;  #controls whether to delete the source files from the FTP Server after copying
$DestLocation = '\DestinationServerAnyLocation';

Let’s start the processing with connecting to the FTP server, getting the file list, and processing:

$credentials = new-object System.Net.NetworkCredential($user, $pass)
 $webclient = New-Object System.Net.WebClient
 $webclient.Credentials = New-Object System.Net.NetworkCredential($user,$pass)  
 
$files=Get-FTPDir $url $credentials
$filesArr = $files.Split("`n")
 
Foreach ($file in ($filesArr )){
    if ($file.length -gt 0) #this actually happens for last file
    {
    $source=$url+$file
    $dest = $DestLocation + $file
    $dest = $dest.Trim()        # this is actually needed, as trailing blank appears in FTP directory listing
    $WebClient.DownloadFile($source, $dest)
    }
}

Let’s now delete the source files, if configured to do so:

$credentials = new-object System.Net.NetworkCredential($user, $pass)
 $webclient = New-Object System.Net.WebClient
 $webclient.Credentials = New-Object System.Net.NetworkCredential($user,$pass)  
 
$files=Get-FTPDir $url $credentials
$filesArr = $files.Split("`n")
 
Foreach ($file in ($filesArr )){
    if ($file.length -gt 0) #this actually happens for last file
    {
    $source=$url+$file
    $dest = $DestLocation + $file
    $dest = $dest.Trim()        # this is actually needed, as trailing blank appears in FTP directory listing
    $WebClient.DownloadFile($source, $dest)
    }
}

Let’s now delete the source files, if configured to do so:

if ($DeleteSource)
{
Foreach ($file in ($filesArr )){
    if ($file.length -gt 0) #this actually happens for last file
    {
    $source=$url+$file
    $ftprequest = [System.Net.FtpWebRequest]::create($source)
    $ftprequest.Credentials =  New-Object System.Net.NetworkCredential($user,$pass)
    $ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
    $ftprequest.GetResponse()
    }
}
}

That’s it in a nutshell. Just don’t try to move zero length files, and trim the filenames for trailing blanks. Ensure your FTP ports are open, and you are good to go!

Start Your PowerShell Migration Project In A Click

Our technology and wide delivery footprint have created billions of dollars in value for clients globally and are widely recognized by industry professionals and analysts.

Incrementing repeating table field values

How to increment a field for new rows added into a repeating table in InfoPath

It is really helpful to users to incrementally auto-populate newly added rows. In InfoPath, XPath is used to specify calculated values. To count the number of rows that exist, we can use the following expression:

count(../preceding-sibling::*)

This returns the 0 based count of existing rows. First row would be zero. To get a one-based count, simply use:

count(../preceding-sibling::*)+1

What if you want to have letters that increase? One way is to use the XPath translate(0 function such as:

translate(count(../preceding-sibling::*), "012345", "ABCDEF")

Workflow error in SP2013 related to App Step

I had a situation when my workflows went to “Suspended” after a few minutes. I noticed this occured when I used the “App Step”. Looking at the “Suspended” state, there was an “i” icon, when clicked, showed this error:

Details: An unhandled exception occurred during the execution of the workflow instance. Exception details: System.ApplicationException: HTTP 401 {“error_description”:”The server was unable to process the request due to an internal error

Here is how the error actually consistently appears:

img-01

It turns out the App Step will not work, without specific configuration to grant the App Step rights.So of course some permission issue was most probable. And I was suspicious of App Step, as I mentioned yesterday as the proximate cause.

It turns out the App Step will not work, without specific configuration to grant the App Step rights.

So of course some permission issue was most probable. And I was suspicious of App Step, as I mentioned yesterday as the proximate cause.

So configured the permission as below link:
https://www.dmcinfo.com/latest-thinking/blog/id/8661/create-site-from-template-using-SharePoint-2013-workflow
One more thing, we need specify the Scope URL as below

On the subweb, you will find the configuration setting for app permission: http :[SPWeb URL] /_layouts/15/appinv.aspx

You can see it set up from here:
[site URL] /_layouts/15/appprincipals.aspx?Scope=Web
Here is the feature to enable:

img-02

For App Step Permissions, it is the 3rd link under permissions from Site Settings.

Here is how the specific App Step Permission appears when viewed from Site Settings App Step Permissions:

img-03

With the permissions granted, the workflows should then work on retry. You can retry from the workflow summary page shown on top. Let’s understood SharePoint Online Infrastructure Services.

Additional Read
How to Fix Bad Taxonomy Terms in Sharepoint Automatically

How to Download All Attachments for All Tasks in a List

Downloading all attachments for a SharePoint task list Tasks can have attachments. In fact, they can have multiple attachments.

However, these are stored in an “AttachmentCollection”. We can iterate through all items in the task list to download all attachments.

What we do is create a folder for each of the items and name the folder by the ID of the task.

 
$webUrl = "http:.."            # this is the URL of the SPWeb
$library = "Compliance Tasks"  # this is the SPList display name
$tempLocation = "D:\PROD"      # Local Folder to dump files
$s = new-object Microsoft.SharePoint.SPSite($webUrl)   
$w = $s.OpenWeb()        
$l = $w.Lists[$library]   
foreach ($listItem in $l.Items)
  {
     Write-Host "    Content: " $listItem.ID
       $destinationfolder = $tempLocation + "\" + $listItem.ID         
          if($listItem.Attachments.Count -gt 0)
          {
               if (!(Test-Path -path $destinationfolder))       
               {           
                 $dest = New-Item $destinationfolder -type directory         
               }
                     foreach ($attachment in $listItem.Attachments)   
                 {       
                       $file = $w.GetFile($listItem.Attachments.UrlPrefix + $attachment)       
                       $bytes = $file.OpenBinary()               
                       $path = $destinationfolder + "\" + $attachment
                       Write "Saving $path"
                       $fs = new-object System.IO.FileStream($path, "OpenOrCreate")
                       $fs.Write($bytes, 0 , $bytes.Length)   
                       $fs.Close()   
                 }
              }
   }

A folder for each task was created to allow for multiple attachments. The ID was applied to each folder to allow a subsequent script to traverse and upload the attachments by ID or for any linkage preservation.

For how to upload attachments from a task list, please see: Uploading attachments to tasks.

Additional Read
Secure Store Master Key Error

How to Configure a URL to a Specific Location Inside a PDF

URL to a Location Inside a PDF

It turns out, there is no way to specify a URL to a bookmark
Here’s how to jump to a place inside a PDF from a link by using what is called “Named Destinations”. Note these are not Bookmarks. Links to Bookmarks do not work.

Additional Read

The Ultimate Guide to Using SharePoint for End Users!

Here is an example link that will work: : http://SharePoint/dept/abc/Shared%20Documents/Test%20document%20for%20anchoring3.pdf#nameddes

See the URL above, note there’s a “#” then “nameddest=” then the named destination called “dest3”. This works.

#nameddest=[destination] in this case, I have a named destination I created called “dest3”

Note these are not bookmarks:
1. Use Adobe Acrobat X

2. Edit the PDF

3. Enable viewing of named destinations, by clicking “View, Show/Hide, Navigation Panes, destinations, see image below

4. Click “Destinations” below “Bookmarks” icon on left pane. See below:

img-01
[/av_textblock]

Named Destination
img-02

Additional Read

SharePoint Online vs On-Premises – Migrate from On-Premises to SharePoint Online

Solution for SharePoint incompatibility with Chrome

Solution for SharePoint in compatibility with Chrome

There have been a number of issues relating to how SharePoint behaves within Chrome. Most relate to legacy applications. These include SilverLight and ActiveX controls.
One significant issue is that Chrome won’t open InfoPath Filler forms in InfoPath Filler, and instead tries to open it in the web version of InfoPath.

A solution to these can be found in this 3rd party Chrome plug-in: IE Tab

It does have a per-user cost, but it enables IE to activated from within Chrome. One just specifies the URL string pattern that should be opened in IE.