In Dynamics 365 environments where project resources are pivotal to billing processes, automating the generation of invoice lines can enhance efficiency and accuracy. Today, we’ll explore how to implement custom JavaScript automation to create invoice lines based on project resources with the click of a button.

Understanding the Scenario

Imagine you are working within a Dynamics 365 environment where project resources are assigned to specific projects, each resource having its own agreed-upon rate and role. To automate the creation of invoice lines based on these project resources, we will implement a custom JavaScript function triggered by clicking a button added to the invoice form. This automation will streamline invoicing workflows and ensure accuracy in billing processes within Dynamics 365.

Prerequisites

Before diving into implementation, ensure the following:

  • Access to a Dynamics 365 environment with necessary privileges.
  • Basic understanding of JavaScript and Dynamics 365 development.

Implementation: Creating the SyncResources Function

We’ll start by defining the SyncResources function, which orchestrates the automation of invoice line creation based on project resources.

async function SyncResources(invoiceid) {
    var formContext = Xrm.Page;
    if (invoiceid != null) {
        invoiceid = invoiceid.replace("{", "").replace("}", "");
    }
    
    console.log(`InvoiceId is ${invoiceid}`);

    var projectlookup = formContext.getAttribute("dyn_projectid").getValue();
    var projectId = projectlookup[0].id.replace("{", "").replace("}", "");
    console.log(`ProjectId is ${projectId}`);

    // Retrieve invoice line records
    var InvoiceLineRecords = await Xrm.WebApi.retrieveMultipleRecords("dyn_invoiceline", `?$filter=_dyn_invoiceid_value eq '${invoiceid}' and dyn_billingtype eq 1`);
    var InvoiceLineArray = InvoiceLineRecords.entities;
    // console.log("InvoiceLines Array are:");
    // console.log(JSON.stringify(InvoiceLineArray, null, 2));

    // Retrieve project resources
    var filter = `?$filter=_dyn_projectid_value eq ${projectId}`;
    var results = await Xrm.WebApi.retrieveMultipleRecords("dyn_projectresource", filter);
    var ResourceArray = results.entities;

    // Remove duplicates based on _dyn_resourceid_value---  Code Starts
    // Remove duplicates based on _dyn_resourceid_value
    var uniqueResourceArray = ResourceArray.filter((resource, index, self) =>
    index === self.findIndex((r) => r._dyn_resourceid_value === resource._dyn_resourceid_value)
);

    // Remove duplicates based on _dyn_resourceid_value---  Code Ends


    if (InvoiceLineArray.length == 0 && uniqueResourceArray.length > 0) {
        // No existing invoice lines, but there are project resources
        for (var i = 0; i < uniqueResourceArray.length; i++) {
            var data = {};
            data["dyn_invoiceid@odata.bind"] = `/dyn_invoices(${invoiceid})`;
            data["dyn_priceperunit"] = parseFloat(uniqueResourceArray[i].dyn_agreedrate);
            data["dyn_resourceid@odata.bind"] = `/dyn_resources(${uniqueResourceArray[i]._dyn_resourceid_value})`;
            data["dyn_unitid@odata.bind"] = `/dyn_units(${uniqueResourceArray[i]._dyn_unitid_value})`;
            data["dyn_resourceroleid@odata.bind"] = `/dyn_resourceroles(${uniqueResourceArray[i]._dyn_resourceroleid_value})`;
        
            try {
                await Xrm.WebApi.createRecord("dyn_invoiceline", data);
                // Run the code after a successful record creation
                setTimeout(function() {
                    // Get the subgrid control
                    var subgridControl = formContext.getControl("Subgrid_new_1");
        
                    // Check if the subgrid control exists
                    if (subgridControl != null) {
                        // Refresh the subgrid
                        subgridControl.refresh();
                    } else {
                        console.error("Subgrid control not found.");
                    }
                }, 5000); // 5000 milliseconds = 5 seconds
            } catch (error) {
                console.error(`Error creating records: ${error.message}`);
            }
        }
        
    } else if (InvoiceLineArray.length !== uniqueResourceArray.length) {
        // Existing invoice lines count doesn't match project resources count
    // Add new invoice lines based on newly added project resources
    for (var i = 0; i < uniqueResourceArray.length; i++) {
        var resourceExists = InvoiceLineArray.some(function(invoiceLine) {
            // Check if the resource already exists in the invoice lines
            return invoiceLine._dyn_resourceid_value === uniqueResourceArray[i]._dyn_resourceid_value;
        });
        if (!resourceExists) {
            // Create a new invoice line for the resource
            var data = {};
            data["dyn_invoiceid@odata.bind"] = `/dyn_invoices(${invoiceid})`;
            data["dyn_priceperunit"] = parseFloat(uniqueResourceArray[i].dyn_agreedrate);
            data["dyn_resourceid@odata.bind"] = `/dyn_resources(${uniqueResourceArray[i]._dyn_resourceid_value})`;
            data["dyn_unitid@odata.bind"] = `/dyn_units(${uniqueResourceArray[i]._dyn_unitid_value})`;
            data["dyn_resourceroleid@odata.bind"] = `/dyn_resourceroles(${uniqueResourceArray[i]._dyn_resourceroleid_value})`;

            try {
                await Xrm.WebApi.createRecord("dyn_invoiceline", data);
                // Run the code after a successful record creation
                setTimeout(function() {
                    // Get the subgrid control
                    var subgridControl = formContext.getControl("Subgrid_new_1");

                    // Check if the subgrid control exists
                    if (subgridControl != null) {
                        // Refresh the subgrid
                        subgridControl.refresh();
                    } else {
                        console.error("Subgrid control not found.");
                    }
                }, 5000); // 5000 milliseconds = 5 seconds
            } catch (error) {
                console.error(`Error creating record: ${error.message}`);
            }
        }
    }
    } else {
        // No need to add new invoice lines
        var alertStrings = {
            confirmButtonLabel: "OK",
            text: "Invoice Lines already added based on Resources."
        };
        var alertOptions = {
            height: 120,
            width: 260
        };
        try {
            Xrm.Navigation.openAlertDialog(alertStrings, alertOptions);
        } catch (error) {
            console.error(`Error displaying alert: ${error.message}`);
        }
    }

}

Triggering the Automation

To invoke the SyncResources function, add a button to the invoice form in Dynamics 365 and associate the button’s click event with the function. Here’s a high-level overview of how this can be achieved:

  1. Add a Button: Edit the invoice form in Dynamics 365 and insert a new button element.
  2. Configure Button Click Event: Specify the SyncResources function as the handler for the button’s click event using form customization options.
  3. Test the Automation: Click the button on the invoice form to trigger the automation and observe the creation of invoice lines based on project resources.

Conclusion

By leveraging custom JavaScript automation within Dynamics 365, organizations can streamline invoicing workflows and ensure accuracy in billing processes. The SyncResources function showcased in this blog post exemplifies how Dynamics 365’s extensibility can be harnessed to meet specific business needs effectively.

Implementing similar automation not only saves time but also enhances data integrity and operational efficiency within Dynamics 365 project management environments.