Dynamics Async Await XRM WebApi JavaScript Functions

One problem that Dynamics 365 CE (CRM) JavaScript Web Resource developers may have is they need to make a backend query to the Dataverse in order to perform some operations on the CE form. The technologies used to implement this objective are Dynamics 365 CE / Dataverse, JavaScript Web Resource, and Xrm.WebApi.

I will demonstrate how to utilize the Xrm.WebApi, using the retrieveMultipleRecords method as an example to create a generic reusable function that will maintain a desired execution order. On its own, this method, and others like it, leans toward the implementation of the results inside the method or with a callback function. Later, we will discover some limitations and drawbacks with these approaches.

Implementing inside the method would look something like this:

Javascript-Functions

Resource – https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/retrievemultiplerecords

After successfully receiving the results, loop through the results and implement whatever logic as it seems fit. If this method was needed more than once, it would have to be implemented all over again, most likely using the same code, which is not efficient.

Additionally, you can use callback functions like this:


    var SetValues = function () {
      var options = "?$select=msdyn_projectteamid,_msdyn_project_value,_msdyn_bookableresourceid_value,_enc_feecategoryid_value,enc_discount";
      options += "&$filter=_msdyn_project_value eq " + project_id;
      options += " and _msdyn_bookableresourceid_value eq " + bookable_resource_id;
      Xrm.WebApi.retrieveMultipleRecords("msdyn_projectteam", options).then(handleProjectTeams, handleError);
    }
    
    var handleProjectTeams = function (data) {
    }
    
    var handleError = function (data) {
    }

This is a step in the right direction and would allow the callback function to be reused for different scenarios; it would avoid handling the results in the same way each time, thus creating duplication of code. However, it doesn’t solve the issue of needing to call the retrieveMultipleRecords more than once and having to implement the processing logic again which creates a duplication of code.

Business Logic

Before we review the code, it is worth a quick look at the overall business logic and participating tables. Specifically, I was working with the quick create Time Entry form for Dynamics Project Service. The Time Entry form had a lookup to Bookable Resource and Project. Bookable Resource had a custom column – Fee Category. Project had a custom column – Discount. Project Team Member – a join table between Bookable Resource and Project – had both these custom columns.

The business logic determined that if the Bookable Resource was associated to a Project Team Member record for the corresponding Project, the Fee Category and Discount data would be pulled from that record. If the Project Team Member record did not exist for the Bookable Resource and Project, Fee Category should be pulled from the Bookable Resource record, and Discount would be pulled from the Project record. Here is another way to consider the business rules.



// Bookable Resource is a hidden field
// On create, Bookable Resource is null so need to set
// On change, field is already populated
IF Bookable Resource does not contain data THEN
  Retrieve Bookable Resource with user id
  Set Bookable Resource
END IF

IF Time Entry Project contains data THEN
  IF Time Entry Bookable Resource is part of a Project Team Member for the selected Project THEN
    Retrieve Fee Category and Discount from Project Team Member
    Set Time Entry Fee Category from Project Team Member
    Set Time Entry Discount from Project Team Member
  ELSE
    Retrieve Fee Category from Bookable Resource
    Set Time Entry Fee Category from Bookable Resource
    Retrieve Discount from Project
    Set Time Entry Discount from Project
  END IF
ELSE
  Clear Time Entry Fee Category
  Clear Time Entry Discount
END IF

 

Tables and Columns

Here are the Dataverse tables and contributing columns:

Time Entry

  • Bookable Resource
  • Project
  • Fee Category
  • Discount

Bookable Resource

  • Fee Category

Project

  • Discount

Project Team Member

  • Bookable Resource
  • Project
  • Fee Category
  • Discount

Issue

I needed to create a handful of JavaScript functions to retrieve data from the backend and set a few fields based on various conditions. I ran into an issue where, depending on the criteria, I had to pull the backend data from a different source, and this almost led me down a path of duplicating code to cover all the dependent scenarios. To accommodate good coding practices, I created functions, that called the Xrm.WebApi.retrievMultipleRecords methods, that could be reused. However, initial results were disappointing.

