AX2009 Cancel SalesOrder with empty SalesLine

TL;DR: With the help of this job, you can cancel a sales order that has an empty salesline


One customer had the the problem, that some of their sales employees cancelled sales orders that they created by deleting all sales lines and letting the sales order open. So now the customer had the problem that they had many sales orders open, and all the queries in their batch job selected more and more orders to check them. So some batches were taking much longer. To solve the problem I wrote a job that selectes all open sales orders (status = backorder) with a position count of 0 (sales line count). For the sales line count I used a already made function by use in the SalesTable controller class.

public RefRecId getPositionCount()
    SalesLine     ltabSalesLine;

    select count (RecId) from ltabSalesLine
           where ltabSalesLine.SalesId == mtabSalesTable.SalesId;

    return ltabSalesLine.RecId;

After finding a sales order for that, the script creates a new sales line entry with one of our dummy items (999999999). And after that we cancel the appropriate position in the system. The part InterCompanyUpdateRemPhys::synchronize is needed to update sales remain amount in the sales line. It is doing exactly the same, as the form SalesUpdateRemain in AX2009. As the difference from remain qty to 0 (delta) is remain qty, I only took the entry ledtQtyDifference = ltabSalesLine.RemainSalesPhysical;. For our case with inserting a sales line and cancelling it that is enough, but in other scenarions you need to differenciate between remain invent physical and remain sales physical. Have fun :smile:

AX2009 Job

static void down1_AuftragStornieren(Args _args)
    SalesTable  ltabSalesTable;
    SalesLine   ltabSalesLine;
    InventTable ltabInventTable;
    InventDim   ltabInventDim;
    Qty         ledtQtyDifference;

    while select forupdate ltabSalesTable where  ltabSalesTable.SalesType == SalesType::Sales && ltabSalesTable.SalesStatus == SalesStatus::Backorder{
        if(ltabSalesTable.axpController().getPositionCount() == 0 && DateTimeUtil::date(ltabSalesTable.createdDateTime) < mkdate(01,01,2021)){


            ltabSalesLine.SalesId = ltabSalesTable.SalesId;

            ltabSalesLine.ItemId = '999999999';

            ltabInventTable = InventTable::find(ltabSalesLine.ItemId);
            ltabInventDim = InventDim::findOrCreate(ltabInventDim);
            ltabSalesLine.InventDimId = ltabInventDim.inventDimId;

            ltabSalesLine.SalesQty =  1;


            while select forUpdate ltabSalesLine
           where ltabSalesLine.SalesId      == ltabSalesTable.SalesId
              && ltabSalesLine.SalesStatus  == SalesStatus::Backorder
              && ltabSalesLine.RemainSalesPhysical != 0
                ledtQtyDifference = ltabSalesLine.RemainSalesPhysical;
                ltabSalesLine.RemainSalesPhysical   = 0;
                ltabSalesLine.RemainInventPhysical  = 0;

                if (ltabSalesLine.validateWrite() && ltabSalesLine.salesTable().checkUpdate())


Originally published August 5, 2023 | View revision history

If you enjoyed this post, you might also enjoy:


MS Dynamics AX / D365 FO developer with experience in administration, developing new and adjusting the existing solutions in the Dynamics AX 2009, AX 2012, D365 FO. Ability to support all phases of implementation of project, starting with design, development, final deployment and administration. Responsible team member always looking for new challenges with experience from international projects in Austria, Germany and Switzerland. Experience in implementation of external service to MS Dynamcis AX / D365, like cash registers and warehouse automations. More about the author →