লাইন-লেভেলে মাল্টি-কারেন্সি (IAS 21)। প্রতিটি JE লাইনে transaction, functional, reporting currency। FX রেট পোস্টিং-সময়ে ক্যাপচার। পিরিয়ড শেষে: closing-rate রিভ্যালুয়েশন। বাংলাদেশের পার্টনারদের জন্য functional currency সাধারণত BDT।
Chapter 5.6 — Multi-Currency Accounting
chapter: 05-accounting/06-multi-currency
version: 1.0.0
status: stable
last_reviewed: 2026-05-26
owners: [accounting, platform-engineering]
1. Purpose
Travel is intrinsically multi-currency. A Dhaka agency sells in BDT, pays Emirates in AED via IATA, settles with a Bangkok hotel in THB, and reports financials in BDT. This chapter explains how travoBooks models currency at every layer — from the operational event to the financial statement — and how foreign-currency translation and revaluation are kept consistent and auditable.
2. Why this matters in modern travel accounting
The most common multi-currency defect in travel back-offices is late translation: storing transactions as functional-currency-only and applying a single end-of-month rate. The result is silent loss of fidelity. By the time someone looks at margin on a USD-priced ticket, the original USD is gone.
travoBooks' rule is: never lose the original. Every monetary value is stored with its currency code. Functional-currency equivalents are additional, captured at the moment of posting with the rate used.
3. Industry relevance
IATA settles in airline currencies, often via local clearing in agency currency. Hotel suppliers quote in their currency. Payment gateways settle in their support currency, applying their own FX. The agency must reconcile across all of this. A multi-currency-correct system makes that reconciliation mechanical; an incorrect one makes it a manual nightmare.
4. Compliance considerations
| Standard | Implication |
|---|---|
| IAS 21 | Distinguishes functional currency (entity reports in) from presentation currency (statements presented in). Requires consistent translation. |
| IFRS 9 / IAS 21 | Monetary items revalued at closing rate each period; differences to P&L (realised vs unrealised). |
| Local GAAP variants | May require official central-bank rates (e.g. Bangladesh Bank, RBI) on specific dates. travoBooks supports a "regulator rate" source per partner. |
| Tax authorities | Often require the rate on transaction date (not period close) for VAT base calculation. |
5. Business logic
5.1 Three currencies on every transaction
- Transaction currency — the currency the event actually happened in. Permanent.
- Functional currency — the partner's statutory reporting currency. Stored on every JE line via
fx_rateandfunctional_amount. - Reporting currency — chosen at report-rendering time; computed from functional balances.
5.2 FX rate model
FxRate
├── id
├── source (provider | manual | regulator | partner)
├── source_ref (e.g. "openexchangerates", "bb-official", "user-upload")
├── from_currency
├── to_currency
├── rate (precise; stored as DECIMAL(18,8) minimum)
├── effective_at (timestamp; rates are point-in-time)
├── effective_date (date; for "official rate of this date" use)
├── created_at
└── partner_id (NULL if platform-shared; else partner-specific)
Rate selection at posting time follows a deterministic precedence:
- Partner-specific rate with
effective_at ≤ event_time, latest first. - Platform-shared rate with
effective_at ≤ event_time, latest first. - Fall back to provider live fetch (if enabled).
If all three fail, the post is rejected with FX_UNAVAILABLE.
5.3 Precision and rounding
- Amounts stored as
DECIMAL(18,2)for normal currencies;DECIMAL(18,3)for currencies that use three decimals (BHD, KWD, OMR);DECIMAL(18,0)for currencies without subunit (JPY, KRW, VND). - Rates stored as
DECIMAL(18,8). - The system maintains a
currency_minor_unitsreference: how many decimal places each currency uses. - Rounding rule: banker's rounding (round half to even) by default per JE line; configurable per partner to half-away-from-zero where required by local convention.
5.4 Translation rules
Different categories of accounts are translated differently per IAS 21:
| Account category | Translation rate |
|---|---|
| Monetary assets/liabilities (cash, AR, AP) | Closing rate at reporting date |
| Non-monetary assets at historical cost | Historical rate at acquisition |
| Non-monetary assets at fair value | Rate at fair-value date |
| Income and expenses | Rate at transaction date (or average for the period) |
| Equity | Historical rate (when contributed) |
travoBooks encodes these by tagging each account with a fx_translation_class and the period-end revaluation job applies the right rate.
5.5 Realised vs unrealised FX
- Realised FX gain/loss — crystallised on settlement (e.g., USD invoice paid in USD when functional is BDT and the BDT-equivalent at receipt date differs from the BDT-equivalent recorded at invoice issuance). Posted to
409x / 609x. - Unrealised FX gain/loss — on period close, monetary balances are revalued at the closing rate. Difference vs prior carrying amount →
4099 / 6099. Reversed at the start of the next period (so the next realised settlement is computed from the original rate).
5.6 Currency on master entities
| Entity | Default currency | Notes |
|---|---|---|
| Customer | default_invoice_currency |
A customer can be invoiced in multiple currencies; this is the default. |
| Supplier | default_settlement_currency |
The currency the partner pays them in (often the airline's BSP currency). |
| Bank account | One currency per bank account | A USD bank account is a separate row from a BDT account. |
| Tax profile | The jurisdiction's currency | Tax is computed in that currency and translated to functional. |
5.7 Group consolidation
For groups with partners in different functional currencies, the group reporting currency is set at the group level. Group reports translate each partner's functional balances at the appropriate rate (closing for balance sheet, average for P&L) and aggregate. Translation differences post to a group-level "Cumulative Translation Adjustment" memo balance.
6. Inputs → processing → outputs
6.1 Configuring currencies for a partner
partner_currencies:
functional: "BDT"
enabled: ["BDT", "USD", "EUR", "AED", "INR", "GBP", "THB"]
display_default: "BDT"
fx_provider: "openexchangerates"
fx_provider_base: "USD"
fx_refresh_cron: "@hourly"
regulator_rate_source: "bangladesh-bank" # optional, for VAT base calc
6.2 Processing on a foreign-currency JE line
- Compute
functional_amount = transaction_amount × fx_rate(transaction_currency → functional_currency, effective_at = event_time). - Round to functional currency's minor units.
- Persist both transaction and functional amounts plus the rate used.
- The JE's balance check (debits = credits) is performed in functional currency.
6.3 Period-end revaluation outputs
For each monetary account with non-functional balances, the period-close job:
- Identifies open balances (e.g., open AR invoices, open AP bills, foreign-currency bank balances).
- Translates each at the closing rate.
- Computes delta vs the carrying functional amount.
- Posts unrealised FX gain/loss JE.
- Writes a reversing JE dated the first day of the next period.
7. Module dependencies
- Reads: currencies reference,
fx_rates, accountfx_translation_class, period state, partner_currencies. - Writes:
fx_rates(on provider refresh), JEs (revaluation), audit logs. - Consumed by: every accounting and reporting module.
8. Security & permissions
| Permission | Allows |
|---|---|
currency.read.partner |
View enabled currencies |
currency.enable.partner |
Enable/disable currencies |
fx.read.partner |
View rates |
fx.override.partner |
Provide a manual rate (with reason, audited) |
fx.revalue.partner |
Run period revaluation |
🔒 Manual FX rate entries require a reason text and are audited; clusters of these are surfaced in the partner's monthly control report.
9. Validation rules
| Rule | Code |
|---|---|
| Currency code is ISO 4217 | CURRENCY_INVALID |
| Currency is enabled for partner | CURRENCY_DISABLED |
FX rate available at effective_at ≤ event_time |
FX_UNAVAILABLE |
| FX rate not in the future | FX_FUTURE |
| Manual rate within ±10% of platform-shared rate of the same date (configurable) | FX_OUTLIER (warning, not block, unless strict mode on) |
Account currency_mode = functional_only cannot accept foreign currency lines |
ACCOUNT_FUNCTIONAL_ONLY |
| Closing rate must exist on period end date for revaluation | FX_CLOSE_RATE_MISSING |
10. Error handling
- FX provider downtime: the system uses the most recent successful rate ≤ event time, with
source = "cached_after_outage". If outage exceeds 24 hours, an alert escalates. - Rate outlier: flagged in audit; the user can confirm with reason or override.
- Period revaluation partial failure: the entire revaluation is transactional; partial states are not visible.
11. Real-world example
Scenario. Partner P-001 (functional BDT). On 5 May 2026 they issue an invoice to a US customer: USD 10,000. The recorded FX rate is 1 USD = 109.5 BDT (so functional 1,095,000 BDT).
On 31 May (period close), the invoice is still open. The closing rate is 1 USD = 110.2 BDT. The closing carrying value would be 1,102,000 BDT.
Period revaluation JE on 31 May:
| Acct | 🅓 | 🅒 |
|---|---|---|
| 1022 AR – US Customer (unrealised) | 7,000 | |
| 4099 Unrealised FX Gain | 7,000 |
Reversing JE dated 1 June:
| Acct | 🅓 | 🅒 |
|---|---|---|
| 4099 Unrealised FX Gain | 7,000 | |
| 1022 AR – US Customer (unrealised) | 7,000 |
On 10 June, the customer pays USD 10,000. Spot rate that day: 1 USD = 110.8 BDT. Cash received in BDT-equivalent: 1,108,000.
Receipt JE (10 June):
| Acct | 🅓 | 🅒 |
|---|---|---|
| 1011 USD Bank | 1,108,000 | |
| 1022 AR – US Customer | 1,095,000 | |
| 4091 Realised FX Gain | 13,000 |
Result: total FX gain on this invoice across periods is BDT 13,000 — all realised. The period-end unrealised gain washed out via the reversing entry. No double-counting; both periods' P&L reflect what actually happened economically.
12. Step-by-step — period revaluation
13. Database tables touched
currencies— ISO reference + minor units.partner_currencies— what's enabled.fx_rates— rate store.fx_rate_sources— providers configured.journal_entries,journal_entry_lines— both transaction and functional amounts.revaluation_runs— audit of each revaluation execution.
14. Future scalability
| Need | Approach |
|---|---|
| Hyperinflation accounting (IAS 29) | Apply general price index to non-monetary items in qualifying economies. Add inflation_index reference. |
| Hedge accounting (IFRS 9) | Add hedging-relationship records linking exposure (e.g. open AP) to instrument (forward contract). |
| Crypto / non-fiat | Treat as commodity in equity-stake terms; not legal tender; out of scope for Phase 1 but the data model accepts new ISO-like codes. |
| Sub-minute rate granularity for high-frequency partners | Already supported via effective_at timestamp. Just refresh more often. |
15. Common pitfalls
- ⚠️ Storing only functional amounts and discarding the original. The original is needed for VAT base, for reconciliation, for refund eligibility, and for restatement.
- ⚠️ Using a single end-of-month rate for all month's transactions ("average rate"). Allowed by IAS 21 for P&L when transaction rates are impractical, but for a system that has transaction rates, use them.
- ⚠️ Forgetting to reverse the unrealised entry on the first day of the next period; otherwise double-counting on realisation.
- ⚠️ Using a manual override rate to "smooth" margins. The audit trail will surface this and it is a control breach.
- 🔒 Never round at multiple stages; round once, at the line level, to functional minor units.
Next: 07-deferred-revenue.md — revenue recognition timing and the deferral mechanism.