Wednesday, June 29, 2022

D365 FO Chain of Command (COC ) & Event Handler

Chain of Command (CoC) enables strongly typed extension capabilities of public and protected methods. It is an amazing piece of development capability that allows technical consultants to extend the application avoiding over-layering.


We can write the COC’s for individual objects by specifying the object name in the [ExtensionOf(ObjectName)] on the class header. 

✓ For Class COC we use [ExtensionOf(classStr(ClassName))] 

✓ For Form COC we use [ExtensionOf(formStr(FormName))] 

✓ For FormControl COC we use [ExtensionOf(formControlStr(FormName,FormControlName))] 

✓ For FormDataSource COC we use [ExtensionOf(formDatasourceStr(FormName,FormDSName))] 

✓ For FormDataFieldStr COC we Use [ExtensionOf(formDataFieldStr(FormName,FormDSName,Field))]

 ✓ For Table COC we use [ExtensionOf(tableStr(TableName))] 

✓ For Data Entity COC we use [ExtensionOf(tableStr(DataEntityName))]


COC for Form Data Field:

[ExtensionOf(formDataFieldStr(SalesTable, SalesLine, ItemId))]

final class SalesTable_SalesLineItemId_Extension

{

public void modified()

{

     FormDataObject formDataObject = any2Object(this) as FormDataObject;

     FormDataSource formDataSource = formDataObject.datasource();

    SalesLine salesLine;

    next modified(); 

    salesLine =     formDataSource.cursor(); 

    salesLine.Name =   InventTable::find(salesLine.ItemId).itemName();

}

}

Event handler for Form Data Field

[FormDataFieldEventHandler(formDataFieldStr(SalesTable, SalesLine,

ItemId), FormDataFieldEventType::Modified)]

public static void ItemId_OnModified(FormDataObject sender,

FormDataFieldEventArgs e)

{

    FormRun formRun = sender.datasource().formRun();

    SalesLine salesLine = formRun.dataSource(formdatasourcestr(SalesTable, SalesLine)).cursor();

    formRun = sender.datasource().formRun();

}


COC for Form Datasource: 

[ExtensionOf(formDatasourcestr(SalesTableListPage,SalesTable))]

final class TM_SalesTable_DataSource_Extension

{

 public int active()

 {

 int ret = next active();

 SalesTable SalesTable_ds = this.cursor();

 if(SalesTable_ds.SalesStatus == SalesStatus::Invoiced)

 {

        this.formRun().design().controlName(formControlStr(SalesTableListPage,Form ButtonControl1)).enabled(true);

 }

 else

 {

    this.formRun().design().controlName(formControlStr(SalesTableListPage,Form   ButtonControl1)).enabled(false);

 }

 return ret;

 }

}

Event handler for Form Datasource: 

[FormDataSourceEventHandler(formDataSourceStr(SalesTableListPage,

SalesTable), FormDataSourceEventType::Activated)]

 public static void SalesTable_OnActivated(FormDataSource sender,

FormDataSourceEventArgs e)

 {

  SalesTable SalesTable = sender.cursor() as SalesTable;

FormRun element = sender.formRun();

FormCheckBoxControl SalesTable_TMCreditLimitChecked =

element.design(0).controlName("SalesTable_TMCreditLimitChecked");

FormCheckBoxControl SalesTable_TMCreditLimitExceeded =

element.design(0).controlName("SalesTable_TMCreditLimitExceeded");

FormMenuButtonControl WorkflowActionBarButtonGroup =

element.design().controlName("WorkflowActionBarButtonGroup");

   if(SalesTable.SalesStatus == SalesStatus::Invoiced)

   {

sender.formRun().design().controlName(formControlStr(SalesTableListPage,FormButtonControl1)).enabled(true);

   }

  else

   {

sender.formRun().design().controlName(formControlStr(SalesTableListPage,FormButtonControl1)).enabled(false);

   }

 }

COC for Form: 

[ExtensionOf(formStr(SalesTable))]

final class TM_SalesTable_Form_Extension

{

  public void init()

  {

FormRun formRun = this as FormRun;

FormDataSource SalesTable_DS = formRun.datasource(FormDatasourceStr(SalesTable,SalesTable));

SalesTable _salestable = SalesTable_DS.Cursor();

FormControl custAccount =

formRun.design().ControlName(FormControlStr(SalesTable,SalesLine_itemid));

next init();

   }

}


Event Handler for Form:

