Acumatica · Customization

Multi-Currency DAC Handling in Acumatica

Multi-currency is the customization requirement that separates developers who've only worked single-currency instances from those who haven't, because every assumption about "a.

John Kihiu12 min read

Multi-currency is the customization requirement that separates developers who've only worked single-currency instances from those who haven't, because every assumption about "a decimal field" quietly breaks once a document can be denominated in a currency other than the base one. I've fixed enough multi-currency bugs in other people's customizations to have a short list of exactly where they hide.

PXDBCurrency ties a field to the document's currency, PXDBDecimal doesn't know currency exists

A plain PXDBDecimal field stores a number and rounds to a fixed, hardcoded number of decimal places, full stop - it has no concept of which currency the value is denominated in, and no idea that different currencies round to different numbers of decimal places (most round to 2, some to 0, a few to 3). PXDBCurrency is the attribute that actually understands multi-currency documents, tying a monetary field's rounding and formatting to the document's currency via a linked CuryInfoID:

C#
public sealed class SOLineExt : PXCacheExtension<SOLine>
{
    public static bool IsActive() => true;

    // Base-currency value - always in the company's functional currency
    [PXDBDecimal(2)]
    public decimal? UsrBaseCommission { get; set; }
    public abstract class usrBaseCommission : PX.Data.BQL.BqlDecimal.Field<usrBaseCommission> { }

    // Document-currency value - follows whatever currency THIS order is in,
    // rounds according to that currency's configured precision
    [PXDBCurrency(typeof(SOLine.curyInfoID), typeof(usrBaseCommission))]
    [PXUIField(DisplayName = "Commission")]
    public decimal? UsrCuryCommission { get; set; }
    public abstract class usrCuryCommission : PX.Data.BQL.BqlDecimal.Field<usrCuryCommission> { }
}

The pattern above - a base-currency field plus a paired currency-aware field, linked through PXDBCurrency's constructor arguments - is how Acumatica's own monetary fields work throughout the platform (OrderTotal / CuryOrderTotal, and so on). Writing a custom monetary field without following this pairing is the single most common multi-currency bug I see: a developer adds one plain PXDBDecimal field, it works fine in testing against the base currency, and breaks the first time a real transaction happens in a foreign currency.

Test every custom monetary field against a document in a non-base currency before shipping

A customization built and tested entirely against base-currency documents can look completely correct and still be fundamentally broken for any client who transacts internationally. If your test data is entirely USD on a USD-base instance, you have not actually tested multi-currency handling regardless of how much testing you've done. I now keep at least one foreign-currency customer and one foreign-currency vendor in every test dataset specifically to catch this.

CuryInfoID is the row's link to a specific exchange rate at a specific moment

Every currency-aware document DAC carries a CuryInfoID - a reference to a CurrencyInfo row holding the exchange rate, rate type, and effective date used for that specific document. This is not a live, always-current rate; it's a snapshot taken (or explicitly refreshed) at a point in the document's life, which is deliberate - an order's line totals shouldn't silently reprice themselves every time the market rate ticks, only when the business process says a rate refresh is appropriate:

C#
// Converting a document-currency amount to base currency explicitly,
// using the row's own CuryInfoID snapshot - not a fresh rate lookup
PXCurrencyAttribute.CuryConvCury(
    Base.Transactions.Cache, line, line.UsrCuryCommission ?? 0m, out decimal baseAmount);

Writing your own exchange-rate math against a live rate table instead of going through PXCurrencyAttribute's conversion helpers and the row's own CuryInfoID is how custom fields end up disagreeing with the document's own displayed totals - two numbers on the same screen, computed via two different rate snapshots, that should agree and don't.

Rounding precision is a property of the currency, not a constant you can hardcode

The assumption "money rounds to 2 decimal places" is wrong often enough to matter on any instance dealing with multiple currencies - some currencies are configured with 0 decimal places, some with 3. A custom calculation that hardcodes Math.Round(value, 2) instead of deferring to the currency's configured precision via PXDBCurrency will produce visibly wrong totals for any currency configured differently, and it's exactly the kind of bug that only a client transacting in that specific currency will ever notice.

Cross-field validation involving money needs to compare in one consistent currency

A validation rule like "commission can't exceed 10% of line total" needs both values compared in the same currency space - comparing a base-currency field against a document-currency field directly, without conversion, produces a nonsensical comparison the moment the document isn't in the base currency. This is a specific case of the general cross-field validation rule (do the comparison in RowPersisting, once every field is final) with an extra twist: convert to a common currency first, using the row's own CuryInfoID, before comparing.

Wrapping up

Every custom monetary field should be built as a base-currency/document-currency pair through PXDBCurrency, not a bare PXDBDecimal, and every conversion should go through PXCurrencyAttribute against the row's own CuryInfoID rather than hand-rolled rate math. Rounding precision belongs to the currency, not a hardcoded constant, and no multi-currency customization is actually tested until it's been run against at least one document in a genuinely non-base currency.

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.