WebApi Results

I observed that the JavaScript function logic would: 1) process lines, 2) call the asynchronous function, and 3) process the remainder of the function logic. However, it would not process according to my desired order.

Imagine we have code that looks something like this:


JS action 1
Asynchronous action 1
JS action 2

Using the developer tools, we would see the results in this order:


JS action 1
JS action 2
Asynchronous action 1

Why? Because the flow logic will call JS action 1, call Asynchronous action 1, but will not wait for it to finish, and then call JS action 2. As a result, JS action 2 would finish before Asynchronous action 1.

This created a problem in that the business rules in place stated that data should be pulled one way if a Project Team Member existed or another way if it didn’t. I discovered a null value for Bookable Resource each time I tested when I knew it existed before the lines of code executed after it.

I needed to find a way to keep the code as clean as possible, prevent a duplication of code (writing similar functionality based on different scenarios), and have it process in the right order.

Xrm.WebApi.retrieveMultipleRecords

Before moving on to the solution, here is Microsoft’s documentation statement regarding Xrm.WebApi:

Provides properties and methods to use Web API to create and manage records and execute Web API actions and functions in model-driven apps.

This means any call using the Xrm.WebApi happens in the background on its own, thus the term asynchronous which means “not existing or happening at the same time.” We can tell from our results above that normal JavaScript functions definitely did not “happen” at the same time as the asynchronous one. Thankfully, there is a simple solution.

Solution

Debajit Dutta came to the rescue with the below article. Thanks, Debajit.

https://debajmecrm.com/how-to-make-xrm-webapi-calls-synchronous-in-dynamics-365-cds/

The heart of the article surrounds these lines of code:

Javascript-Functions

With a proper use of the keywords async and await, you can keep the asynchronous flow of the code in order that you desire. This was a great start to get me to my final destination.

I wanted to change things around and not process inside the function that contained the retrieveMultipleRecords call because I needed to universalize it and use it many times with different purposes. Before I go on, here is a list of all the functions in my final code:

  1. onLoad
  2. onProjectChange
  3. onBookableResourceChange
  4. setValues
  5. getBookableResource
  6. getProject
  7. getProjectTeamMember
  8. setDiscount
  9. setFeeCategory
  10. GetLookupId
  11. GetLookupName
  12. SetLookupValue
  13. StripGUID

1 through 3 are generic functions to call the setValues main function and are needed to communicate from the quick entry Time Entry form to the JavaScript code.

4 is the main function that determines what supporting functions will be called based on available form data (Bookable Resource and Project).

5 through 7 are the Xrm.WebApi functions that retrieve the Dataverse data in the background and pass the results to the calling line.

8 and 9 simply receive the Xrm.WebApi results and sets the appropriate fields on the form.

10 through 13 are black-box functions to write-once-use-many times in client scripting.

This blog will only cover functions 4 through 9.

setValues Function

Below is the basic logic of this function.

Get form context.

If form context exists, continue.

If Bookable Resource does not contain data (on create), call Xrm.WebApi to retrieve Bookable Resource, based on the user’s id and set the Time Entry Bookable Resource.

Time Entry Bookable Resource will now be populated.

If Project does not contain data, clear Fee Category and Discount.

If Project contains data, call Xrm.WebApi to retrieve Project Team Member data, using the Bookable Resource and Project.

If Project Team Member record does exist, set Fee Category and Discount.

If Project Team Member record does not exist, call Xrm.WebApi to retrieve Bookable Resource Fee Category and set Time Entry Fee Category. Call Xrm.WebApi to retrieve Project Discount and set Time Entry Discount.

getBookableResource Function

This function will cover the main point of this blog in that it is calling data asynchronously, but how it is called and used will keep the processing in the desired order.


var getBookableResource = async function (user_id) {
  var options = "?$select=bookableresourceid,_userid_value,_enc_feecategoryid_value";
  options += "&$filter=_userid_value eq " + user_id;
  var data = await Xrm.WebApi.retrieveMultipleRecords("bookableresource", options);
  return data;
} // getBookableResource

