Ringkasan Utama
Menambahkan invoice USD ke ERP berbasis IDR memakan dua hari perubahan skema dan tiga bulan untuk segala hal di sekitarnya: kurs mana yang berlaku pada tanggal mana, bagaimana invoice yang dibayar pada kurs berbeda direkonsiliasi, dan bagaimana pembulatan Rupiah ditangani agar ledger tetap seimbang.
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.
Data Model: Setiap Line Membawa Dua Mata Uang
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.
Kurs: Satu Pipeline, Tiga Jenis Kurs
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:
- Kurs transaksi — kurs pada tanggal dokumen, dipakai membukukan invoice dan tagihan. Kebanyakan klien memakai kurs referensi JISDOR dari Bank Indonesia untuk USD; dipublikasikan per hari kerja dan paling tidak bisa diperdebatkan saat audit.
- Kurs pajak — Kementerian Keuangan menerbitkan kurs mingguan yang wajib dipakai untuk dokumen pajak. Invoice Anda bisa sah membawa satu kurs untuk akuntansi dan kurs lain untuk perhitungan PPN; desain untuk kedua field, jangan asumsikan sama.
- Kurs penutup — kurs pada tanggal neraca, dipakai merevaluasi saldo moneter terbuka di akhir bulan sesuai IAS 21. Inilah yang menggerakkan laba rugi unrealized.
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.
Laba Selisih Kurs Realized vs Unrealized: Dua Peristiwa Berbeda
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.
Revaluasi Akhir Bulan Tanpa Drama
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:
- Pilih semua item moneter terbuka dalam mata uang asing: line AR dan AP yang belum dibayar, rekening bank valas, uang muka dan deposit.
- Revaluasi masing-masing dengan kurs penutup tanggal akhir periode dan hitung delta terhadap nilai buku fungsionalnya saat ini.
- Posting satu jurnal revaluasi per mata uang, bertanggal hari terakhir periode, terhadap akun laba dan rugi FX unrealized.
- Posting pembalik otomatis bertanggal hari pertama periode berikutnya. Pelunasan kemudian menghitung selisih realized terhadap kurs pembukuan asli, tidak terganggu noise revaluasi.
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.
Realita Rounding IDR
Rounding terdengar seperti catatan kaki sampai trial balance Anda selisih 1 rupiah dan auditor ingin tahu kenapa. Aturan yang sekarang saya bangun sejak awal:
- Round di level line, sekali, saat posting — half-up ke rupiah utuh untuk nilai fungsional. Menjumlahkan line yang sudah dibulatkan itu stabil; membulatkan hasil penjumlahan tidak bisa direproduksi dari komponennya.
- Paksa jurnal balance setelah rounding: kalau debit dan kredit selisih beberapa rupiah akibat rounding per line, posting selisihnya ke akun rounding khusus secara otomatis. Jangan biarkan posting gagal atau, lebih buruk, mengakali line sungguhan.
- Simpan kurs dengan enam desimal. Kuotasi bergaya JISDOR membawa presisi lebih dari dua desimal, dan memotong kurs adalah sumber drift kumulatif diam-diam di akun bervolume tinggi.
- Selesaikan residu kecil otomatis: pembayaran yang menyisakan saldo terbuka di bawah ambang yang bisa dikonfigurasi, misal 100 rupiah, sebaiknya otomatis di-write-off ke akun rounding alih-alih menghantui AR aging selamanya.
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.
Intinya
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.

