AX2012/D365FnO – DEVELOPING A CUSTOM SERVICE TO CREATE A SALES ORDER IN D365FnO USING X++

D365fno-PostImage

Developing a Custom Service to Create a Sales Order in D365F&O Using X++

Creating a custom service in D365F&O allows you to expose business logic to external systems. In this guide, we’ll walk through the process of creating a custom service to create a sales order.

Step 1: Create a Data Contract Class for Sales Header

First, we need to create a data contract class to define the structure of the data we will receive as request payload from the external application.

[DataContractAttribute]
class d365fno_SalesOrderHeaderContract
{
    SalesId         externalSalesId;
    CustAccount     customerId;
    List            salesOrderLinesList;
    [DataMemberAttribute("ExternalSalesId")]
    public SalesId parmExternalSalesId(SalesId  _externalSalesId = externalSalesId)
    {
        externalSalesId = _externalSalesId;
        return externalSalesId;
    }
    [DataMemberAttribute("CustAccount")]
    public CustAccount parmCustAccount(SalesId  _customerId = customerId)
    {
        customerId = _customerId;
        return customerId;
    }
    [
        DataMemberAttribute("SalesOrderLinesList"),
        AifCollectionType('return', Types::Class, classStr(d365fno_SalesOrderLinesContract)),
        AifCollectionType('salesOrderLinesList', Types::Class, classStr(d365fno_SalesOrderLinesContract))
    ]
    public List parmSalesOrderLinesList(List  _salesOrderLinesList = salesOrderLinesList)
    {
        salesOrderLinesList = _salesOrderLinesList;
        return salesOrderLinesList;
    }
}

Step 2: Create a Data Contract Class for Sales Lines

Next, we need to create a data contract class to define the structure of sales order lines data that we will receive as sales lines information as sales header contract list.

[DataContractAttribute]
class d365fno_SalesOrderLinesContract
{
    ItemId              itemId;
    SalesQty            salesQty;
    SalesPrice          salesPrice;
    SalesLineDisc       salesLineDisc;
    SalesLinePercent    salesLinePercent;
    [DataMemberAttribute("ItemId")]
    public ItemId parmItemId(ItemId _itemId = itemId)
    {
        itemId = _itemId;
        return itemId;
    }
    [DataMemberAttribute("SalesQty")]
    public SalesQty parmSalesQty(SalesQty _salesQty = salesQty)
    {
        salesQty = _salesQty;
        return salesQty;
    }
    [DataMemberAttribute("SalesPrice")]
    public SalesPrice parmSalesPrice(SalesPrice _salesPrice = salesPrice)
    {
        salesPrice = _salesPrice;
        return salesPrice;
    }
    [DataMemberAttribute("SalesLineDisc")]
    public SalesLineDisc parmSalesLineDisc(SalesLineDisc _salesLineDisc = salesLineDisc)
    {
        salesLineDisc = _salesLineDisc;
        return salesLineDisc;
    }
    [DataMemberAttribute("SalesLinePercent")]
    public SalesLinePercent parmSalesLinePercent(SalesLinePercent _salesLinePercent = salesLinePercent)
    {
        salesLinePercent = _salesLinePercent;
        return salesLinePercent;
    }
}

Step 3: Create a Response Contract Class

Next, we need to create a response contract class that will send back the information as response to external application like, Error Message, Request Success True or False etc.

[DataContractAttribute]
class d365fno_SalesOrderResponseContract
{
    SalesId     axSalesId;
    ErrorMsg    errMsg;
    boolean     success;

    [DataMemberAttribute("AXSalesId")]
    public SalesId parmAXSalesId(SalesId _axSalesId = axSalesId)
    {
        axSalesId = _axSalesId;
        return axSalesId;
    }

    [DataMemberAttribute("ErrorMsg")]
    public ErrorMsg parmErrorMsg(ErrorMsg _errMsg = errMsg)
    {
        errMsg = _errMsg;
        return errMsg;
    }

    [DataMemberAttribute("Success")]
    public boolean parmSuccess(boolean _success = success)
    {
        success = _success;
        return success;
    }
}

Step 4: Create a Service Class

Next, create a service class that contains the business logic for creating a sales order using contract class information that comes as request payload.

class d365fno_SalesOrderService
{
    ErrorMsg        msg;
    boolean         success = false;
    SalesId         axSalesId;

    [
        SysEntryPoint(true),
        AifCollectionTypeAttribute('return', Types::Class, classStr(d365fno_SalesOrderResponseContract))
    ]
    public d365fno_SalesOrderResponseContract createSalesOrder(d365fno_SalesOrderHeaderContract _salesOrderHeaderContract)
    {
        SalesTable  salesTable;
        d365fno_SalesOrderResponseContract salesOrderResponseContract = new d365fno_SalesOrderResponseContract();

        try
        {
            ttsbegin;
            
            // keep some validation as per requirements
            if(_salesOrderHeaderContract != null)
            {
                msg = "Sales order header contract cannot be null for sales order creation";
                throw Exception::Error;
            }
            if(_salesOrderHeaderContract.parmCustAccount() == '')
            {
                msg = "Customer ID cannot be empty for the sales order";
                throw Exception::Error;
            }
            if(_salesOrderHeaderContract.parmSalesOrderLinesList().empty())
            {
                msg = "Sales order lines details connot be enpty for sales order creation";
                throw Exception::Error;
            }
            
            // create private method to create sales order header
            salesTable = this.createSalesOrderHeader(_salesOrderHeaderContract);

            if(salesTable.RecId != 0)
            {
            // create private method to create sales order lines
                this.createSalesOrderLines(salesTable.SalesId, _salesOrderHeaderContract.parmSalesOrderLinesList());
                success = true;
                axSalesId = salesTable.SalesId;
                msg = strFmt("%1 sales order has been created in the system", axSalesId);
            }
            else
            {
                msg = "Unable to create sales order header";
                throw Exception::Error;
            }                
            ttscommit;
        }
        catch (Exception::Error)
        {
            if(msg == '')
            {
                msg = strFmt("Error occured while creating the sales order for external order %1",_salesOrderHeaderContract.parmExternalSalesId());
            }
        }
       // send response back to external application using response contract class 
        salesOrderResponseContract.parmAXSalesId(axSalesId);
        salesOrderResponseContract.parmErrorMsg(msg);
        salesOrderResponseContract.parmSuccess(success);

        return salesOrderResponseContract;
    }