[PostHandlerFor(formStr(SalesTable), formMethodStr(SalesTable, init))]

public static void SalesTable_Post_init(XppPrePostArgs args)

{

   FormRun form = _args.getThis();

   FormDesign design = form.design();

   FormControl itemid = design.controlName(formControlStr(SalesLine,Salesline_itemid));

}

COC for Form Control:
[ExtensionOf(formcontrolstr(SalesTableListPage,buttonUpdateInvoice))]
final class TM_SalesTableListPage_Control_Extension
public void clicked()
{
FormButtonControl _ FormButtonControl= any2Object(this) as
FormButtonControl;
FormDataSource salestable_DS=FormButtonControl.FormRun().Datasouce(tableStr(SalesTable));
SalesTable _salestable = salestable_DS.cursor();
UserGroupList UserGroupList;
UserId userId = curUserId();
select * from UserGroupList where UserGroupList.groupId =="Post" 
        && UserGroupList.userId == userId;
  if(!UserGroupList)
  {
  throw Error("User not allowed to generate Invoice"); }
  }
next clicked();
}
}

Event Handler for Class:


[FormControlEventHandler(formControlStr(SalesTableListPage,
buttonUpdateInvoice), FormControlEventType::Clicked)]
public static void buttonUpdateInvoice_OnClicked(FormControl sender,
FormControlEventArgs e)
{
Args args = new Args();
FormCommandButtonControl callerButton = sender as FormCommandButtonControl;
FormRun form = callerButton.formRun();
FormDataSource SalesTable_ds =
form.dataSource(formDataSourceStr(SalesTableListPage, SalesTable)) as FormDataSource;
SalesTable _salestable = SalesTable_ds.cursor();


COC for Class:
[ExtensionOf(classStr(SalesFormLetter))]
final class TM_SalesFormLetter_Extension
{
 public static void main(Args args)
 {
  RefRecId record = args.record().RecId;
  DocumentStatus parmEnum = args.parmEnum();
  Object callerForm = args.caller();
  MenuItemNameAction callerMenuItem = args.menuItemName();
  SalesTable salesTable = SalesTable::findRecId(record);
 }
}
[ExtensionOf(classStr(SalesFormLetter))]
final class TM_SalesFormLetter_Extension
{

 public boolean validate(Object _calledFrom)
 {

 DocumentStatus doc;

  salesTable salesTablelocal;
  boolean result,processstatus,checkvalidate,deliverytermcheck;
  processstatus=true;
  Dialogbutton db;
  checkvalidate= next validate(_calledFrom);
 }

COC for Table:
[ExtensionOf(tableStr(SalesTable))]
final class TMSalesTable_Extension
{
 public boolean validateWrite()
 {
  boolean ok = true;
  ok = next validateWrite();
  if ( this.TMSaleDiscPer > 100)
  ok = checkFailed("Discount percentage can not be more than 100%");
  return ok;
 }

Event Handler for Table:

[DataEventHandler(tableStr(SalesTable), DataEventType::Inserting)]
public static void SalesTable_onInserting(Common sender, DataEventArgs e)
 {
  SalesTable salesTableObj = sender as SalesTable;
  SalesAgreementHeader salesAgreement;
  AgreementHeaderDefault agreementHeaderDefault;
  SalesAgreementHeaderDefault SalesAgreementHeaderDefault;
  str tmpVal;
  TM_CustomDocumentListParms docList = TM
_CustomDocumentListParms::find(TM_DocsCustom::SalesConfirmation,
salesTableObj.TaxGroup);
if(docList && docList.TermsAndConditions &&
docList.TermsAndConditions != "" &&
salesTableObj.SalesId && salesTableObj.SalesId != "" &&
salesTableObj.TM_TermsAndConditions == "")
{
  salesTableObj.TM_TermsAndConditions = docList.TermsAndConditions;
}
 } 

COC for Data Entity:
 [ExtensionOf(tableStr(SalesOrderLineVEntity))]
final class SalesOrderLineV2Entity_Extension
{
  public boolean validateWrite()
  {
  boolean ret = next validateWrite();
  if (this.DeliveryAddressCountryRegionID != "US")
  {
  ret = checkFailed("Delivery Address is not US.");
  }
  return ret;
  }
}

Tuesday, June 28, 2022

D365 FO Create Custom Web Service

 How to create basic custom Web Service in D365FO.

1- Create a class with name of IntegrationService  and write a method Test.


2  Add new Service and associate your class.



 

3- Add method of your class in service operations node you want to expose.



4- Add Service group and associate your service with the service group. 


A - Set the service property Auto deploy= YES


B- Add service to Service Group



5- Perform Build & Sync and verify from browser.

Service URL 
https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/[ServiceGroupName]/[ServiceName]/[MethodName]

Sample
https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/IntegrationServiceGroup/IntegrationService/Test



Monday, June 27, 2022

D365 FO Azure App Registration

 

To register a new application using the Azure portal

1.  Sign in to the Azure portal. 

2 - Go To Manage Azure Active Directory



3- In the left-hand navigation pane, 
Click App registrations
click New application registration.

 


 

4 - Enter Name for App Registration and Choose Supported Account Type

 

The application will visible like the below screenshot after creating the process completely .

You will find ( Client ID , Tenant ID .. etc)


 5- Application Owner

Add only if you want to make someone an owner. Otherwise, skip the steps.




 6- Application Permission

Below is the permission which is required to activate.




 

  

Following are the further detail of each permission. Please check the below screenshot.

 

7- Keys

For Web API we need a secret key with the help of this key client will get the Authentication.

After generation of the key, please save the key because it will not visible again to you.


Please follow Link to use Dynamics 365 Finance & Operation WebAPI with POST MAN


 

 

D365 Finance & Operation Web Service with POST MAN

 

Dynamics 365 Finance & Operation WebAPI with POST MAN


Dynamics 365 Finance & Operation WebAPI with POSTMAN


Most of the time we used 3rd Party tools like fiddler, Post Man and SoupUI as clients to consume web services.

In this blog i will explain, How can we use POSTMAN to consume Web-API by using Oauth2 Azure Authentication.


1 Download the POSTMAN from this link.

2 Register The Azure Application (Web API). You can follow this link
 
3 Once App registration completed Enter Application ID in D365FO










4 Open POST man and navigate to Authorization Tab and select OAuth2 as Authentication Type.

Reference screenshot





5 Click on Get New Access Token. A Popup will appear.








6 Fill the required fields Like below

Call Back URL > You can't change this.
Token Name (As per your requirement)
Auth URL

https://login.windows.net/YourTenant.com/oauth2/authorize?resource=https://EnvironmentURL.operations.dynamics.com

Access Token URL
https://login.windows.net/YourTenant.com/oauth2/token?resource=https://EnvironmentURL.operations.dynamics.com

You can find the tenant from AX as well. Open D365FO Click on the Setting icon. you can find it the on the top right of the screen then click on about.







Client ID > Application ID You Registered On Azure Portal
Secret Key > Enter the secret key you generate against your Azure Application.







7 If you don't provide a secret key then a popup window will appear for login.


 



8 Click on Request Toke. Within 4 to 5 seconds you will get the Azure Authentication Token.

9  Now select the Header value from the drop-down and click on USE token to add this token in your request.




10 Navigate to Header Tabs for the verification of the Authorization token in your request



11- Enter the Complete service URL and click on send. in my case, I have called test2 service.

Service URL 
https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/[ServiceGroupName]/[ServiceName]/[MethodName]

Sample
https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/IntegrationGroup/IntegrationService/test2 





Please check the above screenshot. You will find the successful result 

Saturday, June 25, 2022

Dynamics 365 FO: Custom Scripts - Run custom X++ scripts with zero downtime

 

Run custom X++ scripts with zero downtime

This feature lets you upload and run deployable packages that contain custom X++ scripts without having to go through Microsoft Dynamics Lifecycle Services (LCS) or suspend your system. Therefore, you can correct minor data inconsistencies without causing any disruptive downtime.

All deployable packages that are uploaded into the system go through a mandatory workflow. As a safety precaution, and to help ensure segregation of duties, the user who uploads a deployable package isn't allowed to approve it for the next steps in the workflow. Another user must approve it. However, after the package is approved, the user who uploaded it will be allowed to complete the remaining steps.

The system requires that all deployable packages go through a test run. Before the script will be allowed to run on production data, a user must validate that the output is correct by selecting Accept test log. If the output isn't correct, the user must mark the package as failed by selecting Abandon. In this case, the script won't be allowed to run on production data.

Every uploaded package is saved in the system and goes through a defined workflow of events. For each event, the system keeps a log that includes a timestamp and the identity of the person who performed the event. In this way, the system ensures that there is an audit trail.

 

Your deployable package must contain exactly one runnable X++ class. In other words, it must have one class that includes a method that has the following signature.

The following code example shows how a deployable package can be structured.


Code example

The following code example shows how a deployable package can be structured.



Best practices

The following list describes some best practices for successfully writing, implementing, and running a script. The list isn't exhaustive, and it should be considered only guidance.

·        Do write a success message at the end of the script. In this way, you will be able to see that the script ran without exceptions.

·        Do add explicit handling of the transaction scope.

·        Do use existing business logic, such as update() methods, but do not bypass business logic by using doUpdate()doInsert(), and doDelete() methods. This approach will help ensure that dependent data is handled correctly. It will also significantly reduce the risk of further data inconsistencies.

·        Do assert the company context. This approach will expose common mistakes as a script runs. For example, it will reveal whether the script is being run in the wrong company.

·        Do assert that the number of affected records matches your expectations. This approach will reveal whether data unexpectedly shifted in the system while the script was being prepared.

·        Do use unique class names for each script (for example, by including a reference to a work item in the name). This approach will prevent name clash issues when you upload the script. If a new iteration of a script is required, be sure to give it a new name.

·        Do test each script in a non-production environment first. Test for the intended impact and for unintentional side-effects on related data. Ensure that all business processes that might be affected can be successfully and fully completed afterwards.

Create a deployable package

Assign duties to users to control access

This feature provides the following duties. Admins can use these duties to help control access to the feature.

·        Maintain custom scripts – This duty grants the ability to upload, test, verify, and run custom X++ scripts in environments (user acceptance testing [UAT] and production).

·        Approve custom scripts – This duty grants the ability to approve an uploaded custom X++ script. Approval is a mandatory step before any script can be tested, verified, and run.

 

Upload and run a deployable package

Use the following procedure to upload and run a script.

    1.     In your Finance and Operations app, go to System administration > Periodic tasks > Database > Custom scripts.

    2.     Select Upload.

 


3.     Select the deployable package that you created as described earlier in this article. You will be prompted to specify the purpose of the script.

o     

 

The script must now be approved by a user other than the user who uploaded it. The approver must follow these steps:

1-    Go to System administration > Periodic > Data base > Custom scripts.

2-    Select the script to approve, and then select Details.

3-    On the Action Pane, on the Process workflow tab, in the Start group, select Approve or Reject. If you select Approve, the script is marked as approved and is unlocked for testing. If you select Reject, the script is locked. In both cases, the event is logged, and a copy of the script is kept in the system.





     The script must be tested to ensure that it does what it's intended to do. The tester can be the same as the uploader or the approver, or it can be a third user who has the required permissions. The tester must follow these steps:

1.     Go to System administration > Periodic > Data base > Custom scripts.

2.     Select the script to test, and then select Details.

3.     On the Action Pane, on the Process workflow tab, in the Test group, select Run test. The script is run inside a temporary transaction that the system will automatically abort while it collects various logs and SQL statements.

4.     When the script has finished running, review the logs, and verify that the results meet your expectations. Follow one of these steps:

§  If you're satisfied with the test result, select Accept test log in the Test group on the Process workflow tab of the Action Pane to allow the script to be run. The event log will reflect the fact that the script was tested, and it will indicate who tested it and when.

§  If you aren't satisfied with the test result, select Abandon in the End group on the Process workflow tab of the Action Pane to prevent the script from being run. The system will keep a copy of the script together with a log of its history.

 



1.     When you're sure that the script meets your expectations, select Run in the Run group on the Process workflow tab of the Action Pane to run it. This command does the same thing as the previous test run, but the transaction will be committed at the end.

2.     After the script has finished running, check the result, and confirm that the script worked as you intended. Follow one of these steps:

o   If you're satisfied with the result, select Purpose resolved in the End group on the Process workflow tab of the Action Pane. The event log will reflect the fact that the script ran successfully, and it will indicate who verified the script and when. The script is saved, but it's now locked and can't be run again.

o   If you aren't satisfied with the result, select Purpose unresolved in the End group on the Process workflow tab of the Action Pane. The event log will reflect the fact that the script failed to fulfill its intended purpose, and it will indicate who ran the script and when. The script is saved, but it's now locked and can't be run again. However, the system doesn't automatically undo the script action. You might have to write, import, and run a new script to undo the effect that the failed script had on your system.

Your selection in the last step defines the final state for the script. You can repeat the process as you require.

 

 





 

 Ref , Ref2