Key Takeaway
Multi-currency in an Indonesian ERP means one functional currency, IDR, with every foreign transaction stored at its original amount and its Rupiah equivalent, plus the Ministry of Finance's weekly kurs pajak for PPN on tax invoices. Period-end revaluation then restates open balances at the closing rate, as PSAK 221 (formerly PSAK 10) requires, keeping the ledger consistent after the rate moves.
For Indonesian businesses that trade internationally — importing raw materials, exporting products, or billing clients in USD or SGD — multi-currency handling in an ERP is not optional. It also means living with two official sets of rates: Bank Indonesia's daily rates, which this module uses for the books, and the Ministry of Finance's weekly tax rate (kurs pajak), which the PPN on a foreign-currency tax invoice must use. In the projects I have worked on, finance teams at trading companies spent several days each month converting foreign currency transactions into Rupiah by hand. As a freelance ERP consultant, I built a multi-currency module for one of my clients that handles the full cycle: transaction entry in any currency, automatic IDR equivalent calculation, period-end revaluation, and consolidated Rupiah reporting.
The Currency Architecture
The system I built has a single functional currency (IDR — Indonesian Rupiah) as the base. All financial reports are in IDR. Foreign currency transactions are stored with both the original foreign currency amount and the IDR equivalent at the transaction date. This dual-amount approach is standard in accounting systems and simplifies reporting: the IDR amounts flow directly into the journal and require no conversion at report time.
Exchange Rate Storage and Updates
I maintain an exchange_rates table with columns: currency_pair (e.g., 'USD/IDR'), rate_date, buy_rate, sell_rate, and mid_rate. Each working day a NestJS cron job at 08:30 WIB pulls the Kurs Transaksi BI from Bank Indonesia's wsKursBI web service: buy and sell rates announced at 08:00 WIB, with the USD midpoint set at the previous working day's JISDOR (BI's USD/IDR reference rate, published at 15:15 WIB). The business can also enter custom rates manually when they use a rate negotiated with their bank — common for large USD transactions. All historical rates are retained (never overwritten) so I can reconstruct the exact IDR amount for any past transaction.
Multi-Currency ERP Architecture (IDR as Functional Currency)
Exchange Rate Source
├── Bank Indonesia wsKursBI (working days, 08:30 WIB) ← Kurs Transaksi BI
│ └── exchange_rates table (historical, never overwritten)
└── Manual override (for negotiated bank rates)
Foreign Currency Transaction Flow:
┌─────────────────────────────────────────────────────────┐
│ AP Invoice: USD 10,000 │
│ │
│ ERP looks up USD/IDR rate: 17,880 (example rate) │
│ │
│ Journal Entry posted: │
│ DEBIT Expense Account IDR 178,800,000 │
│ CREDIT AP Payable — USD USD 10,000 / IDR 178,800K │
└──────────────────────────┬──────────────────────────────┘
│ Payment date: rate 17,950
▼
┌─────────────────────────────────────────────────────────┐
│ Payment: USD 10,000 × 17,950 = IDR 179,500,000 │
│ │
│ DEBIT AP Payable — USD IDR 178,800,000 │
│ DEBIT Forex Loss Account IDR 700,000 ← diff │
│ CREDIT Bank — USD IDR 179,500,000 │
└─────────────────────────────────────────────────────────┘
Month-end Revaluation:
Open FC balances × closing rate − original IDR = unrealized P/L
Posted as reversing journal (reverses on day 1 of next period)From my freelance ERP consulting work: use Bank Indonesia's rate as the default, but always let the user override it for a specific transaction. PSAK 221 (renumbered from PSAK 10 by IAI from 1 January 2024) requires the spot rate on the transaction date, and the rate your bank actually applied often differs slightly from BI's, so store both on each transaction. Tax is separate: PP 44/2022 Article 21 requires PPN on a foreign-currency sale to be converted at the Minister of Finance's rate in force when the tax invoice should have been made, the weekly kurs pajak, so keep that rate on the invoice too.
Handling Foreign Currency Transactions
When an AP invoice arrives in USD, the entry screen shows USD amount, and the system looks up the current USD/IDR rate to compute the IDR equivalent. The journal entry records: debit Expense (IDR equivalent), credit AP Payable — USD (original USD amount + IDR equivalent). When the payment is made, if the IDR/USD rate has changed, the payment generates a foreign exchange gain or loss entry for the difference. This realized gain/loss is posted to a dedicated Forex Gain/Forex Loss account and appears on the P&L.
Period-End Revaluation
At month-end, open foreign currency balances (unpaid invoices, bank accounts in foreign currency) must be revalued at the closing rate. The revaluation job reads all open FC balances, computes the difference between the original IDR equivalent and the IDR equivalent at the closing rate, and posts unrealized gain/loss entries. PSAK 221 and IAS 21 require monetary items at the closing rate with the difference in profit or loss; reversing those entries at the start of the next period (marked with a reverse_on_date) is my design choice, so the payment still measures its realized gain or loss against the original rate. Without ERP support it is completely manual — I've seen companies skip it and produce materially inaccurate balance sheets as a result.
// NestJS: Bank Indonesia exchange rate fetcher
// Kurs Transaksi BI is announced at 08:00 WIB on working days
@Cron('30 8 * * 1-5', { timeZone: 'Asia/Jakarta' })
async fetchBankIndonesiaRates() {
const pairs = ['USD/IDR', 'EUR/IDR', 'SGD/IDR', 'CNY/IDR'];
const today = new Date().toISOString().split('T')[0];
const res = await fetch(
`https://www.bi.go.id/biwebservice/wskursbi.asmx/getSubKursLokal2?tgl=${today}`
);
// Parse BI XML: nil_subkurslokal is the quote unit (100 for JPY)
for (const { pair, unit, buy, sell } of parsedRates) {
await this.exchangeRateRepo.upsert({
currencyPair: pair,
rateDate: today,
buyRate: buy / unit,
sellRate: sell / unit,
midRate: (buy + sell) / 2 / unit,
source: 'bank_indonesia',
}, ['currencyPair', 'rateDate']);
}
this.logger.log(`Updated ${pairs.length} exchange rates from BI`);
}
// PostgreSQL: transaction schema with dual amounts
ALTER TABLE journal_lines ADD COLUMN foreign_currency VARCHAR(3);
ALTER TABLE journal_lines ADD COLUMN foreign_amount NUMERIC(15,2);
ALTER TABLE journal_lines ADD COLUMN exchange_rate NUMERIC(12,6);
-- IDR check: foreign_amount carries the line's sign (negative on credits)
ALTER TABLE journal_lines ADD CONSTRAINT chk_idr_consistency
CHECK (
foreign_amount IS NULL
OR ABS((debit - credit) - (foreign_amount * exchange_rate)) < 1
);How I Approached This
The multi-currency implementation adds three fields to every financial transaction: foreign_currency, foreign_amount, and exchange_rate. The IDR amount is always computed as foreign_amount × exchange_rate, and a PostgreSQL check constraint validates it on every journal line, catching rounding issues. I use 6 decimal places for exchange rates: USD/IDR (JISDOR Rp17,899.00 on 12 October 2026) does not need them, but Bank Indonesia quotes JPY per 100 yen and VND at 0.69, so the fetcher divides by the quote unit and stores per-unit rates.
If your client has multiple entities that transact with each other in different currencies, intercompany elimination in the consolidated accounts becomes significantly more complex. Both sides of the intercompany transaction may record different IDR amounts (because the rate on the invoice date differs from the rate on the payment date). This creates intercompany reconciliation differences that must be explicitly eliminated in the consolidation. I handle this with a dedicated intercompany elimination module and require all intercompany transactions to use a standardized monthly average rate rather than the spot rate — an approximation IAS 21 accepts only while rates do not fluctuate significantly.
Multi-Currency Bank Reconciliation
Bank reconciliation is more complex with multiple currencies: you're reconciling a USD bank account statement against USD transactions in the ERP, then also reconciling the IDR equivalents. I provide a reconciliation screen per bank account per currency, showing outstanding transactions in the foreign currency sorted by amount and date. The reconciliation marks which bank statement entries match which ERP transactions. Unmatched items flag as reconciliation exceptions for the finance team to investigate.
Management Reporting in IDR
All management reports are presented in IDR, with an optional secondary column showing the original foreign currency amounts for FC-heavy businesses. The P&L shows total revenue and costs in IDR, with a Forex line for net realized and unrealized gains/losses. This is important for management — a good operating month can show a poor bottom line if the Rupiah depreciated sharply against USD liabilities. Surfacing the Forex component separately helps management understand the true operating performance versus currency impact.
Sources & Further Reading
- Bank Indonesia — JISDOR, Kurs Acuan Non-USD/IDR, dan Kurs Transaksi
- NetSuite — The Multi-Currency Accounting Guide
- Dynamic Netsoft — ERP Framework for Multi-Country Operations
- Bank Indonesia — Kurs Transaksi BI (JPY quoted per 100 units)
- Bank Indonesia — JISDOR history
- IAI — Perubahan Penomoran PSAK dan ISAK (PSAK 10 menjadi PSAK 221)
- Kementerian Keuangan — Kurs Pajak (weekly KMK rates)
- JDIH BPK — PP No. 44 Tahun 2022 (Article 21: PPN conversion rate)
- BDO — Accounting for foreign exchange transactions under IAS 21
