Upload attachments to SharePoint Online

Some weeks ago, I got a task to implement a functionality with which document attachments can be uploaded to SharePoint Online. In this blog I want to explain and show how this can be done.

Of course, to upload files to SharePoint Online, it requires you to have a SharePoint Online domain and an account which has enough access rights to upload and manage files.

D365fFO offers a functionality with which files can be uploaded to SharePoint Online during the attachment. This is done by creating a document type under Document management which has SharePoint set up as the file location. When uploading a file using this document type, the file is then uploaded to SharePoint.

Using this available framework of D365fFO, a simple functionality can be developed with which attachments can be uploaded to SharePoint Online e.g. by using a button.

To do this, we first need to define the SharePoint Online server and site path and also the path to the folder where we want to put the file. Second, we also need the binary file information of the attachment we want to upload.

Let’s assume the SharePoint address looks like this:
d365dev.sharepoint.com/sites/Test-Site/Shared Documents/Invoices

Now, let’s determine the specific SharePoint paths by splitting up the address. That would be…
SharePoint server: d365dev.sharepoint.com
SharePoint site: sites/Test-Site
SharePoint folder path: Shared Documents/Invoices

One important thing to note: to use the D365fFO framework, it requires a D365fFO user who has a so-called “ExternalId”.

The function could then look like this:

protected void uploadToSharepoint(DocuRef _docuRef)
{
    str filename;
    str fileContetType;
    str externalId;
    System.IO.Stream fileStream;
    DocuValue docuValue = _docuRef.docuValue();

    const str defaultSPServer = 'd365dev.sharepoint.com';
    const str spSite = 'sites/Test-Site';
    const str spFolderPath = 'Shared Documents/Invoices';

    filename = docuValue.filename();
    fileContetType = System.Web.MimeMapping::GetMimeMapping(filename);

    // Get the file stream of the document attachment.
    fileStream = DocumentManagement::getAttachmentStream(_docuRef);
    // Specify a user who has an External Id.
    externalId = xUserInfo::getExternalId(curUserId());

    // Instantiate SharePoint document storage provider with sharepoint address path.
    Microsoft.Dynamics.AX.Framework.FileManagement.IDocumentStorageProvider storageProvider = new Microsoft.Dynamics.AX.Framework.FileManagement.SharePointDocumentStorageProvider(
        defaultSPServer,
        spSite,
        spFolderPath,
        externalId);

    storageProvider.ProviderId = DocuStorageProviderType::SharePoint;

    if (storageProvider != null)
    {
        // Generates a unique file name in case the file name already exists on SharePoint path.
        str uniqueFilename = storageProvider.GenerateUniqueName(filename);
        // Upload file to SharePoint Online path.
        Microsoft.Dynamics.AX.Framework.FileManagement.DocumentLocation location = storageProvider.SaveFile(
            docuValue.FileId,
            uniqueFilename,
            fileContetType,
            fileStream);

        if (location != null)
        {
            info(strFmt("File path: %1", location.get_NavigationUri().ToString()));
        }
    }
}

In my next blog post I will then show how to update the metadata of the uploaded file on SharePoint Online through D365fFO – including managed metadatas.

 

 

Create zip file with entries and download link

Here is an example on how to create a zip file for an attachment and sending a download link to the user.

First, the zip folder needs to be created. After that, file entries can be created inside the zip folder which hold the file information of the attachment by copying the attachment file information to the file entry.

The class “File” has a method called “SendFileToUser” which creates a download link of the zip file and sends the link to the user. The file is put into the temporary blob storage.

// Example of creating a zip file for a document attachment and sending a download link to the user.
public static void main(Args _args)
{
    System.IO.Stream fileStream;
    System.IO.MemoryStream zipArchiveStream = new System.IO.MemoryStream();
    DocuRef docuRef;
    DocuValue docuValue;

    const str extensionZip = '.zip';
    const str zipFileName = 'ZipDownload';
    
    select firstOnly docuRef
        join docuValue
        where docuValue.RecId == docuRef.ValueRecId;

    if (docuRef.RecId != 0)
    {
        // Creates the zip folder.
        using (System.IO.Compression.ZipArchive zipArchive = new System.IO.Compression.ZipArchive(
            zipArchiveStream,
            System.IO.Compression.ZipArchiveMode::Create,
            true))
        {
            // Creates the file entry in the zip folder.
            System.IO.Compression.ZipArchiveEntry fileEntry = zipArchive.CreateEntry(docuValue.filename());
            
            // Opens the created file entry and copies the binary file information of the original file to the file entry in the zip folder.
            using (System.IO.Stream fileEntryStream = fileEntry.Open())
            {
                // Gets the file stream of the document attachment.
                fileStream = DocumentManagement::getAttachmentStream(docuRef);
                fileStream.CopyTo(fileEntryStream);
            }
        }

        // Send a download link of the created zip folder to the user. 
        File::SendFileToUser(zipArchiveStream, zipFileName + extensionZip);
    }
}

