Thursday, January 10, 2019

Merge a report exported into PDF with other PDF files

This is an example on how to alter the PDF created from a SSRS report with other PDF files (such as the ones attached as document attachments).

To be able to do this, we will need to download and install the PDFSharp library, which is an open source library to create or modify PDF files. First, download from www.pdfSharp.com, the extract the PdfSharp.dll and copy it to the bin folder of the model folder which you want to do the modification on. Second, from the Visual Studio, right click on the project, and add a reference into the .dll file.

As for the modification itself, we will need to override this delegate: SRSPrintDestinationSettingsDelegates.toSendFile() and make sure to set the result on the EventHandlerResult to false. This way, after calling the delegate, it will not continue with the rest of the codes in SrsReportRunPrinter.toFile(), which is going to save the report PDF file into the Azure storage or send it to the user.

The sample codes would look like this:

 using PdfSharp;  
 using Microsoft.Dynamics.ApplicationPlatform.SSRSReportRuntime.Instrumentation;  
 class SRSPrintDestinationSettingsDelegates_EventHandler  
 {  
   #SRSFramework  
   private static void saveFile(System.Byte[] reportBytes, SrsReportRunPrinter printer, SrsReportDataContract dataContract)  
   {  
     SRSPrintDestinationSettings printSettings = dataContract.parmPrintSettings();  
     //SRSReportFileFormat fileFormat = printSettings.fileFormat();  
     Filename filename = "";  
     if(printSettings.fileName())  
     {  
       filename = printSettings.fileName();  
     }  
     else  
     {  
       filename = dataContract.parmReportCaption() ? dataContract.parmReportCaption() + ".pdf" : dataContract.parmReportName() + ".pdf";  
     }  
     if (reportBytes)  
     {  
       System.IO.MemoryStream stream = new System.IO.MemoryStream(reportBytes);  
       // Send file to browser for on premise scenario.  
       if(SrsReportRunUtil::isOnPremEnvironment())  
       {  
         Dynamics.AX.Application.File::SendFileToUser(stream, filename);  
         SSRSReportRuntimeEventSource::EventWriteRenderReportToFileTaskStop(  
             "Printing report to file ended.",  
             dataContract.parmReportExecutionInfo().parmReportRunId());  
         return;  
       }  
       // Upload file to temp storage and direct the browser to the file URL  
       SrsFileUploadNameContract fileNameContract = new SrsFileUploadNameContract();  
       fileNameContract.FileName(filename);  
       str categoryName = SrsReportRunUtil::convertAndTrimGuidValue(dataContract.parmReportExecutionInfo().parmReportRunId());  
       fileNameContract.CategoryName(categoryName);  
       SRSFileUploadTempStorageStrategy fileUploader = new SRSFileUploadTempStorageStrategy();  
       fileUploader.uploadFile(stream, FormJsonSerializer::serializeClass(fileNameContract));  
       // Set global cache that indicates there is file uploaded for current report execution.  
       // Using SGC not SGOC because we want scope to be in current user session.  
       // Owner - #RunIdOwner macro, Key - RunId, Value - boolean value.  
       SysGlobalCache globalCache = classfactory.globalCache();  
       if(!globalCache.isSet(#RunIdOwner, dataContract.parmReportExecutionInfo().parmReportRunId()))  
       {  
         globalCache.set(#RunIdOwner, dataContract.parmReportExecutionInfo().parmReportRunId(), true);  
       }  
     }  
     SSRSReportRuntimeEventSource::EventWriteRenderReportToFileTaskStop(  
         "Printing report to file ended.",  
         dataContract.parmReportExecutionInfo().parmReportRunId());  
   }  
   [SubscribesTo(classStr(SRSPrintDestinationSettingsDelegates), delegateStr(SRSPrintDestinationSettingsDelegates, toSendFile))]  
   public static void SRSPrintDestinationSettingsDelegates_toSendFile(System.Byte[] reportBytes, SrsReportRunPrinter printer, SrsReportDataContract dataContract, Microsoft.Dynamics.AX.Framework.Reporting.Shared.ReportingService.ParameterValue[] paramArray, EventHandlerResult result)  
   {  
     Pdf.PdfDocument       outputPDFDocument = new Pdf.PdfDocument();  
     System.IO.MemoryStream   memoryStream = new System.IO.MemoryStream(reportBytes);  
     System.IO.MemoryStream   mergedStream = new System.IO.MemoryStream();  
     boolean           isOutputModified;  
     SRSPrintDestinationSettings printSettings = dataContract.parmPrintSettings();  
     System.Byte[]        finalReportBytes = reportBytes;  
     void addStream(System.IO.MemoryStream _stream, Pdf.PdfDocument _outputPDFDocument)  
     {  
       Pdf.PdfDocument inputPDFDocument = new Pdf.PdfDocument();  
       int       pageCount;  
       Pdf.PdfPages  pdfPages;  
       inputPDFDocument = PdfSharp.Pdf.IO.PdfReader::Open(_stream, PdfSharp.Pdf.IO.PdfDocumentOpenMode::Import);  
       _outputPDFDocument.set_Version(inputPDFDocument.get_Version());  
       pageCount = inputPDFDocument.get_PageCount();  
       pdfPages = inputPDFDocument.get_Pages();  
       if (pageCount > 0)  
       {  
         for (int idx = 0; idx < pageCount; idx++)  
         {  
           _outputPDFDocument.AddPage(pdfPages.get_Item(idx));  
         }  
       }  
     }  
     if (printSettings.fileFormat() == SRSReportFileFormat::PDF && dataContract.parmRdpName() == classStr(SalesInvoiceDP))  
     {  
       SalesInvoiceContract  salesInvoiceContract = dataContract.parmRdpContract();  
       DocuRef            docuRef;  
       int                 attachmentCounter;  
       try  
       {  
         new InteropPermission(InteropKind::ClrInterop).assert();  
         while select docuRef //add some criteria here  
         {  
           if (docuRef.fileType() == 'pdf')  
           {  
             if (!attachmentCounter)  
             {  
               addStream(memoryStream, outputPDFDocument);  
             }  
             System.IO.Stream docuStream = DocumentManagement::getAttachmentStream(docuRef);  
             memoryStream = new System.IO.MemoryStream();  
             docuStream.CopyTo(memoryStream);  
             addStream(memoryStream, outputPDFDocument);  
             attachmentCounter++;  
           }  
         }  
         if (attachmentCounter)  
         {  
           outputPDFDocument.Save(mergedStream, false);  
           finalReportBytes = mergedStream.ToArray();  
           result.result(false); //this is to force the SrsReportRunPrinter.toFile NOT to continue after calling this delegate  
           isOutputModified = true;  
         }  
         CodeAccessPermission::revertAssert();  
       }  
       catch(Exception::CLRError)  
       {  
         str errorMessage = AifUtil::getClrErrorMessage();  
         CodeAccessPermission::revertAssert();  
         throw error(errorMessage);  
       }  
     }  
     if (isOutputModified && finalReportBytes)  
     {  
       SRSPrintDestinationSettingsDelegates_EventHandler::saveFile(finalReportBytes, printer, dataContract);  
     }  
   }  
 }  

Thursday, March 22, 2018

Setup an external catalogue for PunchOut eProcurement

D365O has a functionality to do PunchOut eProcurement. The main idea is not having to maintain the vendor item numbers in your system as well as always having the latest information on the vendor items.

Read more in here https://docs.microsoft.com/en-us/dynamics365/unified-operations/supply-chain/procurement/set-up-external-catalog-for-punchout

The steps needed to set this up:
  1. Open Procurement and sourcing > Catalogues > External catalogues
  2. Create a new entry, map it against a vendor, and select one or more the procurement category that will be used. If you only have one procurement category, then all items returned from the vendor’s website will be mapped to that category, otherwise you will have to map the items manually.
  3. The most important setting will be on the message format section, where you will need to configure it so that D365O knows which URL to open, what kind of information that needs to be passed to the vendor’s website. You should contact your vendor (which supports PunchOut eProcurement) to get the information.
  4. Some dynamic information can be configured in the Extrinsics section. Currently the possible dynamic value options are user email, User name, and random value.
  5. You can then click “Validate settings” to make sure that you can open the vendor’s website without any error.
  6. After that, you can then activate the catalogue.
Once you’ve done all that, in the purchase requisition form, you can click the “External catalogues” button on the line section, which will open a dialog where you can choose which vendor’s website that you want to open (based on the configuration that you’ve done on the external catalogues form).

When you click the button, you will get a prompt that says that you will now be redirected to an external site, and after you click OK, you should see the vendor’s website.

You can then place items into the shopping basket, and when you have completed the check out, it will take you back to D365O which gives you the chance to review the order. You can remove lines that you don’t need, or you can choose a different procurement category if needed.


When you click the Add to requisition button at the bottom of the page, it will then transfer the lines into the purchase requisition.
Few important notes:
  • The unit of measurements from the vendor’s website need to exist in D365O, otherwise the lines will be ignored in the validate shopping cart form.
  • The vendor’s website must support TLS v1.2 as D365O environments in tier 2 or higher enforce it.
  • There is a bug (in application 7.2 and 7.3) where it is expecting an element called “SupplierPartAuxiliaryId” in the XML response from the vendor’s website, which the value from that element is not being used in D365O. The bug will only become an issue if the XML response from the vendor’s website do not have that element.
    KB4094740 can be installed to resolve the issue.

Thursday, March 15, 2018

D365O mobile workspace

Starting from platform update 4, D365O introduces the ability to create mobile workspaces which can be loaded from Microsoft Dynamics 365 Unified Operations app, which is available for Android and iOS devices.

The easiest way to learn this is to follow the video tutorials from
https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/mobile-apps/platform/mobile-platform-home-page

Although it looks like very easy to do, you might need to create some forms to make it easier to add the fields into the workspace. If you take a look on Accounts Payable Mobile or SCM Mobile App models, you'll see that Microsoft does that in order to build the standard mobile workspaces.

For PO approval workspace, you can see this page for information on which hotfixes that you need to install.

So with that workspace, there is a new form called PurchMobileOrdersAssignedToMe, which basically is a simple list with very few header information such as PO number, order account and name. The interesting part is there are some checkboxes to indicate if that PO record should have which workflow buttons can be enabled or not.

        public boolean isMenuItemEnabled(WorkflowWorkItemTable _workItem, str _menuItemName)
        {
            container menuItemsContainer;
            str messageText;
            str instruction;

            [menuItemsContainer, messageText, instruction] = SysWorkflowFormControls::getActionBarContentForWorkItem(_workItem);

            return conFind(menuItemsContainer, _menuItemName) > 0;
        }

        public display boolean purchTableApprovalApproveEnabled(WorkflowWorkItemTable _workItem)
        {
            return this.isMenuItemEnabled(_workItem, menuItemActionStr(PurchTableApprovalApprove));
        }

        public display boolean purchTableApprovalDelegateEnabled(WorkflowWorkItemTable _workItem)
        {
            return this.isMenuItemEnabled(_workItem, menuItemActionStr(PurchTableApprovalDelegate));
        }


These checkboxes then get used in the logic of the workspace to show and hide the action buttons. The logic .js file is like this (I put couple of comments to explain what the line does):

function main(metadataService, dataService, cacheService, $q) {
return {
appInit: function() {
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableApprovalApproveEnabled', { hidden: true }); //to hide the ApproveEnabled checkbox
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableApprovalDelegateEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableApprovalRejectEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableApprovalRequestChangeEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableTaskCompleteEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableTaskDelegateEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableTaskRequestChangeEnabled', { hidden: true });
            metadataService.configureControl('Orders-assigned-to-me', 'PurchTableTaskReturnEnabled', { hidden: true });

            metadataService.hideNavigation('Select-user');

metadataService.addLink('Order-details', 'Header-accounting-distribution', 'header-accounting-distribution', 'Accounting distribution', false);
metadataService.addLink('Order-line-details', 'Line-accounting-distribution', 'line-accounting-distribution', 'Accounting distribution', false);

            metadataService.configureControl('Header-accounting-distribution', 'Grid', { nonEntityProjection: true });
            metadataService.configureControl('Line-accounting-distribution', 'Grid', { nonEntityProjection: true });
            metadataService.configureControl('Order-details', 'LineGrid', { ListStyle: 'Card' });
},
        pageInit: function (pageMetadata, params) {
if (pageMetadata.Name == 'Order-details') {

                metadataService.configureAction('Approve', { visible: false });  //to hide the Approve action/button
                metadataService.configureAction('Reject', { visible: false });
                metadataService.configureAction('Request-change-1', { visible: false });
                metadataService.configureAction('Delegate-approval', { visible: false });

                metadataService.configureAction('Complete-task', { visible: false });
                metadataService.configureAction('Return', { visible: false });
                metadataService.configureAction('Request-change', { visible: false });
                metadataService.configureAction('Delegate-task', { visible: false });

                var entityContextParts = params.pageContext.split(':');
                var data = dataService.getEntityData(entityContextParts[0], entityContextParts[1]);

                var workflowWorkItemRecord = data.getPropertyValue('WorkflowWorkItemTable');
if (workflowWorkItemRecord)
{
var workflowWorkItemData = dataService.getEntityData("WorkflowWorkItemTable", workflowWorkItemRecord);

var approveVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableApprovalApproveEnabled') == 1);
var rejectVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableApprovalRejectEnabled') == 1);
var requestChangeVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableApprovalRequestChangeEnabled') == 1);
var delegateVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableApprovalDelegateEnabled') == 1);

var completeVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableTaskCompleteEnabled') == 1);
var returnTaskVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableTaskReturnEnabled') == 1);
var requestChangeTaskVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableTaskRequestChangeEnabled') == 1);
var delegateTaskVisible = Boolean(workflowWorkItemData.getPropertyValue('purchTableTaskDelegateEnabled') == 1);
}
else
{
var approveVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableApprovalApproveEnabled').value == 1);
var rejectVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableApprovalRejectEnabled').value == 1);
var requestChangeVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableApprovalRequestChangeEnabled').value == 1);
var delegateVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableApprovalDelegateEnabled').value == 1);

var completeVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableTaskCompleteEnabled').value == 1);
var returnTaskVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableTaskReturnEnabled').value == 1);
var requestChangeTaskVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableTaskRequestChangeEnabled').value == 1);
var delegateTaskVisible = Boolean(data.getPropertyValue('WorkflowWorkItemTable/purchTableTaskDelegateEnabled').value == 1);
}

                metadataService.configureAction('Approve', { visible: approveVisible });
                metadataService.configureAction('Reject', { visible: rejectVisible });
                metadataService.configureAction('Request-change-1', { visible: requestChangeVisible });
                metadataService.configureAction('Delegate-approval', { visible: delegateVisible });

                metadataService.configureAction('Complete-task', { visible: completeVisible });
                metadataService.configureAction('Return', { visible: returnTaskVisible });
                metadataService.configureAction('Request-change', { visible: requestChangeTaskVisible });
                metadataService.configureAction('Delegate-task', { visible: delegateTaskVisible });
}
}
};
}



Wednesday, December 20, 2017

D365 task recorder - capture screenshots

To enable the "capture screenshots" function in the D365 task recorder, you have to use Chrome and install the “D365 for Finance and Operations Task Recorder” Chrome plugin.

This unfortunately is not yet mentioned at all in the Dynamics 365 for Finance and Operations documentation.

Wednesday, December 13, 2017

How to increase ItemId size in D365

You will need to increase the length of ItemIdBase and EcoResProductNumber data types.

EcoResProductNumber data type is used in one of the staging table for a data entity.

Friday, November 3, 2017

How to add a new operating unit type

For AX2012, you can follow the steps in this link https://msdn.microsoft.com/en-us/library/gg989762.aspx
Couple of important points:

  1. The new enum value must use the immediate next number for that enum (you should not skip any number)
  2. The new enum name drives the view name that you need to create. AX will look for a view with "DimAttribute" prefix after the enumeration name.
    For example, if the new enum name is OMBranch then the view name must be DimAttributeOMBranch.

For D365, it is quite similar to the steps in AX2012, however in the view it should have a method called registerDimensionEnabledTypeIdentifier and you should use the view name that you just created. This will then add the new operating unit as a dimension type.

Keep in mind that in D365, there are 3 additional operating unit types (branch, rental location, region) that are part of the Fleet Management model, which is actually a sample model and doesn't get installed by default to tier 2 or production environments.

Wednesday, November 1, 2017

Management Reporter scripts

Few scripts that can be used to check the MR integration tasks:

Check all integration tasks that have been executed:

--List details about each DDM task: state, progress, last...
--List details about each DDM task: state, progress, last/next runtime in local time, and the interval that each runs 
--Status 5=Success; 3=Running; 6=Cancelled
select CIG.[Description] 
, STK.[Name] 
, STS.[Progress] 
, CASE STS.[StateType]  
WHEN 3 THEN 'Processing' 
WHEN 5 THEN 'Complete' 
WHEN 7 THEN 'Error' 
END AS StateType
, DATEADD(minute, DATEDIFF(minute,GETUTCDATE(),GETDATE()), STS.[LastRunTime]) as LocalLastRunTime 
, DATEADD(minute, DATEDIFF(minute,GETUTCDATE(),GETDATE()), STS.[NextRunTime]) as LocalNextRunTime 
, CM.[ContinueOnRecordError] 
, STRG.[Interval] 
, CASE STRG.[UnitOfMeasure] 
WHEN 2 THEN 'Minutes' 
ELSE 'Seconds' 
END AS IntervalTiming
, CASE STRG.[IsEnabled] 
WHEN 1 THEN 'Enabled' 
ELSE 'Disabled' 
END AS NameStatus
from [Connector].[Map] CM with (nolock) 
inner join [Scheduling].[Task] STK with (nolock) on STK.[Id] = CM.[MapId] 
inner join [Scheduling].[TaskState] STS with (nolock) on STK.[Id] = STS.[TaskId] 
inner join [Connector].[IntegrationGroup] CIG with (nolock) on CIG.[IntegrationId] = STK.[CategoryId] 
inner join [Scheduling].[Trigger] STRG with (nolock) on STK.[TriggerId] = STRG.[Id] 
order by CIG.[Description], STK.[Name]


Check the tasks outcomes

select CIG.[Description], ST.[Name], SM.[Text], 
DATEADD(minute, DATEDIFF(minute,GETUTCDATE(),GETDATE()), SL.[StartTime]) as LocalStartTime, 
DATEADD(minute, DATEDIFF(minute,GETUTCDATE(),GETDATE()), SL.[EndTime]) as LocalEndTime, 
SL.[TotalRetryNumber], SL.[IsFailed], STT.[Name] as TaskType 
from [Scheduling].[Log] SL with (nolock) 
inner join [Scheduling].[Task] ST with (nolock) on SL.TaskId = ST.Id 
inner join [Scheduling].[Message] SM with (nolock) on SL.Id = SM.LogId 
inner join [Scheduling].[TaskType] STT with (nolock) on ST.TypeId = STT.Id 
inner join [Connector].[IntegrationGroup] CIG with (nolock) on CIG.[IntegrationId] = ST.[CategoryId] 
order by SL.[StartTime] desc


Check the current integration activities

SELECT sqltext.TEXT, 
req.session_id, 
req.status, 
req.command, 
req.cpu_time, 
req.total_elapsed_time 
FROM sys.dm_exec_requests req 
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS sqltext