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.
- In Visual Studio, right-click the project and select Add > New Item.
- Select Service and name it d365fno_SalesOrderService.
- Set the Class property to d365fno_SalesOrderService.
Step 6: Select the Service Operations
Add new service operation
- Under service Right click on Service Operations node.
- Click New Service Operation.
- From property windows select the Method from drop-down.
Step 7: Create a Service Group
Create a service group to host the service.
- In the AOT, right-click Service Groups and select New Service Group.
- Name the service group d365fno_SalesOrderServiceGroup.
- Add the d365fno_SalesOrderService to the service group.
Step 7: Deploy the Service
Deploy the service to make it available for external systems.
- 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.