· sistem  · 10 min read

Forecast Cashflow dari Accurate: Satu Setengah Jam Seminggu Jadi Sepuluh Menit

Forecast cashflow satu setengah jam per minggu jadi sepuluh menit, sementara tab dan email tetap menunggu keputusan orang.

Forecast cashflow satu setengah jam per minggu jadi sepuluh menit, sementara tab dan email tetap menunggu keputusan orang.

Saya menarik data Accurate Online ke Google Sheets dengan Apps Script yang memanggil API Accurate langsung, memakai API Token dan signature HMAC-SHA256 di setiap request. Hasilnya dua sheet: forecast cashflow yang dibangun ulang tiap Senin pagi, dan daftar piutang yang diperbarui tiap pagi.

Sebelumnya, forecast cashflow saya kerjakan sendiri. Tiap minggu saya buka satu per satu sales order, faktur penjualan, purchase order, dan faktur pembelian di Accurate, lalu menyalin angkanya ke Google Sheets. Minggu berikutnya sheet lama diduplikat, dicocokkan lagi mana yang sudah terproses atau lunas, dan yang selesai dihapus.

Ritual itu paling rawan di termin. PO 12 juta yang ditagih 4 juta per bulan dicek tangan setiap bulan sampai lunas.

Piutang dipegang tim finance. Mereka cek jatuh tempo, ambil nilai faktur di Accurate, menulis email per customer, lalu mengirimnya.

Waktu kerja sebelum dan sesudah otomasi, beserta yang masih dikerjakan tangan:

PekerjaanDipegangSebelumSesudahYang masih dikerjakan tangan
Forecast cashflow mingguan dari sales order, purchase order, dan faktur AccurateSayaSekitar 1,5 jam per mingguSekitar 10 menit per mingguMengedit dua nama tab rollover, memeriksa tab DRAFT, mengisi saldo tiga rekening
Reminder piutang ke customerTim financeSekitar 30 menit per minggu, 31 email reminder dalam sebulanSekitar 5 menit per mingguMencentang Send? per baris, lalu memilih menu Send Reminders

Script piutang selesai dibangun dalam sekitar 3 jam.

Yang diotomatiskan hanya penarikan datanya. Tab forecast baru dipakai setelah saya hapus label DRAFT-nya. Reminder piutang tidak terkirim sebelum ada orang yang mencentang kolom Send?.

Integrasi API Accurate Online: pakai OAuth atau API Token?

Untuk API Accurate Online, pakai API Token ditambah Signature Secret. Saya mencoba OAuth lebih dulu, berhenti di access_denied, lalu meninggalkan jalur itu.

API Token didapat dari Accurate Store setelah aplikasi dipasang, dan terikat ke App ID aplikasi tersebut. Mencabut aplikasinya berarti mematikan semua script yang memakai token itu. Signature Secret diambil dari developer.accurate.id, satu per aplikasi.

Setiap panggilan butuh tiga header:

HeaderIsi
AuthorizationBearer {token}
X-Api-Timestampwaktu sekarang, format dd/mm/yyyy hh:nn:ss
X-Api-SignatureBase64 dari HMAC-SHA256 atas timestamp, dengan Signature Secret sebagai kunci

Yang ditandatangani cuma timestamp. Di Apps Script bentuknya begini:

function getSignature(timestamp) {
  var sig = Utilities.computeHmacSha256Signature(
    timestamp, getCredential_(PROP.SIGNATURE_SECRET));
  return Utilities.base64Encode(sig);
}

function getHeaders() {
  var ts = getTimestamp();
  return {
    'Authorization': 'Bearer ' + getCredential_(PROP.API_TOKEN),
    'X-Api-Timestamp': ts,
    'X-Api-Signature': getSignature(ts),
  };
}

Script membangun ulang ketiga header di setiap panggilan, termasuk saat mengirim ulang request. Token dan secret dibaca dari Script Properties dan tidak pernah ditulis di kode.

Panggilan pertama adalah POST ke https://account.accurate.id/api/api-token.do. Response-nya memuat alamat host database di data.d.database.host, dan data.d berupa objek tunggal, jadi dibaca tanpa indeks. Semua panggilan data sesudahnya berbentuk GET {host}/accurate/api/{endpoint}.

Endpoint itu bisa membalas HTTP 308 saat Accurate memindahkan host database. UrlFetchApp tidak mengikutinya sendiri, jadi script membaca header Location lalu mengirim ulang POST ke alamat itu dengan header yang baru.

Kenapa nama customer tidak muncul di response API Accurate?