InventDim fields on Data Entities

To import inventory dimension fields, Microsoft has implemented a map called “InventInventoryDimensionEntityFieldsMapping” which can be used in Data Entity mappings. This map needs to be added to the Data Entity under “Mappings” node and the fields between the map and the Data Entity fields need to be mapped accordingly.

Note: Not the fields of the datasource need to be mapped, but the Data Entity fields itselves. This means that you first need to include the InventDim table as a datasource to your Data Entity and drag the needed inventory dimension fields into your Data Entity fields.

inventorymapping

Additionally, the method “mapEntityToDatasource” needs to be implemented with below code. Upon insertion or update, the specified inventory dimensions will be resolved and the InventDim record is returned.

public void mapEntityToDatasource(DataEntityRuntimeContext _entityCtx, DataEntityDatasourceRuntimeContext _dataSourceCtx)
{
    super(_entityCtx, _dataSourceCtx);

    switch (_dataSourceCtx.name())
    {
        case dataEntityDataSourceStr(SuppItemTableEntity, InventDim):
            InventDim inventDim = this.InventInventoryDimensionEntityFieldsMapping:resolveInventDim();
            _dataSourceCtx.setBuffer(inventDim);
            _dataSourceCtx.setDatabaseOperation(DataEntityDatabaseOperation:Update);
            break;
    }
}

Note: If a Data Entity involves more than one InventDim datasource (e.g. a Data Entity for the SuppItemTable table), then the mapping does not work. In this case, custom code needs to be written to map the fields correctly.

Disable editing of specific financial dimensions on form

The form control “DimensionEntryControl” has a method called “parmEditableDimensionSet” which sets only a specific dimension set as editable. Thus, by excluding financial dimensions from the dimension set, it is possible to make specific dimension not editable.

First, a dimension set storage needs to be created. After that, each financial dimension will be added to the dimension set storage excluding the dimensions which should not be editable. At the end, the dimension set storage will be passed to the mentioned parm-method “parmEditableDimensionSet” which then only sets the dimensions in the dimension set storage as editable.

Below is an example which implements the code in the form event handler “OnInitialized” to make dimension not editable upon calling a form.

[FormEventHandler(formStr(SalesTable), FormEventType:Initialized)]
public static void SalesTable_OnInitialized(xFormRun _sender, FormEventArgs _e)
{
    DimensionEntryControl dimControl = _sender.design().controlName(identifierStr(DimensionEntryControlTable));
    DimensionEnumeration dimensionSetId = DimensionCache:getDimensionAttributeSetForLedger();
    DimensionAttributeSetStorage dimensionAttributeSetStorage;
    DimensionAttribute dimensionAttribute;
    DimensionAttributeSetItem dimAttrSetItem;

    const str contractNumber = 'ContractNo';
    const str contractType = 'ContractType';

    dimensionAttributeSetStorage = new DimensionAttributeSetStorage();

    while select dimensionAttribute
        where dimensionAttribute.Name != contractNumber // Exclude specific dimension which should be not editable
            && dimensionAttribute.Name != contractType // Exclude specific dimension which should be not editable
        join dimAttrSetItem
            where dimAttrSetItem.DimensionAttribute == dimensionAttribute.RecId
                && dimAttrSetItem.DimensionAttributeSet == dimensionSetId
    {
        dimensionAttributeSetStorage.addItem(
            dimensionAttribute.RecId,
            dimensionAttribute.HashKey,
            NoYes::Yes);
    }

    dimControl.parmEditableDimensionSet(dimensionAttributeSetStorage.save());
}

As a result, the dimension fields “ContractNo” and “ContractType” are no longer editable on the sales order form.

findim

Yet another Dynamics AX/365 blog

You might ask yourself why this blog exists. Well, to be honest, it exists to give me the possibility to document whatever I’ve developed in and for Dynamics AX and Dynamics 365 for Finance and Operations. It also serves as a Know-How database by collecting all my experiences and knowledge about Dynamics AX/365 and all other things around it. A good side-effect is that you can also profit from it 🙂

My goal is also to help out others by sharing my knowledge and also giving a platform to discuss about all the stuff around it.

My last reason: since I’ve already come across so many sites and blogs where people’s posts helped me out so much, I also want to give that back 🙂

Please enjoy your stay on my blog and feel free to contribute to the DAX/D365 community by commenting and sharing your thoughts/knowledge!