Acumatica · Customization

Acumatica Field Attributes — Decimal Precision and the Ones That Bite

A practical guide to Acumatica field-level attributes — PXDBDecimal precision, PXDBString length, PXDefault, PXSelector, and the ones that look innocent but trip you up at scale.

John Kihiu12 min read

A client once asked me why their custom "landed cost markup" field, computed as a percentage and multiplied against a currency amount, was off by fractions of a cent on thousands of transactions — enough to fail their auditor's reconciliation. The root cause was a mismatched decimal precision between two attributes three customizations apart. Precision bugs in Acumatica are quiet: they don't throw exceptions, they just silently produce numbers that are subtly wrong, and they compound. Here's what I've learned about the attributes that control this.

PXDBDecimal: precision is the argument, not a database-level default

PXDBDecimal(n) declares how many digits after the decimal point the field stores and rounds to. This is not automatically inherited from the underlying SQL column's actual precision — the attribute enforces its own rounding on assignment, independent of what the column could technically hold:

C#
[PXDBDecimal(2)]                    // rounds to 2 places: currency-style
public decimal? UnitPrice { get; set; }

[PXDBDecimal(4)]                    // rounds to 4 places: unit cost, exchange rates
public decimal? UnitCost { get; set; }

[PXDBDecimal(6)]                    // rounds to 6 places: some quantity conversions
public decimal? ConvFactor { get; set; }

The trap: when you compute a new field from two existing fields with different precisions and don't explicitly control rounding on the result, you inherit whatever precision the target field's own attribute declares — sometimes silently truncating precision your calculation actually needed. A markup percentage stored as PXDBDecimal(2) applied against a PXDBDecimal(4) unit cost, multiplied and reassigned into another PXDBDecimal(2) field, rounds twice: once implicitly by whichever intermediate field held the value, and once on final assignment. Two roundings compound differently than one.

CommitChanges and currency-aware fields add a second layer

Fields using PXDBCurrency (as opposed to plain PXDBDecimal) tie precision to the currency's configured decimal places via CurySource, which can differ per currency — some currencies round to zero decimal places. If your custom calculation mixes a plain PXDBDecimal field with a PXDBCurrency field and assumes both round to 2 places, multi-currency clients will surface the bug the first time someone transacts in a currency with different rounding rules.

PXUIField's display formatting is cosmetic, not authoritative

A common misunderstanding: developers assume PXUIField's display formatting controls precision. It does not — it only controls how many digits the UI shows, entirely independent of what's actually stored and used in calculations:

C#
[PXDBDecimal(4)]                                  // stores 4 decimal places
[PXUIField(DisplayName = "Rate", DisplayMask = "0.00")]  // shows only 2
public decimal? UsrCommissionRate { get; set; }

This is legitimate and common — you often want more precision stored than displayed, especially for rates and factors that compound over many transactions. The bug pattern is the reverse assumption: a developer sees a field displaying 2 decimal places and assumes it's safe to compare or round it to 2 places elsewhere in code, when the actual stored value carries 4 or 6. Always check the PXDBDecimal argument, never the display mask, when reasoning about what a field actually holds.

PXDefault interacts with precision in a way that bites on inserts

Because every DAC decimal property must be nullable (decimal?), a field without an explicit PXDefault can enter the cache as null and stay null through several event handlers before any arithmetic touches it — and null-times-anything in a naive calculation throws, or worse, is silently coerced to zero depending on how the arithmetic is written:

C#
// Fragile — throws on null Qty or UnitPrice during row creation,
// before FieldDefaulting has run for those fields
e.Row.UsrExtPrice = e.Row.Qty.Value * e.Row.UnitPrice.Value;

// Correct — explicit null handling, precision-aware rounding
e.Row.UsrExtPrice = decimal.Round(
    (e.Row.Qty ?? 0m) * (e.Row.UnitPrice ?? 0m), 2, MidpointRounding.AwayFromZero);

Acumatica's own base fields handle this defensively throughout the framework; custom calculation code frequently doesn't, because it's easy to test only with fully-populated rows and never exercise the moment a row is freshly inserted with nulls still in flight.

MidpointRounding is not a formality

.NET's default Math.Round behavior is banker's rounding (round-half-to-even), which does not match how most accounting systems and most humans expect currency to round (round-half-away-from-zero). Acumatica's own currency-aware attributes handle this correctly internally, but custom calculations using bare decimal.Round without specifying MidpointRounding.AwayFromZero will occasionally disagree with the ERP's own totals by a cent — exactly the kind of discrepancy that makes an auditor's reconciliation fail and takes hours to trace back to a rounding-mode default nobody thought to specify.

A short checklist before shipping a calculated decimal field

Wrapping up

Decimal precision bugs in Acumatica don't crash anything — they just make numbers quietly wrong in ways that only surface during reconciliation, months after the customization shipped. Treat the PXDBDecimal argument as the actual source of truth for stored precision (never the display mask), null-guard every operand explicitly, and set rounding mode deliberately rather than trusting the runtime default. It's a five-minute discipline that prevents a very unpleasant audit conversation.

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.