Endpoint list Accurate hanya mengembalikan field yang disebut di parameter fields, dan nama customer tidak tersedia di endpoint list. Nama itu baru ada di detail.do, jadi setiap record butuh satu panggilan tambahan.

Field yang tidak disebut hilang dari response, termasuk key-nya. Script yang membaca r.customerName dari hasil list mendapat undefined, tanpa error.

Polanya di script forecast saya, untuk sales order:

var records = accurateList('/accurate/api/sales-order/list.do', {},
  ['id', 'number', 'transDate', 'totalAmount', 'statusName', 'description']);

for (var i = 0; i < filtered.length; i++) {
  var r = filtered[i];
  var detail = accurateDetail('/accurate/api/sales-order/detail.do', r.id);
  var customerName = detail.customer ? detail.customer.name : '';
  var netDays = detail.paymentTerm ? detail.paymentTerm.netDays : 0;
}

Panggilan detail itu sekalian mengambil paymentTerm.netDays. Untuk sales order yang belum jadi faktur, estimasi jatuh temponya dihitung dari tanggal transaksi ditambah netDays.

Untuk faktur, panggilan yang sama mengambil primeOwing, yaitu sisa tagihan. totalAmount di endpoint list adalah nilai faktur penuh, jadi faktur yang sudah dibayar sebagian, misalnya lewat uang muka atau potongan PPh 23, akan tercatat penuh kalau angkanya diambil dari list.

Ongkosnya ada di jumlah panggilan. Forecast menarik empat jenis dokumen, yaitu sales order, purchase order, faktur penjualan, dan faktur pembelian, dan tiap record yang lolos filter memicu satu detail.do. Batas pemanggilan API Accurate adalah 8 panggilan per detik (Okt-26), jadi script tidur 150 milidetik setelah setiap panggilan.

Jeda itu juga yang membuat waktu eksekusi naik seiring jumlah record. Apps Script menghentikan satu eksekusi di menit ke-6, menurut halaman kuota resmi Google Apps Script (Okt-26).

Script reminder piutang memakai pola yang sama, ditambah satu panggilan lagi. Ia memindai seluruh faktur penjualan, menyaring yang berstatus Belum Lunas di sisi script, lalu memanggil detail.do untuk nama customer dan sisa tagihan. Kalau email customer tidak ada di detail faktur, script memanggil customer/detail.do sekali per customer dan menyimpan hasilnya di cache.

Script piutang juga punya retry dengan backoff eksponensial, mulai dari 1 detik.

Kenapa tanggal dari API Accurate jadi salah tanpa error?

Accurate mengirim tanggal berformat dd/mm/yyyy, sedangkan new Date() di JavaScript membaca string bergaris miring sebagai mm/dd/yyyy. Tanggal 1 sampai 12 tertukar diam-diam, dan tanggal 13 ke atas jadi Invalid Date, juga tanpa exception.

Contohnya 05/02/2026. Accurate memaksudkannya 5 Februari 2026, sedangkan new Date() membacanya 2 Mei 2026.

Di forecast cashflow, kesalahan jenis pertama yang paling berbahaya. Faktur yang jatuh tempo 5 Februari pindah ke kolom Mei, totalnya tetap terlihat wajar, dan tidak ada sel yang error. Yang salah cuma bulan tempat angka itu jatuh.

Perbaikannya adalah memecah string itu sendiri.

function parseAccurateDate_(dateStr) {
  if (!dateStr) return null;
  var parts = dateStr.toString().split('/');
  if (parts.length !== 3) return null;
  return new Date(
    parseInt(parts[2], 10),
    parseInt(parts[1], 10) - 1,
    parseInt(parts[0], 10)
  );
}

Bulan dikurangi satu karena konstruktor Date menghitung Januari sebagai 0.

Error yang melempar exception ketahuan di hari pertama. Tanggal yang tertukar baru ketahuan kalau ada yang mencocokkan satu faktur di sheet dengan Accurate secara manual.

Forecast mingguan: script menarik data, rollover membawa alokasi

Tiap Senin pagi, trigger mingguan menjalankan script forecast. Ia menarik sales order dan purchase order berstatus Menunggu diproses atau Sebagian diproses, serta faktur penjualan dan pembelian yang Belum Lunas, lalu menulis semuanya ke tab baru bernama Forecast <tanggal>. Tab minggu sebelumnya tidak disentuh.

Tiap dokumen masuk ke satu dari enam kolom bulan, menurut perkiraan jatuh temponya. Faktur yang sudah lewat jatuh tempo dilempar ke bulan berjalan, dan yang jatuh setelah bulan keenam ditumpuk di kolom terakhir. Akibatnya kolom pertama dan terakhir selalu memuat ekor yang tidak kelihatan dari angkanya saja.

