How to Create a Sales Order via X++ in Dynamics 365 F&O

If you work with Dynamics 365 Finance & Operations long enough, you'll eventually need to create a sales order in X++ code. Whether you're building a custom REST API, processing staging records, or writing a custom import framework, getting the order creation logic right is critical.

A common mistake I see developers make is trying to manually populate every single field on SalesTable or SalesLine. D365 has built-in framework logic designed to do the heavy lifting for you—if you call the right initialization methods.

Important Note on Values

The code snippet below uses example values like "CUS001" for the customer, "IT001" for the item, and generic Site/Warehouse IDs. Make sure to replace or parameterize these variables based on your actual data source or request input.


The Code

Here is a clean implementation wrapped in a database transaction block

public static void createSalesOrder(Args _args)

{

    SalesTable  salesTable;

    SalesLine   salesLine;

    NumberSeq   numberSeq;


    // Wrap the operation in a transaction block to avoid orphaned header records

    ttsbegin;


    // 1. Fetch the next Sales Order ID from the sequence framework

    numberSeq = NumberSeq::newGetNum(SalesParameters::numRefSalesId());


    // 2. Setup the Sales Header

    salesTable.clear();

    salesTable.initValue();

    salesTable.SalesId          = numberSeq.num();

    salesTable.CustAccount      = "CUS001"; // Pass your customer account here

    

    // Pull default currency, payment terms, tax groups, etc. from the customer

    salesTable.initFromCustTable();

    

    // Set or override mandatory inventory dimensions

    salesTable.SalesType        = SalesType::Sales;

    salesTable.InventSiteId     = "Site";   // Set your Site ID

    salesTable.InventLocationId = "WH";     // Set your Warehouse ID


    // Always validate before saving

    if (!salesTable.validateWrite())

    {

        throw error("Sales order header validation failed.");

    }

    

    salesTable.insert();


    // 3. Setup the Sales Line

    salesLine.clear();

    salesLine.initValue();

    salesLine.SalesId           = salesTable.SalesId;

    salesLine.ItemId            = "IT001";  // Pass your item ID here

    

    // Initialize item defaults (Unit, Name) and inherit header settings (Delivery Info, Site/WH)

    salesLine.initFromInventTable(InventTable::find(salesLine.ItemId));

    salesLine.initFromSalesTable(salesTable);

    

    // Set quantity and pricing

    salesLine.SalesQty          = 1;

    salesLine.QtyOrdered        = 1;

    salesLine.SalesPrice        = 10;

    

    // Calculate total line amount

    salesLine.LineAmount        = salesLine.SalesQty * salesLine.SalesPrice;

    

    // Set requested dates

    salesLine.ShippingDateConfirmed = salesTable.ShippingDateConfirmed;

    salesLine.ShippingDateRequested = salesTable.ShippingDateRequested;


    // createLine handles trade agreements, inventory allocations, and line checks

    salesLine.createLine(true, true, true, true, true, true, true);


    ttscommit;


    info(strFmt("Sales Order %1 created successfully.", salesTable.SalesId));

}


Why This Approach Works

  1. initFromCustTable() & initFromInventTable()

    These two calls map dozens of fields in the background—saving you from having to write repetitive assignments for sales units, tax groups, price groups, or currency.

  2. salesLine.createLine(...) vs insert()

    Using .insert() on SalesLine directly skips important framework behavior. Calling createLine() ensures that D365 evaluates trade agreement rules, updates inventory allocations, and sets up price structures properly.

  3. Transaction Safety (ttsbegin / ttscommit)

    If an exception happens while setting up the line item, the entire transaction rolls back. This keeps your database clean and prevents "ghost" sales orders without line items.

Comments