Discount approval is unusual among the workflows in this series because the thing being gated isn't the document as a whole — it's a percentage on a line, or a manual override of a price the pricing engine already calculated. That means the workflow condition has to reach into child-table data or into a comparison against what the system expected, not just read a header field. This is the sales order (SO301000, SOOrderEntry) discount-override scenario I build most often.
Line-level discounts need a rollup, not a direct condition
.When() conditions run against the header DAC (SOOrder) in this graph's primary view; they cannot directly inspect whether any SOLine has a manual discount percent above a threshold, because that's a child-table aggregate. The pattern is the same one that shows up whenever workflow logic needs to cross tables: compute a header-level unbound field in a graph extension that scans lines, and condition the transition on that field instead.
protected virtual void SOLine_ManualDisc_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e)
{
var line = (SOLine)e.Row;
if (line == null) return;
var order = Base.Document.Current;
if (order == null) return;
bool anyLineOverThreshold = Base.Transactions.Select()
.RowCast<SOLine>()
.Any(l => l.ManualDisc == true && l.DiscPct >= 15m);
order.RequiresDiscountApproval = anyLineOverThreshold;
Base.Document.Update(order);
}
// workflow condition then reads the flag:
// .When(SOOrder.requiresDiscountApproval.IsEqual(true))
Manual overrides, not calculated discounts, are the actual risk
Acumatica's pricing engine can apply calculated discounts from discount schedules automatically — those are pre-approved by definition, since someone configured the schedule deliberately. The approval requirement is almost always specifically about SOLine.ManualDisc = true — a rep typing in a number that overrides what the engine would have calculated. Conditioning on discount percentage alone, without checking ManualDisc, means a large legitimate promotional discount from a schedule triggers the same approval friction as a rep giving away margin ad hoc, which trains sales staff to resent the workflow rather than respect it.
A flat "over 15% off list" rule breaks down the moment list price itself is stale or the item is already on a clearance schedule. The more robust condition compares the line's actual unit price against what SOLine.CuryUnitPrice would be with only the schedule discount applied — i.e. approve based on incremental margin erosion from the manual override, not the sticker discount percentage in isolation.
Where the approval state sits relative to order processing
Discount approval usually needs to happen before the order is allowed to proceed to shipment, but you don't necessarily want to block the whole order from being saved while a rep is still building it — reps add and remove lines, adjust discounts, iterate. My usual placement: gate the transition out of Open on the action that matters operationally, which for sales orders is typically Actions.Release or a custom "Submit for Fulfillment" action, not Save. Gating on Save means every keystroke re-evaluates a rollup across all lines, which is wasteful and can produce a jarring "pending approval" flip mid-edit before the rep is done.
Approvers need to see the specific line, not just the order total
A generic approval notification saying "SO012345 needs approval" is useless to someone approving a discount — they need to see which line, what percentage, and against what list price, without reopening the whole order and hunting. I extend the approval notification email template (tied into the assignment map) to include a rendered summary of just the flagged lines, pulled from the same rollup logic that set the approval flag, so the approver's inbox is the decision-making surface rather than a link they have to click through and then investigate.
Wrapping up
Discount approval workflows are really child-table aggregation problems wearing a workflow costume: compute the rollup into a header flag rather than fighting .When()'s single-table limitation, distinguish manual overrides from schedule-driven discounts, gate on a deliberate action rather than every save, and get the approver enough context in the notification itself that they don't have to reverse-engineer what they're approving.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.