Workflow · Workflow

Acumatica Workflow — Bill Approval Patterns

Acumatica Workflow — Bill Approval Patterns is one of the most common workflow customisations in Acumatica. Every business has a process that does not fit the standard approval.

John Kihiu12 min read

AP bill approval (screen AP301000, graph APInvoiceEntry) is the workflow I have customized more than any other, because "approve the bill" is never actually the whole requirement — it's "approve the bill, but skip approval if it matches a PO within tolerance, and route differently for non-PO bills, and let the AP clerk override with a reason code." This post covers the bill-specific mechanics: the statuses involved, how PO-matched bills should short-circuit approval, and the two-way/three-way match interaction that trips up first attempts.

The status set you're actually working with

Out of the box, APInvoice.status includes Hold, Balanced, PendingApproval, Open, Closed, among others. Bill approval customizations live between Balanced (the bill is internally consistent — debits equal credits, required fields filled) and Open (approved and ready to pay). The Release action is the natural trigger point, but I've also seen requirements to gate approval earlier, at the point the bill leaves Hold, so it never becomes schedulable for payment batch selection while still pending — that changes which action you hook and forces you to think about what "still on hold" versus "pending approval" means to the AP team using the screen day to day.

C#
public static void Configure(WorkflowContext<APInvoiceEntry, APInvoice> context)
{
    context.Graph.WithTransitions(transitions => transitions
        .AddGroupFrom<APInvoice.status.Balanced>(g => g
            .Add(t => t
                .To<APInvoice.status.PendingApproval>()
                .IsTriggeredOn(a => a.Actions.Release)
                .When(APInvoice.docTotal.IsGreaterEqual(1000m)
                      & APInvoice.pOOrderNbr.IsNull()))   // non-PO bills only
            .Add(t => t
                .To<APInvoice.status.Open>()
                .IsTriggeredOn(a => a.Actions.Release)
                .When(APInvoice.pOOrderNbr.IsNotNull()      // PO-matched: skip approval
                      | APInvoice.docTotal.IsLess(1000m)))));
}

PO-matched bills: approval already happened upstream

The most common bill-approval mistake is requiring a second human sign-off on a bill that already passed a three-way match against a released purchase order and its receipt. If the PO itself went through acumatica-workflow-purchase-order-approval-style routing, and the bill's quantities and amounts fall within the matching tolerance configured on the AP Preferences screen, re-approving the bill is redundant friction that AP staff will route around by padding the tolerance until nothing ever gets flagged. My default: condition the bill's approval transition on POOrderNbr.IsNull() (no PO reference — it's a non-PO, discretionary spend bill) or on a computed WithinMatchTolerance unbound field that a graph extension sets during the match, and only route matched, in-tolerance bills straight to Open.

Out-of-tolerance matches still need eyes on them

A PO-matched bill that exceeds the configured price or quantity variance should still hit approval — don't let the presence of a PO number alone bypass the gate. I compute the tolerance check in a RowUpdated handler on the bill and store the result in an unbound field the workflow condition reads, since .When() can't itself compare against the PO's tolerance settings.

Letting a controller override, with an audit trail

Clients often want an escape hatch: a controller-level user can push a bill straight to Open, bypassing the approval chain, but only with a reason code logged. I implement this as a second action (a custom PXAction guarded by role, e.g. APManualApprove) wired into the same PendingApproval state as an additional outbound transition, rather than trying to make the approval map itself conditional on role — the approval map's job is who approves normally; the override is a deliberate escape valve and reads much more clearly as its own transition with its own trigger.

The silent blocker: unreleased AP subaccount restrictions

One gotcha specific to bills: if the bill's GL distribution touches a restricted subaccount the current user can't post to (row-level security on subaccounts), the Release action itself throws before the workflow transition is ever evaluated. This looks identical to "the approval workflow is broken" from the support ticket, but it's a security exception, not a workflow bug — check the trace for a PXSetPropertyExceptionConfigure method.

Wrapping up

Bill approval is straightforward once you stop treating every bill the same: split PO-matched from non-PO, respect match tolerance rather than the mere presence of a PO number, and give controllers an explicit, audited override action instead of a role hack buried in the approval map. Ninety percent of "our bill approval doesn't work right" tickets I get are one of those three things, not a broken transition.

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.