Purchase order dihitung beda. Perkiraan jatuh temponya adalah hari ini ditambah netDays, bukan tanggal PO ditambah netDays, karena yang diperkirakan adalah kapan kas keluar. PO yang dibuat tiga bulan lalu dan belum diproses tidak akan dibayar tiga bulan lalu.

Script menaruh nilai penuh satu dokumen di satu bulan, dan itu tidak cocok untuk termin. PO customer senilai 12 juta yang ditagih 4 juta per bulan harus dipecah ke tiga kolom. Pemecahan itu keputusan saya, tidak ada di data Accurate. Alokasi yang sama juga tempat saya mengurangi sales order dan purchase order berstatus Sebagian diproses, karena script menariknya dengan nilai penuh sementara bagian yang sudah difakturkan ikut masuk lagi sebagai faktur Belum Lunas.

Kalau pemecahan itu harus diulang tiap Senin, otomasinya tidak menghemat apa pun. Script kedua menyalin angka per bulan dari tab minggu lalu yang sudah saya periksa ke tab baru. Baris dicocokkan lewat nomor dokumennya, misalnya SO26-0068, dan hanya di dalam bloknya sendiri.

Dokumen yang baru muncul minggu ini dibiarkan seperti keluaran script. Dokumen yang sudah hilang dari tarikan Accurate tidak ikut disalin. Setiap sel yang ditulis rollover diberi warna kuning, supaya angka bawaan minggu lalu terbedakan dari angka yang saya isi sendiri.

Blok saldo bank sengaja tidak ikut. Saldo adalah posisi kas aktual, dan angka minggu lalu yang terbawa ke minggu ini akan terlihat sah padahal sudah basi. Saldo tiga rekening tetap saya isi tangan.

Rollover tidak punya trigger. Sebelum menjalankannya, saya mengedit dua nama tab di konfigurasinya:

var CFG = {
  SOURCE_SHEET: '1008',        // tab lama, alokasinya sudah diperiksa
  TARGET_SHEET: 'DRAFT 2408',  // tab baru dari script forecast
};

Nama tab itu juga satu-satunya penanda status. Script menulis Forecast <tanggal>, saya menggantinya jadi DRAFT 2408, memeriksa isinya, lalu menghapus kata DRAFT begitu angkanya cocok. Tab yang sudah bersih dari DRAFT jadi sumber rollover Senin berikutnya.

ACCURATE ONLINEForecast tanggalDRAFT ddMMddMMSO, PO, faktur belum lunasnilai penuh di satu bulanrollover menyalin alokasiboleh dipakaiotomatis, Senin pagiganti namaperiksa, isi saldo banktab yang sudah diperiksa jadi sumberrollover Senin berikutnyascripttangan saya

Sekitar 10 menit per minggu yang tersisa sudah termasuk mengedit dua nama tab di konfigurasi tadi.

Reminder piutang: script menyiapkan, finance yang mengirim

Script piutang jalan tiap pagi. Ia menarik faktur Belum Lunas, menghitung umur tagihannya, dan menyarankan satu dari empat jenjang reminder: H-3, H+1, H+7, atau H+14. Fungsi pengirim emailnya tidak punya trigger sama sekali.

Email baru keluar setelah tim finance mencentang kolom Send? di baris tertentu, lalu memilih menu Send Reminders. Baris yang tidak punya jenjang dilewati walau sudah dicentang.

Satu baris piutang dicentang tangan sebelum reminder dikirim

Jenjangnya dihitung dari umur faktur dan dari jenjang terakhir yang sudah terkirim. Faktur berumur 9 hari yang belum pernah diingatkan langsung mendapat H+7, tanpa melewati H-3 dan H+1. Setelah H+14 terkirim, statusnya jadi Done dan baris itu tidak bisa dikirim lagi.

Satu email bisa memuat beberapa faktur untuk alamat yang sama, dan subjeknya mengikuti jenjang tertinggi di antaranya.

Gerbang manusia itu ada karena status Belum Lunas di Accurate tidak selalu berarti customer belum membayar:

KasusYang tercatat di AccurateKalau reminder terkirim otomatis
Customer memotong PPh 23 sebesar 2% dari nilai jasaFaktur tetap Belum Lunas dengan sisa 2%, sampai potongannya dicatatCustomer yang sudah membayar menerima tagihan
Dana masuk kurang Rp 2,500 atau Rp 6,000 karena biaya admin transfer bankFaktur tetap Belum Lunas dengan sisa senilai biaya transferCustomer diingatkan membayar biaya yang dipotong bank

