Akuntansi Multi-Currency di ERP: Kurs, Laba Selisih, Rounding IDR

Foto oleh Polina Tankilevitch

Foto oleh Polina Tankilevitch
Pertama kali klien meminta saya menambahkan invoice USD ke ERP berbasis IDR, estimasi saya dua minggu. Perubahan skemanya selesai dua hari. Tiga bulan sisanya habis untuk semua hal di sekelilingnya: kurs mana yang berlaku di tanggal mana, apa yang terjadi saat invoice USD dibayar dengan kurs berbeda, bagaimana revaluasi akhir bulan bekerja, dan kenapa laporan AR aging tiba-tiba menampilkan customer berutang minus 37 rupiah. Multi-currency bukan sekadar field di form — ini subsistem akuntansi.
Artikel ini adalah desain yang saya harap saya punya sejak hari pertama, dibentuk dari pengalaman membangun AP/AR untuk ANCoraPRO di NestJS dan PostgreSQL: data model dua mata uang, pipeline kurs, laba rugi realized versus unrealized, dan realita rounding mata uang fungsional yang praktis tidak punya angka desimal.
Aturan yang tidak bisa ditawar dari IAS 21, standar akuntansi yang mengatur efek selisih kurs: entitas membukukan dalam mata uang fungsionalnya — untuk perusahaan Indonesia, hampir selalu IDR — dan transaksi mata uang asing ditranslasikan dengan kurs pada tanggal transaksi. Skema Anda harus mencerminkan dualitas itu di setiap line moneter: nilai dokumen dalam mata uang transaksi, dan nilai yang dibukukan dalam mata uang fungsional, dikonversi sekali saat posting lalu dibekukan.
Dibekukan adalah kata kuncinya. Kelas bug multi-currency terbesar yang saya lihat di ERP custom adalah hanya menyimpan nilai asing lalu mengonversi saat dibaca, memakai kurs terkini. Akibatnya laporan berubah retroaktif setiap tabel kurs di-update, dan dua cetakan ledger yang sama tidak cocok. Simpan kurs yang dipakai, simpan nilai hasil konversi, dan jangan pernah menghitung ulang keduanya:
-- Every monetary line stores BOTH currencies, always
CREATE TABLE journal_line (
id BIGSERIAL PRIMARY KEY,
journal_entry_id BIGINT NOT NULL REFERENCES journal_entry(id),
account_id BIGINT NOT NULL REFERENCES account(id),
-- transaction (document) currency
currency_code CHAR(3) NOT NULL, -- 'USD', 'SGD', ...
amount_txn NUMERIC(18,2) NOT NULL, -- signed
-- functional currency (IDR), converted at posting time
exchange_rate NUMERIC(18,6) NOT NULL, -- rate USED, frozen
amount_functional NUMERIC(18,0) NOT NULL -- IDR has no cents
);
-- The rate table keeps history; never overwrite a rate
CREATE TABLE exchange_rate (
currency_code CHAR(3) NOT NULL,
rate_date DATE NOT NULL,
rate NUMERIC(18,6) NOT NULL, -- 1 unit -> IDR
source TEXT NOT NULL, -- 'BI_JISDOR' | 'MANUAL'
PRIMARY KEY (currency_code, rate_date)
);Perhatikan amount_functional adalah NUMERIC tanpa desimal. IDR modern tidak punya subunit yang terpakai — sen ada di undang-undang tapi tidak di pembayaran — jadi membukukan nilai fungsional dalam rupiah utuh sejak awal menyelamatkan Anda dari saldo pecahan hantu di kemudian hari. Sisi mata uang transaksi tetap dua desimal karena USD, SGD, dan EUR memang memakainya.
Keputusan desain kedua adalah kurs mana berlaku kapan. Dalam praktik di Indonesia Anda akan berurusan dengan sampai tiga keluarga kurs, dan tabel kurs Anda harus tahu yang mana:
Secara operasional saya memperlakukan kurs sebagai data referensi append-only dengan kolom source: fetch harian otomatis menulis baris BI_JISDOR, finance bisa memasukkan override MANUAL untuk mata uang yang tidak tercakup feed, dan tidak ada yang meng-update baris yang sudah ada. Kalau ada kurs salah, Anda memasukkan kurs koreksi dan mem-posting ulang dokumen terdampak secara sengaja — perbaikan retroaktif diam-diam adalah awal pembukuan berhenti rekonsil.
Pro tip: buat service posting gagal dengan jelas saat tidak ada kurs untuk tanggal dokumen, alih-alih fallback ke kurs terakhir yang tersedia. Fallback terasa membantu saat development, lalu diam-diam membukukan invoice hari Jumat dengan kurs hari Rabu di production. Tim finance memaafkan posting yang terblokir; mereka tidak memaafkan angka salah yang ketahuan tiga bulan kemudian.
Selisih kurs datang dalam tepat dua rasa, dan mencampuradukkannya adalah bug konseptual paling umum di modul multi-currency buatan sendiri:
Laba rugi realized
Terjadi saat pelunasan. Invoice dibukukan dengan satu kurs, pembayaran terkonversi dengan kurs lain, dan selisihnya adalah uang nyata yang masuk laporan laba rugi begitu pembayaran efektif.
Laba rugi unrealized
Terjadi saat tutup periode. Saldo mata uang asing yang masih terbuka direvaluasi dengan kurs penutup. Tidak ada kas berpindah — ini penyesuaian di atas kertas, dan akan dibalik atau disesuaikan lagi periode berikutnya.
Logika pelunasan layak diberi contoh kerja, karena invariant subledger yang dilindunginya adalah yang menjaga AR dan AP tetap jujur:
Worked example — realized FX gain on a USD invoice
Day 1 Invoice issued USD 10,000 @ 16,200 = IDR 162,000,000
DR Accounts Receivable 162,000,000
CR Revenue 162,000,000
Day 35 Payment received USD 10,000 @ 16,450 = IDR 164,500,000
DR Bank 164,500,000
CR Accounts Receivable 162,000,000
CR FX Gain (realized) 2,500,000
The AR subledger must close to EXACTLY zero in BOTH currencies:
USD 10,000 - 10,000 = 0 and IDR 162,000,000 - 162,000,000 = 0
The 2,500,000 difference NEVER touches AR — it goes to FX gain.Invariant itu — open item harus menutup ke nol di mata uang transaksi DAN mata uang fungsional — adalah hal yang harus dites tanpa ampun. Setiap pembayaran parsial, setiap credit note, setiap write-off wajib mempertahankannya. Mesin akuntansi Odoo melakukan hal yang sama dengan jurnal exchange difference otomatisnya: selisih dibukukan ke akun FX khusus, tidak pernah ke piutangnya sendiri.
FX unrealized adalah batch job, dan cara paling bersih yang saya temukan untuk menjalankannya di ERP custom adalah prosedur empat langkah yang sepenuhnya bisa dibalik:
Pakai pola reversing entry meskipun akuntan Anda bilang ERP yang dipakainya dulu menyesuaikan saldo langsung di tempat. Pembalikan menjaga revaluasi benar-benar terpisah dari histori transaksional, artinya closing yang kacau bisa diulang dengan membalik satu jurnal, bukan mengurai open item yang sudah teradjust satu per satu.
Rounding terdengar seperti catatan kaki sampai trial balance Anda selisih 1 rupiah dan auditor ingin tahu kenapa. Aturan yang sekarang saya bangun sejak awal:
Tidak ada ilmu komputer canggih di sini. Ini cuma soal memutuskan kebijakan secara eksplisit, mengodekannya di satu tempat — service posting — dan menolak membiarkan tiap layar berhitung sendiri-sendiri.
Akuntansi multi-currency di ERP berdiri di empat kaki: penyimpanan dua mata uang dengan kurs yang dibekukan, pipeline kurs append-only yang disiplin, pemisahan ketat selisih realized dan unrealized, dan kebijakan rounding eksplisit yang ditegakkan di lapisan posting. Benahi itu semua dan sisanya — laporan, dashboard, aging — mengikuti dengan sendirinya, karena angka dasarnya bisa dipercaya.
Dan sebelum menulis kode apa pun, duduklah dengan akuntan klien selama satu jam dan sepakati kurs mana berlaku untuk dokumen mana. Percakapan itu lebih murah daripada refactor mana pun yang akan Anda lakukan.