    private SalesTable createSalesOrderHeader(d365fno_SalesOrderHeaderContract _salesOrderHeaderContract)
    {
        SalesTable  salesTable;
        NumberSeq   numberSeq;
        SalesId     newSalesId;

        if(CustTable::find(_salesOrderHeaderContract.parmCustAccount()).AccountNum)
        {
            salesTable.CustAccount = _salesOrderHeaderContract.parmCustAccount();
        }
        else
        {
            
            msg = strFmt("Customer ID %1 is not exist in system", _salesOrderHeaderContract.parmCustAccount());
            throw Exception::Error;
        }
        salesTable.initFromCustTable(); // init values from the customer master

        newSalesId = NumberSeq::newGetNum(SalesParameters::numRefSalesId()).num();
        
        //salesTable.ExternalSalesId = _salesOrderHeaderContract.parmExternalSalesId(); // extend the SalesTable and create the field to save the external sales Id for reference
        salesTable.SalesId = newSalesId;
        salesTable.initValue(); // init values from sales table

        /*
            set the Site and Warehouse on the customer master level it will automatically come on sales order using salesTable.initFromCustTable() method.
            otherwise you have to give the values for site and warehouse using code
            //salesTable.InventSiteId = '1';
            //salesTable.InventLocationId = '11';
        */

        if (!salesTable.validateWrite())
        {
            msg = "Sales order header detail is not validated";
            throw Exception::Error;
        }

        salesTable.insert();
        return SalesTable;
    }

    private void createSalesOrderLines(SalesId _salesId, List _salesOrderLinesList)
    {
        SalesLine       salesLine;
        InventDim       inventDim;
        ListEnumerator  listEnumerator;
        d365fno_SalesOrderLinesContract salesOrderLinesContract;

        listEnumerator = _salesOrderLinesList.getEnumerator();

        if(listEnumerator.moveNext())
        {
            salesOrderLinesContract = listEnumerator.current();

            if(InventTable::find(salesOrderLinesContract.parmItemId()).RecId != 0)
            {
                salesLine.clear();
                inventDim.clear();

                salesLine.SalesId       = _salesId;
                salesLine.ItemId        = salesOrderLinesContract.parmItemId();
                salesLine.SalesQty      = salesOrderLinesContract.parmSalesQty(); // you can put the empty validation for sales quantity

                salesLine.createLine(true, // validate sales lines
                    true, // initValuesFromSalesOrder
                    true, // initValuesFromInventTable
                    true, // calculateInventoryQty (qtyOrdered, remainSalesPhy, remainInventPhy, etc)
                    false, // searchMarkup (miscellaneous chargers)
                    false, // passing the search price (enable when initValuesFromInventTable = false)
                    false // automatic
                    );

                salesLine.SalesPrice    = salesOrderLinesContract.parmSalesPrice(); // you can put the empty validation for item sales price 
                salesLine.LineDisc      = salesOrderLinesContract.parmSalesLineDisc(); // validation is not required for discount
                salesLine.LinePercent   = salesOrderLinesContract.parmSalesLinePercent(); // validation is not required for discount percent

                salesLine.update();

                if(salesLine.RecId == 0)
                {
                    msg = strFmt("Sales line has not been created for item %1",salesOrderLinesContract.parmItemId()) ;
                    throw Exception::Error;
                }

            }
            else
            {
                msg = strFmt("%1 item is not availabel in system",salesOrderLinesContract.parmItemId()) ;
                throw Exception::Error;
            }
        }
    }

}

Step 5: Create a Service Node

Create a service node in the AOT to expose the service class.

  1. In Visual Studio, right-click the project and select Add > New Item.
  2. Select Service and name it d365fno_SalesOrderService.
  3. Set the Class property to d365fno_SalesOrderService.

Step 6: Select the Service Operations

Add new service operation 

  1. Under service Right click on Service Operations node.
  2. Click New Service Operation.
  3. From property windows select the Method from drop-down.

Step 7: Create a Service Group

Create a service group to host the service.

  1. In the AOT, right-click Service Groups and select New Service Group.
  2. Name the service group d365fno_SalesOrderServiceGroup.
  3. Add the d365fno_SalesOrderService to the service group.

Step 7: Deploy the Service

Deploy the service to make it available for external systems.

  1. Ensure the service is deployed successfully.

Step 8: Test the Service

Use a tool like Postman to test the service by sending a request to create a sales order.

By following these steps, you can create a custom service in D365F&O to handle sales order creation. This service can be called from external systems, allowing for seamless integration with other applications.

Feel free to experiment with these examples and adapt them to your specific requirements. Happy coding!
This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.