Acumatica · Customization

Acumatica SO Module — Extension Patterns

Acumatica SO Module — Extension Patterns is one of those Acumatica customisations that every team eventually needs and almost no team does well the first time.

John Kihiu12 min read

Sales Orders is usually the most customized module in an Acumatica implementation, because it's where a business's actual selling process — pricing exceptions, allocation rules, order holds — collides with a generic order-to-cash flow. The DACs are large and heavily interconnected, so the discipline here is knowing which extension point matches the kind of change you're making.

Core DACs and screens

SOOrder and SOLine make up the order (Sales Orders screen, SO301000), and SOShipment/SOShipLine handle fulfillment (Shipments screen, SO302000). The graphs are SOOrderEntry and SOShipmentEntry. Pricing runs through its own engine (price/discount matrices, sometimes referred to via ARPrice/discount schedules) that's evaluated as lines are added, which is worth knowing before assuming a pricing customization belongs on the order graph itself.

Extension points

Order-level custom fields and validation (a custom order type restriction, a required custom field before an order can be completed) are standard PXCacheExtension<SOOrder> plus RowPersisting work. Allocation logic — deciding which warehouse or lot fulfills a line when stock is split across locations — is a more involved extension, usually on the shipment creation graph, since that's where Acumatica actually commits inventory to a specific location and lot/serial.

Order holds are a status field, not a custom flag

If the requirement is "don't let this order ship until X is resolved," check whether it maps to the existing Hold status on SOOrder before adding a parallel custom flag. A second "is this order actually held" mechanism living alongside the real one is exactly the kind of divergence that causes an order to ship when it shouldn't, six months after the customization was forgotten about.

A realistic scenario: custom allocation rule

A common SO customization: when stock for a line is split across multiple lots or locations, fulfill from the oldest lot first regardless of the warehouse's default picking sequence — a FEFO-style (first-expired-first-out) rule for a client selling something with shelf life, layered on top of Acumatica's standard allocation.

C# · SOShipmentEntry EXTENSION
public class SOShipmentEntry_FEFOAllocation_Extension : PXGraphExtension<SOShipmentEntry>
{
    protected virtual IEnumerable<INLotSerialStatus> OrderCandidateLots(string inventoryID, string siteID)
    {
        return PXSelect<INLotSerialStatus,
            Where<INLotSerialStatus.inventoryID, Equal<Required<INLotSerialStatus.inventoryID>>,
                And<INLotSerialStatus.siteID, Equal<Required<INLotSerialStatus.siteID>>,
                And<INLotSerialStatus.qtyOnHand, Greater<decimal0>>>>,
            OrderBy<Asc<INLotSerialStatus.expireDate>>>
            .Select(Base, inventoryID, siteID).RowCast<INLotSerialStatus>();
    }
}

This example sketches the shape of the lookup — sorting candidate lots by expiration date ascending — rather than the full allocation override, which also has to handle partial-lot splits and reserve/unreserve bookkeeping that Acumatica's own allocation engine already does; the safest implementation calls into the existing allocation API with a pre-sorted lot list rather than reimplementing allocation and reservation from scratch.

Pricing and discount interactions

Custom pricing logic (a customer-specific override, a promotional rule the price/discount matrices can't express) needs to run at the point where the line's unit price is calculated, and needs to interact correctly with the stock discount engine rather than bypass it — a customization that sets CuryUnitPrice directly after the stock pricing engine has already run can produce an order where the displayed discount percentage no longer matches the actual price, which is confusing for the person reviewing the order and worse for anyone auditing margin later.

Testing considerations

Test against a multi-line order with mixed stock/non-stock items, at least one line requiring lot or serial allocation, and a partial shipment — SO extensions frequently work fine on a single-line, single-shipment order and then misbehave the moment a real order with backorders and split shipments runs through them.

Wrapping up

Sales Order extensions are where the most customization budget usually goes, and the ones that hold up over time are the ones that extend the existing hold, pricing, and allocation mechanisms rather than building parallel versions of them. When in doubt about which graph owns a piece of logic, trace where inventory actually gets committed — that's usually the right layer.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.