Billing rules are the piece of the Projects module that translates accumulated cost and effort into an actual customer invoice, and they're also one of the more misunderstood extension points — the rule engine is more configurable than most people assume, and custom code should only fill the specific gap configuration can't reach.
Core DACs and screens
PMBillingRule defines how project transactions on PMTran get converted into invoice lines — by cost-plus markup, by fixed milestone amount, by rate table lookup, or by a combination keyed on transaction type and cost code. Billing runs through a dedicated billing engine graph invoked from the project or from a batch billing process, which reads unbilled PMTran rows matching the rule's criteria and produces AR invoice data.
Extension points
Rate table and markup percentage differences by client, item category, or date range are usually expressible through the existing billing rule configuration (rate tables, effective-dated pricing). Custom code becomes necessary when the billing logic depends on something outside the rule engine's inputs — a contractual cap on total billable amount for a phase, a blended rate that changes based on cumulative hours billed to date, or an external approval gate before a milestone amount can be invoiced.
A surprising share of "we need custom billing logic" requests turn out to be solvable with a rate table that has more granularity than whoever scoped the requirement assumed — different rates by employee class, by task type, effective on different dates. Confirm the rule engine's existing configuration options are actually insufficient before writing a graph extension; it's a smaller, more upgrade-safe deliverable if configuration covers it.
A realistic scenario: contract cap enforcement
A frequent PM billing customization: a fixed-fee phase has a not-to-exceed cap, and the billing run needs to stop generating invoice amounts once cumulative billed-to-date hits the cap, flagging the excess as unbilled overage for internal cost tracking rather than silently invoicing over the contracted amount.
public class ProjectBillingEngine_ContractCap_Extension : PXGraphExtension<ProjectBillingEngine>
{
protected virtual decimal ApplyContractCap(PMTask task, decimal proposedAmount)
{
decimal billedToDate = GetBilledToDate(task.TaskID);
decimal cap = GetContractCap(task.TaskID);
if (cap <= 0) return proposedAmount; // no cap configured, bill as computed
decimal remainingCapacity = Math.Max(0, cap - billedToDate);
if (proposedAmount > remainingCapacity)
{
LogOverageForReview(task.TaskID, proposedAmount - remainingCapacity);
return remainingCapacity;
}
return proposedAmount;
}
}
Logging the overage rather than discarding it matters — project managers need visibility into work performed but not billable under the contract, both for staffing decisions and for the inevitable change-order conversation with the client.
Revenue recognition alignment
If revenue is recognized on a basis different from what's actually billed (percentage-of-completion vs. milestone billing, for instance), a billing rule customization needs to be checked against how it interacts with revenue recognition calculations — capping what's billed without adjusting what's recognized can create a mismatch between AR and recognized revenue that finance will have to reconcile manually every period.
Testing considerations
Test a full contract lifecycle: several billing runs approaching the cap, one that hits it exactly, and one that would exceed it — off-by-one and rounding errors in cap enforcement are common, and they're the kind of bug that only shows up on the run that actually crosses the threshold, not the several before it.
Wrapping up
Billing rule extensions pay off when they're scoped to the genuine gap between what the rule engine can already express and what the contract requires. Confirm rate tables and effective dating first, keep overage visible rather than silently discarded, and check alignment with revenue recognition before calling a cap-enforcement customization done.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.