Currencies, Exchange Rates, and Multi-Currency Accounting
Multi-currency is where homemade systems break first. Get this model right and a lot of hard problems become easy.
The currency types
Every posting is stored in at least two, usually three or more currencies simultaneously.
| Currency type | Also called | Meaning |
|---|---|---|
| Transaction currency | Document currency | The currency the deal was actually done in |
| Local currency | Company code currency, functional currency | The legal entity’s reporting currency — what the statutory accounts are in |
| Group currency | Global currency | The parent’s currency, for consolidation |
| Hard currency | A stable currency used in high-inflation countries | |
| Index-based currency | For hyperinflation reporting |
Example: a Bangladeshi subsidiary of a US group sells in EUR.
Invoice line:
Transaction currency EUR 10,000.00
Local currency BDT 1,320,000.00 @ 132.00
Group currency USD 10,800.00 @ 1.08
All three are stored on the journal line. Not computed on read — stored, because the rate at posting time is the legally correct one and rate tables get corrected retroactively.
Table shape
CREATE TABLE journal_line (
...
currency_code CHAR(3) NOT NULL,
amount NUMERIC(19,4) NOT NULL,
local_currency_code CHAR(3) NOT NULL,
amount_local NUMERIC(19,4) NOT NULL,
group_currency_code CHAR(3),
amount_group NUMERIC(19,4),
exchange_rate NUMERIC(19,9),
exchange_rate_date DATE,
exchange_rate_type VARCHAR(10)
);
Exchange rate types
You need more than one rate for the same currency pair on the same day:
| Type | Used for |
|---|---|
| Spot / daily | Actual transactions |
| Buying rate (bid) | Converting foreign receipts |
| Selling rate (ask) | Paying foreign suppliers |
| Average rate | P&L translation for the period |
| Closing rate | Balance sheet translation at period end |
| Historical rate | Equity items, fixed assets in some GAAPs |
| Planning / budget rate | Fixed for the year so budget variance isn’t FX noise |
| Customs rate | Set by the customs authority, mandatory for import valuation |
Customs/statutory rates are a real thing: many countries (India, Bangladesh, Brazil) publish an official rate that must be used for import duty calculation regardless of the market rate.
Rate storage: direct vs indirect quotation
Direct quotation: 1 USD = 121.50 BDT (units of local per 1 foreign)
Indirect quotation: 1 BDT = 0.00823 USD (units of foreign per 1 local)
Both exist in the wild. Store which convention you used, or you will silently invert rates. Also store a ratio/factor for currencies with large magnitude differences:
from=IDR, to=USD, ratio_from=100000, ratio_to=1, rate=6.30
Rounding — where the money actually gets lost
Rules:
- Store amounts as exact decimals (
NUMERIC/DECIMAL), never floats. Never. This is the #1 rule of financial code. - Currency decimal places vary: JPY and KRW have 0, most have 2, BHD/KWD/OMR/TND/JOD have 3, and some crypto/pricing contexts use more.
- Round at the right level. Common convention: calculate at line level, round at line level, then sum. Summing unrounded and rounding at the end gives different totals from what’s printed on the lines — and customers reconcile line by line.
- Total rounding differences must post somewhere. Every ERP has a “rounding difference” GL account, because
sum(rounded lines) ≠ rounded(sum)and the journal must still balance to zero. - Price precision ≠ amount precision. A unit price may need 5 decimals (fuel, chemicals, electricity) while the amount has 2.
unit_price NUMERIC(19,6), -- more precision
amount NUMERIC(19,4), -- storage precision
-- display/print rounded to currency decimals
FX gains and losses
Realised
When a foreign-currency receivable is settled at a different rate than when it was booked:
1 Mar: Invoice EUR 10,000 @ 132 BDT → AR 1,320,000
1 May: Customer pays EUR 10,000 @ 135 → Bank 1,350,000
Bank Dr 1,350,000
AR Cr 1,320,000
Realised FX gain Cr 30,000
Unrealised (FX revaluation)
At each period end, all open foreign-currency items are revalued at the closing rate. The difference goes to unrealised FX gain/loss. Under IFRS this is required for monetary items.
Key subtlety: the revaluation is usually reversed on the first day of the next period, so it doesn’t interfere with the eventual realised calculation. If your system doesn’t reverse it, you double-count.
Translation (consolidation)
Translating a subsidiary’s whole balance sheet into group currency:
- Assets/liabilities at closing rate
- Income/expenses at average rate
- Equity at historical rate
- The plug goes to Cumulative Translation Adjustment (CTA) in equity, not to P&L
This is IAS 21 / ASC 830. Getting CTA right is a consolidation-engine problem; know it exists.
Hyperinflation
IAS 29: when cumulative 3-year inflation exceeds ~100%, financial statements must be restated using a general price index before translation. Affects entities in Argentina, Turkey, Venezuela, Zimbabwe, Lebanon and others at various times. Real ERPs have a specific hyperinflation accounting function; don’t try to hand-roll it.
Practical rules
- Never use floating point for money. Use
NUMERIC/DECIMALin the DB,BigDecimal/decimal.Decimal/integer-minor-units in code. - Never store an amount without its currency code. A bare
amountcolumn is a bug. - Store the rate used on the document, don’t re-look-it-up. Rate tables get corrected retroactively; historical documents must not change.
- Rate lookup is: (from, to, rate_type, valid_from ≤ date) ORDER BY valid_from DESC LIMIT 1 — rates are effective-dated, and you take the latest one on or before the date, not an exact match.
- A missing rate must be a hard error, never a silent 1.0 or 0. A silent default here can misstate an entire period.
- Rate tables need a daily automated load with a monitoring alert when it fails, because month-end close blocks on it.
- Test with JPY (0 decimals), KWD (3 decimals), and a high-magnitude currency like IDR or VND. Two-decimal assumptions are everywhere.
Common bugs
| Bug | Symptom |
|---|---|
| Inverted quotation | Amounts off by rate² |
| Missing ratio/factor | Amounts off by 1,000 or 100,000 |
| Float arithmetic | Journals off by 0.01, refuse to balance |
| Rounding sum vs sum of rounded | Invoice total ≠ sum of printed lines |
| Rate looked up at read time | Historical reports change every day |
| No rate-type distinction | Balance sheet translated at average rate |
| Unrealised revaluation not reversed | Double-counted FX in the next period |
| Assuming 2 decimals | JPY amounts inflated 100× |