Showing posts with label D365. Show all posts
Showing posts with label D365. Show all posts

Friday, September 10, 2021

Batch framework contention reduction

 "Batch framework contention reduction" feature has been added since PU31 and something that I would suggest to be enabled at all times.

This feature is especially useful when using batches that generates additional runtime tasks (ie. bulk sales posting for example). 

In some scenario there could be deadlock issues that could cause the runtime tasks not to run, however the batch (header) status would still be set to Ended. Let's hope Microsoft will change this to Error in the future versions - as otherwise nobody would even be aware of this issue happening.

After enabling the feature, we haven't noticed the deadlock issue happening anymore.

Some more explanation about the feature is available at this link

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


Monday, February 4, 2019

Chain of Command - avoiding the next call

This is possibly something that needs to be used very sparingly if at all, but apparently there is a way (up until the latest version - v8.1) to avoid calling the next() call when using chain of command.

The key is to use double nested conditions:

//this will not work "call to 'next' should be done only once and unconditionally"
    protected void chooseLinesPackingSlip(boolean _append)
    {
       If (true)
{
           this.somethingElse(_append);
           return;
       }

       next chooseLinesPackingSlip(_append);

    }

//this will work
    protected void chooseLinesPackingSlip(boolean _append)
    {
       If (true)
{
    if (true)
    {
               this.somethingElse(_append);
               return;
           }
       }

       next chooseLinesPackingSlip(_append);

    }

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.

Sunday, October 8, 2017

How to get the field type/size and mandatory fields out of data management import/export project

Whenever we do data migration tasks, there always a need to know the field type/size out of the data entities that will be used, and also to identify which fields are the mandatory ones. Unfortunately in Dynamics 365 for Operation, there is no easy way to get this information from the data management workspace.

I found out that there are "legacy" DMF forms from AX2012 that were upgraded to Dynamics 365 for Operation, however they are not exposed through menus or buttons. These particular forms contain the entity "attributes" and here are the steps to access the forms:

  1. Open D365O and in the url, replace the mi value to DMFDefinitionGroup
    Usually when you first open D365O, the url will be like
    https://<axurl>/?cmp=USMF&mi=DefaultDashboard
    This needs to be changed into
    https://<axurl>/?cmp=USMF&mi=DMFDefinitionGroup
  2. It will then show a form where you can select the import/export project (or back in AX2012 it was called DMF definition group). Select the record and click the "Entities" button
  3. Highlight one of the data entities in the project, and click the "Entity attributes" button
  4. You will then see a form that displays all the entity fields, and shows the field type and size, as well indicate which fields are mandatory


Wednesday, August 30, 2017

Testing Dynamics 365 for Operation Recurring Integrations with Postman

Continuing on the last post, let's do some tests on the D365O recurring integrations with Postman.

Initial setup

We still need the "GetToken" call as before. Please check on the previous post for information on how to set this up.

Next on the D365O data management workspace, we need to create an import and an export projects. To make it simple, let's use the "Customer groups" entity.

Create an import project as usual, and you need to create a sample of import file. Once you click the "Upload" button, then click the "Create recurring data job" button at the top. You will then need to:

  • Specify a name
  • Specify the application ID, and tick the checkbox beside it
    (This is the client ID that we use in the GetToken call)
  • Click the "Set processing recurrence" and set the job recurrence
  • Click the "Set monitoring recurrence" and set the monitoring recurrence
  • Keep note of the job ID
  • Click OK
Next, create an export project as usual. After you click the "Add entity" button, then click the "Create recurring data job" button at the top. You will then to do the same as above.

Send a file to the recurring import job

Open Postman, and execute the "GetToken" call to get the access token. Then create a new POST call with this url https://<axurl>/api/connector/enqueue/<activity id>?entity=<entity name>

Activity ID is the job ID that was displayed when you created the recurring jobs.


Then click on the Body tab, choose Binary, and click the "Choose Files" button to choose the import file. After that just click the "Send" button.

If it's successful, it will return with a job ID and HTTP 200.

You then can inquire the status of the job by doing a GET call to this url: https://<axurl>/api/connector/jobstatus/<activity_id>?jobId=<job_id> 
The result will then show you the job status, including the job started/completed date time and execution logs

Please note that you always need to add the authorisation and the access token in the message header with every call.

Receive a file from the recurring export job

Open Postman, and execute the "GetToken" call to get the access token. Then create a new GET call with this url https://<axurl>/api/connector/dequeue/<activity id>
If it's successful, it will return with a download location and HTTP 200.

Then you'll need to create a new GET call and use the download location as the URL, and then instead of clicking the "Send" button, you'll need to click the "Send and Download" button. Postman will then display a dialog where you can use to select the location where you want the file to be saved.

After downloading the file, you should send an acknowledgement:
https://<axurl>/api/connector/ack/<activity_id>
{
  "CorrelationId": "<CorrelationId>",
  "PopReceipt": "<PopReceipt>",
  "DownloadLocation": "<DownloadLocation>"
}

The body (correlationId, popReceipt, downloadLocation) should be the same as the body from the original dequeue request.

Please note that you always need to add the authorisation and the access token in the message header with every call.

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.