# Desain Modul Inventory ERP: Movement, Valuasi, dan Guard

> Cara saya mendesain modul inventory ERP: ledger stock movement yang immutable, FIFO vs average costing, dan guard stok negatif yang tahan beban concurrent.

URL: https://www.matthewswong.com/id/blog/erp-inventory-management-module-design/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-04-02
- Updated: 2026-04-02
- Category: ERP

Setiap ERP yang saya lihat gagal di inventory selalu gagal dengan cara yang sama: seseorang menyimpan jumlah stok on-hand sebagai satu angka mutable di tabel item, meng-update-nya dari enam jalur kode berbeda, dan dalam tiga bulan angka di layar sudah tidak cocok dengan angka di rak gudang. Saat saya membangun alur AP/AR dan stok untuk ANCoraPRO, ERP production untuk klien nyata di Indonesia dengan React, NestJS, dan PostgreSQL, keputusan arsitektur pertamanya justru kebalikannya: kuantitas tidak pernah disimpan sebagai sumber kebenaran, selalu diturunkan dari ledger.

Artikel ini membahas tiga keputusan desain yang menentukan hidup-matinya modul inventory: memodelkan stock movement sebagai ledger immutable, memilih metode valuasi yang bisa Anda pertanggungjawabkan ke akuntan, dan membangun guard stok negatif yang tahan terhadap penulisan bersamaan. Bagian-bagian ini tidak bisa ditambal belakangan, karena semua modul lain — purchasing, sales, accounting — dibangun di atasnya.

## Stock Movement Itu Ledger, Bukan Counter

Insight intinya dipinjam langsung dari akuntansi double-entry: Anda tidak pernah mengedit saldo, Anda menambahkan transaksi. Setiap penerimaan, pengeluaran, transfer, dan penyesuaian menjadi satu baris immutable di tabel stock_movement dengan kuantitas bertanda. Stok on-hand untuk item apa pun di gudang mana pun hanyalah SUM dari movement-nya. Menghapus atau mengedit movement dilarang; koreksi dilakukan lewat movement kompensasi baru, yang otomatis memberi Anda audit trail gratis.

Setiap movement membawa reference_type dan reference_id yang menunjuk balik ke dokumen bisnis penyebabnya — baris purchase order, pengiriman penjualan, stock opname. Inilah yang membuat Anda bisa menjawab pertanyaan yang benar-benar ditanyakan tim gudang: bukan cuma berapa stok kita, tapi kenapa sistem bilang 40 padahal saya hitung 38. Dengan ledger, Anda bisa melacak selisih itu ke dokumen spesifik dalam hitungan menit.

Memang, SUM atas jutaan baris lama-lama melambat. Solusinya bukan meninggalkan ledger — melainkan baris snapshot periodik plus menjumlahkan hanya movement setelah snapshot, persis seperti bank menangani saldo rekening. PostgreSQL menangani pola ini dengan nyaman sampai ratusan juta baris sebelum Anda butuh sesuatu yang eksotis.

Pro tip: tambahkan tabel stock_snapshot bulanan sejak hari pertama, meskipun belum dibutuhkan untuk performa. Closing akhir bulan di accounting toh akan butuh saldo historis yang dibekukan, dan memasang snapshot ke ledger yang sudah live di minggu closing adalah penderitaan yang tidak saya rekomendasikan.

## Memilih Metode Valuasi: FIFO vs Average Cost

Valuasi menjawab pertanyaan yang berbeda dari kuantitas: berapa nilai stok ini, dan biaya berapa yang dibukukan saat stok keluar. Metode yang Anda pilih mengubah COGS, margin, dan posisi pajak, jadi keputusan ini diambil bersama akuntan klien, bukan sendirian di depan keyboard. Tiga metode yang relevan di praktik:

Metode

Bagaimana biaya mengalir

Paling cocok untuk

Biaya implementasi

FIFO