Notice the word async right before the word function. This states that the JavaScript function will be processed asynchronously. I prep the async call according to the Xrm.WebApi parameters, using the option variable. The next line takes the Xrm.WebApi results and stores them in the variable named data and returns the data back to the line that called it.

Here is one of the most important points: the word await is set right before the Xrm.WebApi call. The word dictates that the function will act as if it is a synchronous function.

According to Mozilla, “Await expressions make promise-returning functions behave as though they’re synchronous by suspending execution until the returned promise is fulfilled or rejected.”

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

Let us review how we would call and use the function.


var bookable_resource_data;
var brd_cnt = 0;

var bookable_resource_id = GetLookupId(formExecutionContext, "msdyn_bookableresource");

if (bookable_resource_id == null) {
  bookable_resource_data = await getBookableResource(user_id,null);
  brd_cnt = bookable_resource_data.entities.length;

  if (brd_cnt > 0) {
    bookable_resource_id = bookable_resource_data.entities[0]["bookableresourceid"];
    bookable_resource_name = bookable_resource_data.entities[0]["_userid_value@OData.Community.Display.V1.FormattedValue"];
    SetLookupValue(formExecutionContext, "msdyn_bookableresource", bookable_resource_id, bookable_resource_name, "bookableresource");
  }
}

First, we don’t want to call the function unless the Bookable Resource field is blank.

Retrieve the current Bookable Resource value from the lookup field:


var bookable_resource_id = GetLookupId(formExecutionContext, "msdyn_bookableresource");

