How to create a SysOperation batch job - sales order confirmation

Batch confirming sales orders with SysOperation

A job that confirms sales orders in bulk shows up on most F&O projects sooner or later. Someone wants the open orders for a customer confirmed on a nightly schedule, or a single stuck order pushed through by hand. The logic itself is nothing special. What matters is packaging it as a SysOperation job that runs cleanly on the batch server and still makes sense when you open it again a year later.

This is the structure I use.

Structure

SysOperation expects three classes. The data contract holds the parameters, and the dialog is generated from it, so you never draw a form. The controller binds the contract to a service method and decides how the job runs. The service class is where the work happens.

The contract

[DataContract]
public class SalesConfirmBatchContract implements SysOperationValidatable
{
    SalesId     salesId;
    CustAccount custAccount;

    [DataMember, SysOperationControlVisibilityAttribute(true)]
    public SalesId parmSalesId(SalesId _v = salesId)
    {
        salesId = _v;
        return salesId;
    }

    [DataMember, SysOperationControlVisibilityAttribute(true)]
    public CustAccount parmCustAccount(CustAccount _v = custAccount)
    {
        custAccount = _v;
        return custAccount;
    }

    public boolean validate()
    {
        boolean ok = true;

        if (!salesId && !custAccount)
        {
            ok = checkFailed("Specify a sales order, a customer, or both.");
        }

        return ok;
    }
}

Two optional filters: a specific sales id, or a customer account. The validate method blocks the empty case, because a run with no filter would happily confirm every open order in the company.

The controller

class SalesConfirmBatchController extends SysOperationServiceController
{
    protected void new()
    {
        super(classStr(SalesConfirmBatchService),
              methodStr(SalesConfirmBatchService, run),
              SysOperationExecutionMode::ScheduledBatch);
    }

    public static void main(Args _args)
    {
        SalesConfirmBatchController controller = new SalesConfirmBatchController();
        controller.parmArgs(_args);
        controller.startOperation();
    }
}

The last argument to super is the execution mode. ScheduledBatch lets the job sit on the batch schedule with a recurrence, which is what you want for something running overnight. Synchronous runs in the calling session and blocks it; Asynchronous and ReliableAsynchronous are the fire-and-forget variants.

The service

class SalesConfirmBatchService extends SysOperationServiceBase
{
    public void run(SalesConfirmBatchContract _contract)
    {
        SalesTable salesTable;

        while select SalesId from salesTable
            where salesTable.SalesType       == SalesType::Sales
               && salesTable.SalesStatus     == SalesStatus::Backorder
               && salesTable.DocumentStatus  == DocumentStatus::None
               && (!_contract.parmSalesId()     || salesTable.SalesId     == _contract.parmSalesId())
               && (!_contract.parmCustAccount() || salesTable.CustAccount == _contract.parmCustAccount())
        {
            try
            {
                SalesConfirmBatchService::confirmOne(salesTable.SalesId);
            }
            catch
            {
                error(strFmt("Confirmation failed for %1: %2",
                             salesTable.SalesId, CLRInterop::getLastException().ToString()));
            }
        }
    }

    public static void confirmOne(SalesId _salesId)
    {
        SalesTable salesTable = SalesTable::find(_salesId);

        if (!salesTable.RecId
         || salesTable.SalesStatus    != SalesStatus::Backorder
         || salesTable.DocumentStatus != DocumentStatus::None)
        {
            return;
        }

        SalesFormLetter salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
        salesFormLetter.transDate(systemDateGet());
        salesFormLetter.specQty(SalesUpdate::All);
        salesFormLetter.proforma(false);
        salesFormLetter.printFormLetter(false);
        salesFormLetter.update(salesTable);
    }
}

A few things in there are deliberate.

The confirmOne method re-reads the order and checks its status before doing anything. The query result is a snapshot. By the time the loop reaches a given row, someone may have already confirmed or cancelled it, so the check is worth the two lines it costs.

The try/catch sits inside the loop rather than around it. If one order has a credit hold or a bad delivery date, I want that order logged and skipped, not the whole batch rolled back after it has already processed a few thousand others.

There is no ttsbegin around confirmOne. SalesFormLetter.update runs its own transaction, and wrapping it in another one just widens the lock scope and holds locks longer than the posting itself needs.

One point that has nothing to do with the code: if the sales confirmation number sequence is set to continuous, confirmations end up serialising on that sequence's lock. For anything running in bulk, set it to non-continuous.

That is the whole job. If it ever needs to run faster, confirmOne is already the piece you would move onto worker threads, but that is a separate exercise.

Comments