Biaya penerimaan tertua dikonsumsi lebih dulu; setiap pengeluaran dicocokkan ke layer penerimaan yang tersisa secara berurutan.

Barang dengan masa simpan, pelaporan margin yang stabil, bisnis yang alur fisiknya memang first-in-first-out.

Paling tinggi — Anda harus menyimpan cost layer per penerimaan dan melacak sisa kuantitas per layer.

Average (AVCO)

Setiap penerimaan menghitung ulang rata-rata tertimbang biaya per unit; setiap pengeluaran memakai rata-rata terkini.

Stok bersifat komoditas atau campuran, harga beli fluktuatif, tim yang ingin perhitungan akhir bulan sederhana.

Rendah — satu running average per item per scope costing, dihitung ulang saat penerimaan.

Standard

Biaya preset dipakai untuk semua movement; selisih terhadap biaya beli aktual dibukukan ke akun variance.

Manufaktur dengan tim cost engineering disiplin yang memelihara standarnya.

Kode menengah, disiplin proses tinggi — percuma kalau tidak ada yang memelihara standarnya.

Dokumentasi desain Business Central dari Microsoft jujur soal trade-off-nya: FIFO menggelembungkan nilai neraca saat harga naik, average cost meratakan volatilitas, dan movement yang di-backdate memaksa perhitungan ulang semua entri yang terdampak. Poin terakhir itulah yang sering menggigit custom build — kalau Anda mengizinkan penerimaan backdate, implementasi AVCO Anda harus me-replay rata-rata maju dari tanggal itu, atau pembukuan Anda melenceng diam-diam.

Untuk mayoritas klien SMB Indonesia, default saya adalah average cost. Harga beli dalam IDR cukup fluktuatif sehingga pelacakan layer FIFO menambah kompleksitas nyata, sementara AVCO memberi akuntan satu angka yang bisa dipertanggungjawabkan per item. Saya mengimplementasikan FIFO hanya kalau bisnisnya memang merotasi stok seperti itu dan tim finance yang memintanya. Dokumentasi Odoo sendiri menyebut FIFO paling akurat tetapi sangat sensitif terhadap data input dan human error — deskripsi jujur tentang apa yang akan Anda pelihara.

## Guard Stok Negatif yang Tahan Concurrency

Stok negatif adalah penyakit kualitas data klasik di modul inventory. Dua operator gudang mengeluarkan 5 unit terakhir di saat bersamaan, kedua pembacaan melihat on-hand 5, kedua penulisan sukses, dan sekarang sistem bilang minus 5. Semua laporan hilir — valuasi, COGS, saran reorder — diam-diam jadi salah.

Pola check-then-insert di kode aplikasi saja tidak cukup. Di antara SELECT dan INSERT Anda, transaksi lain bisa commit pengeluarannya sendiri. Anda butuh database yang menserialisasi penulis pada pasangan item-gudang yang sama: row-level locking, isolation level serializable, atau exclusion constraint. Code review tidak akan menangkap ini; load testing yang akan.

Pendekatan saya di NestJS dengan PostgreSQL: bungkus pembacaan dan penulisan dalam satu transaksi, ambil row-level lock pada movement untuk item dan gudang itu dengan SELECT FOR UPDATE, validasi saldo, lalu append. Lock memaksa pengeluaran bersamaan untuk item yang sama mengantre dan membaca ulang saldo yang benar:

Satu nuansa yang layak dibangun: flag allow_negative_stock per gudang. Beberapa klien benar-benar perlu kirim sekarang dan bereskan dokumen belakangan — blokir keras menghentikan bisnis, dan mereka akan mengakali sistem Anda lewat Excel, yang lebih buruk. Kompromi yang berhasil: izinkan per gudang, tandai setiap movement stok negatif untuk review wajib, dan tampilkan daftarnya di dashboard inventory sampai ada yang membereskannya.

## Reserved, Available, dan Empat Kuantitas yang Penting