if (bookable_resource_id == null) {

Call the function that holds the Xrm.WebApi call:


bookable_resource_data = await getBookableResource(user_id,null);

 

Proceed only if there are results:


if (brd_cnt > 0) {

Place retrieved value into a variable that can be used throughout the rest of the code if needed:


bookable_resource_id = bookable_resource_data.entities[0]["bookableresourceid"];

Set the Bookable Resource lookup field, using the SetLookupValue function:


SetLookupValue(formExecutionContext, "msdyn_bookableresource", bookable_resource_id, bookable_resource_name, "bookableresource");

 

getProjectTeamMember Function

After the Bookable Resource is set, we need to determine from which tables to pull our Fee Category and Discount values.

Here is the code for the getProjectTeamMember Xrm.WebAPI call:


var getProjectTeamMember = async function (project_id, bookable_resource_id) {
  var options = "?$select=msdyn_projectteamid,_msdyn_project_value,_msdyn_bookableresourceid_value,_enc_feecategoryid_value,enc_discount";
  options += "&$filter=_msdyn_project_value eq " + project_id;
  options += " and _msdyn_bookableresourceid_value eq " + bookable_resource_id;
  var data = await Xrm.WebApi.retrieveMultipleRecords("msdyn_projectteam", options);
  return data;
} // getProjectTeamMember

Back in the setValues function, below is how we call and use the results:


if (project_id == null || bookable_resource_id == null) {
  formContext.getAttribute("enc_feecategoryid").setValue(null);
  formContext.getAttribute("enc_discount").setValue(null);
}
else if (project_id != null && bookable_resource_id != null) {
  var project_team_member_data = await getProjectTeamMember(project_id, bookable_resource_id);
  var ptm_cnt = project_team_member_data.entities.length;
  if (ptm_cnt > 0) {
    console.info("Project Team Member exists");
    setFeeCategory(formExecutionContext, project_team_member_data);
    setDiscount(formExecutionContext, project_team_member_data);
  }
  else {
    console.info("Project Team Member does not exist");
    setFeeCategory(formExecutionContext, bookable_resource_data);

    var project_data = await getProject(project_id);
    var project_cnt = project_data.entities.length;
    if(project_cnt > 0) setDiscount(formExecutionContext, project_data);
  }
}

If Project does not contain data, clear Fee Category and Discount:


if (project_id == null || bookable_resource_id == null) {
  formContext.getAttribute("enc_feecategoryid").setValue(null);
  formContext.getAttribute("enc_discount").setValue(null);
}

If Project and Bookable Resource contain data, continue.


else if (project_id != null && bookable_resource_id != null) {

Retrieve Project Team Member data:


  var project_team_member_data = await getProjectTeamMember(project_id, bookable_resource_id);
  var ptm_cnt = project_team_member_data.entities.length;

If there is a Project Team Member record, set Fee Category and Discount from the Project Team Member record:


  if (ptm_cnt > 0) {
    console.info("Project Team Member exists");
    setFeeCategory(formExecutionContext, project_team_member_data);
    setDiscount(formExecutionContext, project_team_member_data);
  }

If there is no Project Team Member record, retrieve Fee Category from Bookable Resource and Discount from Project with yet another backend Xrm.WebApi call:


  else {
    console.info("Project Team Member does not exist");
    setFeeCategory(formExecutionContext, bookable_resource_data);

    var project_data = await getProject(project_id);
    var project_cnt = project_data.entities.length;
    if(project_cnt > 0) setDiscount(formExecutionContext, project_data);
  }

Line 3 of the code above is a great example of reusing previous backend results. If you remember, we called the getBookableResource function if the Bookable Resource field is blank. We used the results to populate the Bookable Resource field, and here we reuse the results to get the Fee Category data instead of recalling, or, worse, rewriting the same call for a different purpose.

setDiscount Function

Regardless of where the data is being pulled from the Xrm.WebApi (Project Team Member or Bookable Resource), setDiscount will take the retrieved data and set the Discount field.


var setDiscount = function (formExecutionContext, data) {
  formContext = formExecutionContext.getFormContext();
  // ptm = project team member

  var ptm_discount;

  if (data.entities.length > 0) {

    ptm_discount = data.entities[0]["enc_discount"];
    console.info("ptm discount: " + ptm_discount);

    if (ptm_discount != null)
    {
      formContext.getAttribute("enc_discount").setValue(ptm_discount);
    }
  }
} // setDiscount

 

setFeeCategory Function

Regardless of where the data is being pulled from the Xrm.WebApi (Project Team Member or Project), setFeeCategory will take the retrieved data and set the Discount field.


var setFeeCategory = function (formExecutionContext, data) {
  formContext = formExecutionContext.getFormContext();
  // ptm = project team member

  var ptm_fee_category_id;
  var ptm_fee_category_name;

  if (data.entities.length > 0) {
    ptm_fee_category_id = data.entities[0]["_enc_feecategoryid_value"];
    console.info("ptm fee category id: " + ptm_fee_category_id);
    ptm_fee_category_name = data.entities[0]["_enc_feecategoryid_value@OData.Community.Display.V1.FormattedValue"];

    if (ptm_fee_category_id != null)
    {
      SetLookupValue(formExecutionContext, "enc_feecategoryid", ptm_fee_category_id, ptm_fee_category_name, "enc_feecategory");
    }
  }
} // setFeeCategory

 

Results

The code works great! Fee Category and Discount are cleared if Project or Bookable Resource are blank. If both are populated and a Project Team Member exists, it pulls the correct data from this record; if Project Team Member does not exist, it pulls Fee Category from the Bookable Resource record and Discount from the Project record.

When a Project is selected where the Bookable Resource is associated to a Project Team Member record:

Javascript-Functions

Notice:

Fee Category = Executive Management

Discount = 50.00

When a Project is selected where the Bookable Resource is not associated to a Project Team Member record:

Javascript-Functions

Notice:

Fee Category = Systems Development and Support

Discount = 10.00

Seeing the Difference

Hopefully, we will see the advantage of the second approach and how the functions can be reused.

Version 1 Version 2
Call Xrm.WebAPI [Bookable Resource]( Call Xrm.WebAPI [Bookable Resource]
 Process inside Process outside
  Next call Xrm.WebAPI [Project Team Member]( Call Xrm.WebAPI [Project Team Member]
   Process inside Process outside
    Next call Xrm.WebAPI [Project]( Call Xrm.WebAPI [Project]
     Process inside Process outside
       )
     )
    )
// CALL PROJECT TEAM MEMBER FOR DIFFERENT SCENARIO // CALL PROJECT TEAM MEMBER FOR DIFFERENT SCENARIO
// THIS WOULD POTENTIALLY PROCESS OUT OF ORDER // THIS WOULD PROCESS IN ORDER

On the left, because it is asynchronous, all processing must occur inside the initial Bookable Resource results. If, for whatever reason, the Project Team Member function needed to be called again, it would have to be placed inside the Bookable Resource call; otherwise, it may process before the Bookable Resource call has completed, potentially causing problems. Each call is dependent on the first call.

On the right, all processing occurs in order and there are no processing dependencies.

Conclusion

This blog has demonstrated how to use the Xrm.WebApi along with async and await to create reusable backend-call JavaScript functions that will maintain a synchronous processing order.

Final Code

My Code Comments

You may notice a few personal coding implementations. I will attempt to briefly explain these differences.

Form Context

Form context is common for Dynamics client scripting, but I discovered that the context object will be different depending on how it was sent.

Microsoft suggests this method:

Resource – https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/clientapi-form-context

However, this will not work when calling a JavaScript function from a custom button which I frequently do.

PRIYESHWAGH777 demonstrates how to utilize the context coming from a custom button here:

Javascript-Functions

Resource – https://d365demystified.com/2020/09/22/pass-execution-context-to-js-script-function-as-a-parameter-from-a-ribbon-button-in-dynamics-365-ribbon-workbench/

The former method takes the incoming parameter and then calls the getFormContext method, whereas the latter method takes the incoming parameter as is.

So, in the code below, I have allowed for either method.

var setValues = async function (formExecutionContext, _caller) {
  console.info(_caller + " called this function");

  var formContext = null;
  try {
    formContext = formExecutionContext.getFormContext(); // call from home or sub grid
  }
  catch (e) {
    console.warn("Not being called from Home or Sub Grid");
    try {
      formContext = formExecutionContext;
    }
    catch (e) {
      console.warn("Not being called from Form");
    }
  }
}

 

Function Caller

This is an interesting one. At one point JS programmers could use the arguments.caller property, but that was deprecated. Then the Function.caller was used, but that has been deprecated as well.

Here are some sites that explain the issue:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/caller

https://stackoverflow.com/questions/50873585/function-caller-arguments-is-depricated-so-what-is-the-replacement-for-this

I couldn’t find a reasonable replacement in a reasonable amount of time, so I added the function name as one of the parameters as suggested above. Not the greatest, but it works and it’s easy to implement and change or upgrade when the time comes.

if (typeof (Encore) == 'undefined') {
  Encore = {
    __namespace: true
  }
}
if (typeof (Encore.TimEntryLibrary) == 'undefined') {
  Encore.TimEntryLibrary = {}
}

Encore.TimEntryLibrary = (function () {
  var onLoad = async function (formExecutionContext) {
    await setValues(formExecutionContext, "onLoad");
  } // onLoad

  var onProjectChange = async function (formExecutionContext) {
    await setValues(formExecutionContext, "onProjectChange");
  } // onProjectChange

  var onBookableResourceChange = async function (formExecutionContext) {
    await setValues(formExecutionContext, "onBookableResourceChange");
  } // onBookableResourceChange

  var setValues = async function (formExecutionContext, _caller) {
    console.info(_caller + " called this function");

    var formContext = null;
    try {
      formContext = formExecutionContext.getFormContext(); // call from home or sub grid
    }
    catch (e) {
      console.warn("Not being called from Home or Sub Grid");
      try {
        formContext = formExecutionContext;
      }
      catch (e) {
        console.warn("Not being called from Form");
      }
    }

    try {
      var formType = formContext.ui.getFormType();

      var userSettings = Xrm.Utility.getGlobalContext().userSettings;
      var user_id = userSettings.userId;
      user_id = StripGUID(user_id);

      if (formContext != null) {
        var project_id = GetLookupId(formExecutionContext, "msdyn_project");
        var project_name = GetLookupName(formExecutionContext, "msdyn_project");

        // PROJECT TEAM MEMBER = BOOKABLE RESOURCE
        var bookable_resource_id = GetLookupId(formExecutionContext, "msdyn_bookableresource");
        var bookable_resource_name = GetLookupName(formExecutionContext, "msdyn_bookableresource");

        // GRAB BOOKABLE RESOURCE FOR CREATE AND UPDATE
        var bookable_resource_data;
        var brd_cnt = 0;

        // ALLOW USER TO CHANGE BOOKABLE RESOURCE
        // ONLY PULL BOOKABLE RESOURCE FROM USER ON LOAD
        if (bookable_resource_id == null && (_caller == "onLoad")) {
          bookable_resource_data = await getBookableResource(user_id,null);
          brd_cnt = bookable_resource_data.entities.length;

          if (brd_cnt > 0) {
            bookable_resource_id = bookable_resource_data.entities[0]["bookableresourceid"];
            bookable_resource_name = bookable_resource_data.entities[0]["_userid_value@OData.Community.Display.V1.FormattedValue"];

            SetLookupValue(formExecutionContext, "msdyn_bookableresource", bookable_resource_id, bookable_resource_name, "bookableresource");
          }
        }
        // GET BOOKABLE RESOURCE DATA FROM ENTERED BOOKABLE RESOURCE
        else if (bookable_resource_id != null) {
          bookable_resource_data = await getBookableResource(null,bookable_resource_id);
          brd_cnt = bookable_resource_data.entities.length;

          if (brd_cnt > 0) {
            bookable_resource_id = bookable_resource_data.entities[0]["bookableresourceid"];
            bookable_resource_name = bookable_resource_data.entities[0]["_userid_value@OData.Community.Display.V1.FormattedValue"];

            SetLookupValue(formExecutionContext, "msdyn_bookableresource", bookable_resource_id, bookable_resource_name, "bookableresource");
          }
        }

        // CLEAR FEE CATEGORY AND DISCOUNT
        if (project_id == null || bookable_resource_id == null) {
          formContext.getAttribute("enc_feecategoryid").setValue(null);
          formContext.getAttribute("enc_discount").setValue(null);
        }

        // SET FEE CATEGORY AND DISCOUNT
        // ON CHANGE
        else if (project_id != null && bookable_resource_id != null) {
          var project_team_member_data = await getProjectTeamMember(project_id, bookable_resource_id);
          var ptm_cnt = project_team_member_data.entities.length;

          if (ptm_cnt > 0) {
            console.info("Project Team Member exists");
            setFeeCategory(formExecutionContext, project_team_member_data);
            setDiscount(formExecutionContext, project_team_member_data);
          }
          else {
            console.info("Project Team Member does not exist");
            setFeeCategory(formExecutionContext, bookable_resource_data);

            var project_data = await getProject(project_id);
            var project_cnt = project_data.entities.length;
            if(project_cnt > 0) setDiscount(formExecutionContext, project_data);
          }
        }

        // SINCE BOOKABLE RESOURCE IS BEING SET ABOVE, THIS WILL NOT BE REACHED
        else if (project_id != null && bookable_resource_id == null) {
        }
      } // if formContext != null
    }
    catch (e) {
      console.error("ERROR [setValues]: " + e);
    }
  } // setValues

  var getBookableResource = async function (user_id, bookable_resource_id) {
    var options = "?$select=bookableresourceid,_userid_value,_enc_feecategoryid_value";
    if (user_id != null) options += "&$filter=_userid_value eq " + user_id;
    if (bookable_resource_id != null) options += "&$filter=bookableresourceid eq " + bookable_resource_id;
    var data = await Xrm.WebApi.retrieveMultipleRecords("bookableresource", options);
    return data;
  } // getBookableResource

  var getProject = async function (project_id) {
    var options = "?$select=msdyn_projectid,enc_discount";
    options += "&$filter=msdyn_projectid eq " + project_id;
    var data = await Xrm.WebApi.retrieveMultipleRecords("msdyn_project", options);
    return data;
  } // getProject

  var getProjectTeamMember = async function (project_id, bookable_resource_id) {
    var options = "?$select=msdyn_projectteamid,_msdyn_project_value,_msdyn_bookableresourceid_value,_enc_feecategoryid_value,enc_discount";
    options += "&$filter=_msdyn_project_value eq " + project_id;
    options += " and _msdyn_bookableresourceid_value eq " + bookable_resource_id;
    var data = await Xrm.WebApi.retrieveMultipleRecords("msdyn_projectteam", options);
    return data;
  } // getProjectTeamMember

  var setDiscount = function (formExecutionContext, data) {
    formContext = formExecutionContext.getFormContext();

    var ptm_discount;

    if (data.entities.length > 0) {
      ptm_discount = data.entities[0]["enc_discount"];

      if (ptm_discount != null) // discount == null && 
      {
        formContext.getAttribute("enc_discount").setValue(ptm_discount);
      }
    }
  } // setDiscount

  var setFeeCategory = function (formExecutionContext, data) {
    formContext = formExecutionContext.getFormContext();

    var ptm_fee_category_id;
    var ptm_fee_category_name;

    if (data.entities.length > 0) {
      ptm_fee_category_id = data.entities[0]["_enc_feecategoryid_value"];
      ptm_fee_category_name = data.entities[0]["_enc_feecategoryid_value@OData.Community.Display.V1.FormattedValue"];

      // SET FEE CATEGORY
      if (ptm_fee_category_id != null)// fee_category_id == null && 
      {
        SetLookupValue(formExecutionContext, "enc_feecategoryid", ptm_fee_category_id, ptm_fee_category_name, "enc_feecategory");
      }
    }
  } // setFeeCategory

  function GetLookupId(executionContext, _lookup) {
    var rtn = null;

    var formContext = executionContext.getFormContext();

    var lookup_field = formContext.getAttribute(_lookup);

    if (lookup_field != null) {
      if (lookup_field.getValue() != null) {
        rtn = lookup_field.getValue()[0].id;
        rtn = rtn.replace("{", "").replace("}", "");
      }
    } // lookup != null

    return rtn;
  } // GetLookupId

  function GetLookupName(executionContext, _lookup) {
    var rtn = null;

    var formContext = executionContext.getFormContext();

    var lookup_field = formContext.getAttribute(_lookup);

    if (lookup_field != null) {
      if (lookup_field.getValue() != null) {
        rtn = lookup_field.getValue()[0].name;
      }
    } // lookup != null

    return rtn;
  } // GetLookupName

  function SetLookupValue(executionContext, fieldName, id, name, entityType) {
    var formContext = executionContext.getFormContext();

    if (fieldName != null) {
      var lookupValue = new Array();
      lookupValue[0] = new Object();
      lookupValue[0].id = id;
      lookupValue[0].name = name;
      lookupValue[0].entityType = entityType;

      if (lookupValue[0].id != null) {
        formContext.getAttribute(fieldName).setValue(lookupValue);
      }
    }
  } // SetLookupValue

  var StripGUID = function (_guid) {
    var rtn = "";
    rtn = _guid.replace("{", "").replace("}", "");
    return rtn;
  } // StripGUID

  return {
    onLoad: onLoad,
    setValues: setValues,
    onProjectChange: onProjectChange,
    onBookableResourceChange: onBookableResourceChange
  }
})()

If you have any questions about this process, please get in touch with us.

Dynamics 365 CRM How-To eGuide

41 pages of step-by-step instructions for 6 different key tasks in Dynamics 365 CRM apps. Includes interactions with Power Apps and Power Automate!

Get eGuide

Dynamics 365 CRM How-To eGuide

Get eGuide