Melacak inventaris sebagai aliran event berarti stok saat ini selalu dihitung sebagai jumlah semua pergerakan, sehingga audit trail tersedia secara otomatis dan rekonstruksi historis menjadi mudah. Pendekatan ini juga menghilangkan masalah 'ghost stock', yaitu saat kuantitas menunjukkan angka tertentu tetapi tidak ada yang bisa menjelaskan asal-usulnya. Semua perubahan — penerimaan, pengeluaran, penyesuaian, dan pengembalian — harus melalui movement engine, sehingga setiap selisih bisa ditelusuri ke tindakan pengguna dan timestamp tertentu.
Setiap transfer membuat dua baris pergerakan secara atomik: Goods Issue (GI) dari bin sumber dan Goods Receipt (GR) di bin tujuan, keduanya di-commit dalam satu transaksi PostgreSQL yang sama. Jika salah satu gagal dimasukkan, tidak ada satu pun yang dicatat. Ini mencegah skenario 'in-transit limbo' di mana stok menghilang di tengah transfer dan tidak bisa dipertanggungjawabkan di lokasi mana pun.
Materialized view yang di-refresh setiap 5 menit memberikan performa query dashboard yang cepat, tetapi menciptakan jendela ketidaktepatan — saat sesi penerimaan volume tinggi, kuantitas yang ditampilkan bisa tertinggal hingga 5 menit. Untuk mengatasinya, endpoint 'live balance' terpisah melakukan query agregasi real-time untuk item spesifik yang sedang dilihat, sedangkan materialized view hanya digunakan untuk dashboard ringkasan. Menampilkan total yang basi di layar picking sengaja dihindari karena staf gudang yang bertindak berdasarkan data basi bisa menyebabkan masalah operasional nyata.
Job cron NestJS berjalan setiap malam, memeriksa setiap item yang stoknya telah turun di bawah reorder_point yang dikonfigurasi, lalu membuat draft Purchase Request yang sudah terisi vendor pilihan dan kuantitas reorder. Sistem sengaja tidak langsung menerbitkan Purchase Order otomatis agar manajer gudang dapat meninjau dan menyetujui setiap pagi, menjaga keterlibatan manusia dalam proses. Pendekatan ini menghemat sekitar 2 jam per hari bagi tim gudang klien dari pengecekan stok manual.
Sistem menggunakan workflow 'freeze and count': lokasi gudang dikunci untuk mencegah pergerakan apa pun selama penghitungan, kuantitas fisik direkam item per item, varians terhadap kuantitas sistem dihitung, dan pergerakan ADJ dibuat untuk setiap varians sebelum lokasi dibuka kembali. Laporan varians disimpan sebagai dokumen untuk ditinjau auditor, yang sangat penting bagi bisnis di Indonesia dengan persyaratan audit PKWT. Proses yang sebelumnya memakan waktu seminggu dengan spreadsheet kini selesai dalam satu hari.
Perusahaan yang mengimplementasikan pelacakan inventaris real-time melalui sistem ERP melaporkan peningkatan akurasi inventaris sebesar 35% dan peningkatan efisiensi operasional sebesar 40%. Namun hanya 23% UKM yang telah berinvestasi pada alat inventaris real-time. Di Indonesia, di mana rantai pasokan kompleks dan operasi gudang sering mencakup beberapa lokasi, visibilitas real-time adalah perbedaan antara keputusan pengadaan yang percaya diri dan kehabisan stok yang mahal. Dalam pekerjaan konsultasi ERP freelance saya, saya membangun modul inventaris yang melacak setiap pergerakan stok sebagai event yang tidak dapat diubah dan menampilkan data langsung ke staf gudang dan dashboard manajemen.
Keputusan desain inti: lacak inventaris sebagai aliran event, bukan kuantitas yang dapat diubah. Setiap penerimaan, pengeluaran, transfer, penyesuaian, atau pengembalian membuat baris stock_movements yang tidak dapat diubah. Kuantitas saat ini untuk item apa pun di lokasi mana pun selalu dihitung sebagai jumlah pergerakan. Ini memberikan audit trail bawaan, membuat rekonstruksi historis menjadi mudah, dan menghilangkan masalah 'ghost stock'.
Saya mendukung enam jenis pergerakan: GR (Penerimaan Barang dari PO), GI (Pengeluaran Barang ke produksi atau pelanggan), TR (Transfer antar lokasi), ADJ+ (Penyesuaian Positif), ADJ- (Penyesuaian Negatif), dan RTN (Pengembalian dari pelanggan atau ke vendor). Setiap pergerakan mencatat: item_id, location_id, quantity, movement_type, reference_id, unit_cost, dan created_by.
Inventory Event Ledger Architecture (NestJS + PostgreSQL)
┌─────────────────────────────────────────────────────┐
│ TRANSACTION SOURCES │
│ Purchase Receipt │ Sales Issue │ Transfer │ Adjust │
└──────────────────────┬──────────────────────────────┘
│ create_movement()
▼
┌─────────────────────────────────────────────────────┐
│ stock_movements (append-only ledger) │
│──────────────────────────────────────────────────── │
│ id UUID PRIMARY KEY │
│ item_id UUID REFERENCES items(id) │
│ location_id UUID REFERENCES locations(id) │
│ quantity NUMERIC (+ve always) │
│ movement_type VARCHAR GR|GI|TR|ADJ+|ADJ-|RTN │
│ reference_id UUID (PO, SO, or transfer_order) │
│ unit_cost NUMERIC │
│ created_by UUID REFERENCES users(id) │
│ created_at TIMESTAMPTZ DEFAULT NOW() │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌──────────────────┐ ┌─────────────────────────┐
│ Materialized View│ │ WebSocket NOTIFY │
│ current_stock │ │ → React UI updates live │
│ (refresh 5min) │ │ → No polling needed │
└──────────────────┘ └─────────────────────────┘Dari pengalaman saya sebagai konsultan ERP freelance: jangan pernah izinkan pengeditan langsung kuantitas inventaris melalui UI. Semua perubahan stok harus melalui movement engine, bahkan penyesuaian manual. Ini terlihat membatasi sampai audit pertama — lalu klien bersyukur dapat menelusuri setiap perubahan kuantitas ke tindakan pengguna dan timestamp.
Skema saya mendukung model lokasi hierarkis: Gudang → Zona → Lorong → Bin. Item dapat berada di beberapa bin secara bersamaan. Transfer order memindahkan kuantitas dari satu bin ke bin lain, membuat dua baris pergerakan (GI dari sumber dan GR di tujuan). Transfer bersifat atomik — kedua baris di-commit dalam transaksi PostgreSQL yang sama.
Setiap item memiliki reorder_point dan reorder_qty yang dapat dikonfigurasi. Job cron NestJS berjalan setiap malam dan memeriksa semua item di mana stok saat ini telah turun di bawah titik reorder. Untuk setiap kecocokan, sistem membuat draft Purchase Request di modul pengadaan, sudah diisi dengan vendor pilihan dan kuantitas reorder. Manajer gudang meninjau dan menyetujui PR draft ini setiap pagi.
-- PostgreSQL: Validated stock movement function
CREATE OR REPLACE FUNCTION create_movement(
p_item_id UUID,
p_location_id UUID,
p_quantity NUMERIC,
p_type VARCHAR,
p_reference_id UUID,
p_unit_cost NUMERIC,
p_user_id UUID
) RETURNS UUID AS $$
DECLARE
v_current_stock NUMERIC;
v_movement_id UUID;
BEGIN
-- Validate sufficient stock for outbound movements
IF p_type IN ('GI', 'TR') THEN
SELECT COALESCE(SUM(
CASE WHEN movement_type IN ('GR','ADJ+','RTN') THEN quantity
ELSE -quantity END
), 0)
INTO v_current_stock
FROM stock_movements
WHERE item_id = p_item_id AND location_id = p_location_id;
IF v_current_stock < p_quantity THEN
RAISE EXCEPTION 'Insufficient stock: % available, % requested',
v_current_stock, p_quantity;
END IF;
END IF;
INSERT INTO stock_movements
(item_id, location_id, quantity, movement_type,
reference_id, unit_cost, created_by)
VALUES
(p_item_id, p_location_id, p_quantity, p_type,
p_reference_id, p_unit_cost, p_user_id)
RETURNING id INTO v_movement_id;
-- Notify connected WebSocket clients
PERFORM pg_notify('inventory_update',
json_build_object('item_id', p_item_id,
'location_id', p_location_id)::text);
RETURN v_movement_id;
END;
$$ LANGUAGE plpgsql;Modul inventaris berjalan di NestJS dengan TypeORM. Saya menggunakan fungsi PostgreSQL create_movement() yang memvalidasi pergerakan, memasukkan baris, dan memicu NOTIFY untuk me-refresh klien yang terhubung via WebSocket. UI gudang React berlangganan WebSocket ini dan memperbarui kuantitas yang ditampilkan secara real-time — tidak perlu polling.
Jika Anda menggunakan materialized view untuk total inventaris, Anda harus menangani jendela ketidaktepatan dengan hati-hati. Selama sesi penerimaan volume tinggi, kuantitas langsung dalam materialized view mungkin tertinggal hingga 5 menit. Saya menambahkan endpoint 'live balance' yang melakukan query agregasi real-time untuk item spesifik yang sedang dilihat. Jangan pernah tampilkan total yang basi di layar picking — staf gudang yang bertindak berdasarkan data basi menyebabkan masalah operasional nyata.
Manajemen mendapatkan dashboard yang menampilkan: total nilai inventaris berdasarkan kategori, item yang bergerak lambat (tidak ada pergerakan dalam 90 hari), item di bawah titik reorder, item dengan stok negatif (tanda masalah integritas data), dan perputaran inventaris bulanan berdasarkan kategori.
Penghitungan fisik tahunan atau kuartalan adalah kenyataan di gudang Indonesia, terutama untuk bisnis dengan persyaratan audit PKWT. Sistem kami mendukung workflow 'freeze and count': kunci lokasi (tidak ada pergerakan yang diizinkan selama penghitungan), rekam kuantitas penghitungan fisik item per item, hitung varians terhadap kuantitas sistem, buat pergerakan ADJ untuk setiap varians, dan buka kunci lokasi.