Kesalahan desain kedua yang paling umum: menganggap on-hand sebagai satu-satunya kuantitas. Begitu Anda mendukung sales order yang sudah dikonfirmasi tapi belum dikirim, atau purchase order yang masih dalam perjalanan, satu angka tidak cukup. Anda butuh empat, dan UI harus eksplisit menunjukkan yang mana:

## On hand

Stok fisik di gudang saat ini — SUM dari ledger movement. Inilah yang dihitung saat stock opname.

## Reserved

Sudah dialokasikan ke sales order atau production order yang dikonfirmasi tapi belum dikeluarkan. Reservasi punya tabel sendiri, bukan movement.

## Available

On hand dikurangi reserved. Inilah angka yang dibutuhkan tim sales saat menjanjikan tanggal kirim — tampilkan ini, bukan on hand.

## Incoming

Baris purchase order terkonfirmasi yang belum diterima. Menjadi dasar logika reorder supaya Anda tidak memesan barang yang sudah di atas truk.

Jaga reservasi tetap di luar ledger movement. Reservasi adalah niat, bukan kejadian fisik — mencampur keduanya membuat valuasi salah dan audit trail berbohong. Saat barang fisik keluar, reservasi dikonsumsi dan movement ISSUE sungguhan ditambahkan dalam transaksi yang sama.

## Checklist Desain Sebelum Menulis Migration Pertama

Ini pertanyaan-pertanyaan yang sekarang saya paksa diri saya jawab sebelum menulis kode modul inventory, kira-kira berurutan dari yang paling menyakitkan kalau dilewati:

Apakah setiap perubahan stok adalah baris movement immutable dengan referensi ke dokumen bisnisnya? Bisakah on-hand selalu dibangun ulang dengan me-replay ledger?

Metode valuasi mana yang sudah disetujui akuntan klien — dan apakah tertulis? Mengganti metode belakangan adalah peristiwa akuntansi, bukan refactor.

Apa yang terjadi saat dua user mengeluarkan stok yang sama bersamaan? Buktikan guard-nya dengan tes concurrent, bukan sekadar membaca kode.

Apakah movement backdate diizinkan? Kalau ya, siapa yang menghitung ulang biaya terdampak dan berapa lama job itu pada volume realistis?

Apakah layar sales menampilkan available-to-promise atau on-hand mentah? Angka yang salah di sini berarti overselling, dan bisnis akan menyalahkan ERP-nya.

Bagaimana penyesuaian stock opname dibukukan — sebagai movement kompensasi dengan reason code, atau edit diam-diam? Hanya satu yang lolos audit.

Apakah konversi unit of measure ditangani di tepi (form input) sehingga ledger menyimpan tepat satu base unit per item?

## Intinya

Modul inventory adalah sistem akuntansi yang kebetulan menghitung kardus. Perlakukan kuantitas seperti uang: ledger append-only, saldo turunan, guard yang ditegakkan database, dan aturan valuasi yang disepakati dengan orang-orang yang menandatangani laporan keuangan. Setiap jalan pintas melawan prinsip itu berubah jadi rapat rekonsiliasi enam bulan setelah go-live.

Kalau Anda membangun ini di NestJS dan PostgreSQL seperti saya, semua yang Anda butuhkan sudah ada: transaksi, row lock, dan kolom NUMERIC. Bagian sulitnya bukan teknologi — melainkan menolak menyimpan satu kolom kuantitas mutable yang menggoda itu saat tekanan deadline bilang sebaiknya iya.

## Sumber dan bacaan lanjutan

- Microsoft Learn — Design Details: Costing Methods di Business Central
- Dokumentasi Odoo 18 — Konfigurasi Automatic Inventory Valuation