Dua reminder lain di Edavos, perpanjangan kontrak IT Support dan masa garansi atau lisensi, mengirim sendiri dan menahan diri lewat stempel tanggal. Untuk piutang, yang menahan adalah orang. Tagihan yang salah kirim ke customer lebih mahal daripada reminder kontrak yang salah kirim.

Kenapa API Accurate memunculkan error Unexpected token '<'?

Error Unexpected token '<' dari API Accurate muncul karena Accurate membalas dengan halaman HTML, dan script langsung menyerahkan body-nya ke JSON.parse. Karakter < adalah awal tag HTML, dan yang gagal di sini adalah parsing balasannya.

Gejalanya mirip token mati, padahal panggilan yang sama dengan token yang sama berhasil kalau diulang. Balasan HTML ini muncul di tengah run panjang, misalnya saat script memanggil detail.do untuk setiap faktur.

Perbaikannya adalah memeriksa status dan bentuk body sebelum JSON.parse dipanggil.

var body = response.getContentText();

if (code === 429 || code >= 500) {
  throw retryableError_('HTTP ' + code + ' dari ' + url);
}
if (code !== 200) {
  // 401/403/404: bukan gangguan sesaat, jangan diulang.
  throw new Error('Accurate HTTP ' + code + ' dari ' + url + ' - ' + body.slice(0, 200));
}

var firstChar = body.replace(/^\s+/, '').charAt(0);
if (firstChar !== '{' && firstChar !== '[') {
  throw retryableError_('Body non-JSON dari ' + url);
}

return JSON.parse(body);

Error yang ditandai retryableError_ diulang sampai tiga kali, dengan jeda 1, 2, lalu 4 detik. Status 429, 5xx, body non-JSON, dan 308 tanpa header Location masuk kelompok ini.

Status 4xx lain langsung berhenti, dan pesan errornya membawa 200 karakter pertama body. Token yang memang salah tetap gagal di percobaan pertama, dengan pesan yang menyebut sebabnya.

Cek satu faktur sebelum percaya forecast Anda

Kalau Anda sudah punya sheet yang ditarik dari Accurate, buatan sendiri atau buatan vendor, pilih satu faktur Belum Lunas yang sudah dibayar sebagian dan jatuh temponya belum lewat. Tanggal jatuh temponya harus di antara 1 dan 12, dan berbeda dari angka bulannya, misalnya 5 Februari.

Cocokkan baris faktur itu di sheet dengan Accurate. Angkanya harus sama dengan sisa tagihan, dan faktur itu harus ada di kolom Februari.

Kalau angkanya masih nilai faktur penuh, sheet Anda membaca totalAmount dari endpoint list. Kalau faktur itu muncul di kolom Mei, tanggalnya dibaca lewat new Date(). Keduanya tidak memunculkan error, dan satu faktur ini cara tercepat menemukannya tanpa membuka kode.

- Daud (@daudwihardi)

Daud Wihardi

Catatan Terkait

Lihat semua catatan »
34 Pekerjaan Berulang yang Jalan Sendiri, dalam Tiga Lapisan

34 Pekerjaan Berulang yang Jalan Sendiri, dalam Tiga Lapisan

Inventaris otomasi di kantor saya, dibagi tiga lapisan, dan kenapa lapisan paling membosankan dibangun duluan.

Wiki Karpathy Baru Separuh Loop

Wiki Karpathy Baru Separuh Loop

Wiki membuat pekerjaan saya bisa ditanyakan ke Claude. Yang membuatnya belajar adalah keputusan yang punya kondisi batal.

Skill Claude saya mengulang rekomendasi yang sudah dikerjakan

Skill Claude saya mengulang rekomendasi yang sudah dikerjakan

Perbaikannya satu file teks yang menyimpan tiap keputusan beserta kondisi yang membatalkannya.

Tujuh dari 12 Claude Skill yang Saya Hapus Ternyata Cuma Data

Tujuh dari 12 Claude Skill yang Saya Hapus Ternyata Cuma Data

Cara memisahkan instruksi dari data di folder kerja Anda, dan berapa yang saya bayar karena melewatkannya selama dua bulan.

Otomatisasi Lead dengan n8n: 80 Leads Sebulan Tanpa Ada yang Jaga Inbox

Otomatisasi Lead dengan n8n: 80 Leads Sebulan Tanpa Ada yang Jaga Inbox

Delapan node n8n mengganti dua orang yang bergantian membuka inbox, dengan ongkos di bawah Rp 90,000 sebulan.

Website Company Profile: WordPress atau Astro?

Website Company Profile: WordPress atau Astro?

Banyak website company profile kena hack bukan karena diserang, tapi karena ditinggal.