Showing posts with label AX7. Show all posts
Showing posts with label AX7. Show all posts

Thursday, August 12, 2021

Generating D365 URL for a particular menu item

MenuItemName menuItemName = menuItemDisplayStr(WorkflowWorkListAssignedToMe);

var generator = new Microsoft.Dynamics.AX.Framework.Utilities.UrlHelper.UrlGenerator();

generator.EncryptRequestQuery = true;

generator.HostUrl = SysWorkflowHelper::getClientEndpoint();

generator.Company = curExt();


generator.MenuItemName = menuItemName;

generator.Partition = getCurrentPartition();


var fullURI = generator.GenerateFullUrl();

Info(fullURI.AbsoluteUri);


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);  
     }  
   }  
 }  

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.

Friday, August 4, 2017

Accessing Dynamics 365 for Operations ODATA services with Fiddler

Before you start, you will need to do a new application registration first through Azure portal to get the Client ID and the Client Secret key.

After that, download and install Fiddler from http://www.telerik.com/fiddler

Open Fiddler and go to the Composer [tab] and Options [tab]
Enable "Inspect session" and "Fix content-length header"

Then open the Scratchpad [tab] and paste this

POST https://login.windows.net/<tenant>/oauth2/token HTTP/1.1 
Content-Type: application/x-www-form-urlencoded
Host: login.windows.net

resource=https://<axurl>&client_id=<client-id>&client_secret=<client-secret>&grant_type=client_credentials

Replace the <tenant>, <axurl>, <client-id> and <client-secret> with the valid values, then select/highlight the statements and click the Execute button
The view should be switched to the Inspector [tab] and you can click on the Raw [tab] to see the raw result.
Copy the value of the access_token as you will need this for the subsequent service calls.


If you don't have the client secret key, but you have the username and password, you can use this to get the token:
POST https://login.windows.net/<tenant>/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: login.windows.net

resource=https://<axurl>&client_id=<client-id>&authorityURL=https://login.windows.net/<tenant>&username=<username>&password=<password>&grant_type=password
Replace the <tenant>, <axurl>, <client-id>, <username> and <password> with the valid values, then select/highlight the statements and click the Execute button
The view should be switched to the Inspector [tab] and you can click on the Raw [tab] to see the raw result.
Copy the value of the access_token as you will need this for the subsequent service calls.


After we get the access token, let's try to call the LedgerJournalHeaders service.

To do a GET call:

GET https://<axurl>/data/LedgerJournalHeaders HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>

Replace the <axurl> and <access_token> with the valid values, then select/highlight the statements and click the Execute button.
The view should be switched to the Inspector [tab] and you can click on the JSON [tab] to see the result.

To insert a new journal header:

POST https://<axurl>/data/LedgerJournalHeaders HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.LedgerJournalHeader",
"dataAreaId":"USMF",
"JournalName":"GenJrn",
"Description":"Test journal"
}

Replace the <axurl> and <access_token> with the valid values, then select/highlight the statements and click the Execute button.
The view should be switched to the Inspector [tab], if the insert is successful then it will return the newly inserted record as the result.


To update a journal header:

PATCH https://<axurl>/data/LedgerJournalHeaders(JournalBatchNumber='<journalNumber>',dataAreaId='<dataAreaId>') HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.LedgerJournalHeader",
"dataAreaId":"USMF",
"Description":"Edited journal description"
}

Replace the <axurl>, <access_token>, <journalNumber> and <dataAreaId> with the valid values, then select/highlight the statements and click the Execute button.
Please note that D365O by default will get the values from the integration user's default company, regardless of the dataAreaId that you're actually specifying on the call. If you need to access a record on a different company, then you'll need to add ?cross-company=true

If the update is successful then it will return HTTP status 204, otherwise it will return HTTP status 400 with the error message.


To delete a journal header:

DELETE https://<axurl>/data/LedgerJournalHeaders(JournalBatchNumber='<journalNumber>',dataAreaId='<dataAreaId>') HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>

Replace the <axurl>, <access_token>, <journalNumber> and <dataAreaId> with the valid values, then select/highlight the statements and click the Execute button.
If the update is successful then it will return HTTP status 204, otherwise it will return HTTP status 400 with the error message.


To insert multiple journal headers in one request:

POST https://<axurl>/data/$batch HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: multipart/mixed; boundary=batch_boundary
Accept: multipart/mixed
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>
 
--batch_boundary
Content-Type: multipart/mixed; boundary=changeset_boundary
 
--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1
POST https://<axurl>/data/LedgerJournalHeaders HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.LedgerJournalHeader",
"dataAreaId":"USMF",
"JournalName":"GenJrn",
"Description":"Test journal 1"
}
 
--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2
POST https://<axurl>/data/LedgerJournalHeaders HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.LedgerJournalHeader",
"dataAreaId":"USMF",
"JournalName":"GenJrn",
"Description":"Test journal 2"
}


To call an ODATA action:

POST https://<axurl>/data/<EntityPublicCollectionName>([EntityKey])/Microsoft.Dynamics.DataEntities.<ActionName> HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access_token>
Host: <axurl>



As you can see, Fiddler can be used as a simple tool to access the D365O services. I think this is easier and faster than having to build custom codes in Visual Studio to do the calls.
However depending how many tests that you'll need to perform, it can be a bit painful to copy paste the access token values for every call.

Credit to Kalle Sõber on his post http://www.k3technical.com/testing-ax7-odata-services-fiddler/ 

In the next post, I will show how to use Postman to call D365O services. Postman has variable system that makes it easier to do the calls so that you don't have to copy paste the access token, like we did just now with Fiddler.