> **Key takeaway:** Modul inventory gagal dengan cara yang sama: jumlah stok disimpan sebagai satu angka yang bisa diubah di tabel item, ditulis dari beberapa jalur kode, sampai angka di layar tidak lagi cocok dengan gudang. Menurunkan jumlahnya dari ledger pergerakan yang hanya bisa ditambah menghapus jenis bug itu sepenuhnya.

## Source links

- https://learn.microsoft.com/en-us/dynamics365/business-central/design-details-costing-methods
- https://www.odoo.com/documentation/18.0/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/inventory_valuation_config.html

## FAQ

### Mengapa stok on-hand disimpan sebagai ledger movement immutable, bukan diperbarui sebagai satu angka?

Counter mutable yang diperbarui dari banyak jalur kode akan melenceng dari kenyataan dalam hitungan bulan karena penulisan bersamaan dan bug tidak meninggalkan jejak. Dengan menjadikan setiap penerimaan, pengeluaran, transfer, dan penyesuaian sebagai baris immutable di tabel stock_movement, on-hand selalu diturunkan lewat SUM dan setiap selisih bisa dilacak ke dokumen bisnis spesifik dalam hitungan menit. Koreksi dilakukan lewat movement kompensasi baru, bukan pengeditan, sehingga audit trail terbentuk otomatis.

### Bagaimana memilih antara valuasi FIFO dan average cost (AVCO) untuk modul inventory ERP?

Pilihan metode mengubah COGS, margin, dan posisi pajak, sehingga harus diputuskan bersama akuntan klien. Untuk mayoritas klien SMB Indonesia, default-nya adalah average cost karena harga beli dalam IDR cukup fluktuatif sehingga pelacakan layer FIFO menambah kompleksitas nyata, sementara AVCO memberi akuntan satu angka yang bisa dipertanggungjawabkan per item. FIFO baru diimplementasikan kalau bisnisnya memang merotasi stok seperti itu dan tim finance yang memintanya.

### Mengapa logika 'check-then-insert' di level aplikasi tidak cukup untuk mencegah stok negatif?

Di antara SELECT yang membaca saldo terkini dan INSERT yang menambahkan movement pengeluaran, transaksi lain bisa commit pengeluarannya sendiri — sehingga kedua penulisan berhasil dan saldo menjadi negatif. Database harus menserialisasi penulis pada pasangan item-gudang yang sama lewat row-level locking (SELECT FOR UPDATE), isolation level serializable, atau exclusion constraint. Load test secara bersamaan, bukan code review, yang membuktikan guard ini benar-benar tahan.

### Apa empat kuantitas stok yang harus dilacak modul inventory, dan mengapa penting?

Keempat kuantitas itu adalah on-hand (stok fisik sebagai SUM ledger movement), reserved (dialokasikan ke order terkonfirmasi tapi belum dikeluarkan), available (on-hand dikurangi reserved), dan incoming (baris purchase order terkonfirmasi yang belum diterima). Menampilkan hanya on-hand membuat tim sales over-promise dan logika reorder menduplikasi stok yang masih dalam perjalanan. Reservasi harus berada di tabel tersendiri di luar ledger movement karena reservasi adalah niat, bukan kejadian fisik — mencampurnya merusak valuasi.

### Apa fungsi snapshot stok periodik, dan kapan sebaiknya ditambahkan?

Menjumlahkan seluruh ledger movement menjadi lambat saat jumlah baris sangat besar, sehingga baris snapshot periodik memungkinkan sistem hanya menjumlahkan movement setelah snapshot terakhir. Artikel ini merekomendasikan menambahkan tabel stock_snapshot bulanan sejak hari pertama, bahkan sebelum performa menuntutnya, karena closing akhir bulan di accounting toh sudah butuh saldo historis yang dibekukan — memasang snapshot ke ledger yang sudah live di minggu closing adalah penderitaan yang bisa dihindari.

---

- HTML version: https://www.matthewswong.com/id/blog/erp-inventory-management-module-design/
- English: https://www.matthewswong.com/en/blog/erp-inventory-management-module-design/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
