Prinsip kerja gudang ini sederhana namun ketat: tabel stok_lokasi tidak pernah diubah secara langsung tanpa alasan. Setiap kenaikan dan penurunan stok harus lahir dari sebuah mutasi — dokumen sumber yang mencatat siapa, kapan, barang apa, jumlah berapa, dan di lokasi mana. Dengan disiplin itu, pertanyaan “kok stok Kertas tinggal 30?” selalu punya jawaban yang bisa ditelusuri sampai ke surat jalan. Pada bagian ini kita menghidupkan mekanisme tersebut untuk dua arus pertama: barang masuk dari supplier dan barang keluar menuju toko.
Menyiapkan Fondasi Bagian #
Setiap bagian proyek ini dirancang dapat dijalankan sendiri, jadi berkas siapkan.sql memasang ulang skema dan data master dari Bagian 1 ke database yang masih kosong. Pemeriksaan singkat di akhir berkas memastikan sembilan barang dan lima lokasi sudah siap, sementara stok_lokasi memang masih kosong: gudang belum menerima kiriman apa pun.
-- siapkan.sql — fondasi Bagian 2: skema dari Bagian 1 + data master.
-- Bagian ini dimaksudkan dapat dijalankan sendiri, sehingga seluruh
-- struktur dan data master dipasang ulang dari nol.
CREATE TABLE barang (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori text NOT NULL,
satuan text NOT NULL,
stok_minimum integer NOT NULL CHECK (stok_minimum >= 0),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0)
);
CREATE TABLE lokasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
gudang text NOT NULL,
rak text NOT NULL,
UNIQUE (gudang, rak)
);
CREATE TABLE stok_lokasi (
barang_id integer NOT NULL REFERENCES barang (id) ON DELETE CASCADE,
lokasi_id integer NOT NULL REFERENCES lokasi (id),
qty integer NOT NULL CONSTRAINT stok_qty_nonneg CHECK (qty >= 0),
PRIMARY KEY (barang_id, lokasi_id)
);
CREATE TABLE mutasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
tipe text NOT NULL CHECK (tipe IN ('masuk', 'keluar', 'pindah')),
tanggal date NOT NULL,
petugas text NOT NULL,
keterangan text
);
CREATE TABLE mutasi_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mutasi_id integer NOT NULL REFERENCES mutasi (id) ON DELETE CASCADE,
barang_id integer NOT NULL REFERENCES barang (id),
qty integer NOT NULL CHECK (qty > 0),
lokasi_asal_id integer REFERENCES lokasi (id),
lokasi_tujuan_id integer REFERENCES lokasi (id),
CONSTRAINT arah_lokasi_terisi
CHECK (num_nonnulls(lokasi_asal_id, lokasi_tujuan_id) >= 1)
);
CREATE INDEX idx_mutasi_tanggal ON mutasi (tanggal);
CREATE INDEX idx_mutasi_item_barang ON mutasi_item (barang_id);
CREATE INDEX idx_mutasi_item_mutasi ON mutasi_item (mutasi_id);
INSERT INTO barang (kode, nama, kategori, satuan, stok_minimum, harga_beli) VALUES
('BRG-001', 'Kertas HVS A4 80gsm', 'Alat Tulis', 'box', 10, 185000.00),
('BRG-002', 'Pulpen Gel Hitam', 'Alat Tulis', 'box', 20, 42000.00),
('BRG-003', 'Buku Tulis 58 halaman', 'Alat Tulis', 'lusin', 30, 38500.00),
('BRG-004', 'Tinta Printer Hitam', 'Alat Tulis', 'pcs', 8, 95000.00),
('BRG-005', 'Sabun Cuci Cair 1L', 'Rumah Tangga','dus', 12, 87000.00),
('BRG-006', 'Deterjen Bubuk 800g', 'Rumah Tangga','dus', 15, 72000.00),
('BRG-007', 'Minyak Goreng 2L', 'Pangan', 'dus', 25, 115000.00),
('BRG-008', 'Beras Premium 5kg', 'Pangan', 'karung', 10, 320000.00),
('BRG-009', 'Kopi Sachet Instan', 'Pangan', 'dus', 40, 96000.00);
INSERT INTO lokasi (kode, gudang, rak) VALUES
('G1-A-01', 'Gudang Utama', 'Rak A-01'),
('G1-B-02', 'Gudang Utama', 'Rak B-02'),
('G1-C-03', 'Gudang Utama', 'Rak C-03'),
('G2-A-01', 'Gudang Cadangan', 'Rak A-01'),
('G2-B-02', 'Gudang Cadangan', 'Rak B-02');
SELECT (SELECT count(*) FROM barang) AS jumlah_barang,
(SELECT count(*) FROM lokasi) AS jumlah_lokasi,
(SELECT count(*) FROM stok_lokasi) AS baris_stok;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE INDEX
CREATE INDEX
INSERT 0 9
INSERT 0 5
jumlah_barang | jumlah_lokasi | baris_stok
---------------+---------------+------------
9 | 5 | 0
(1 row)Mutasi Masuk: Barang Datang dari Supplier #
Dokumen MUT-0001 dan MUT-0002 adalah dua penerimaan barang pada awal Februari 2026. Ketiganya — kepala mutasi, baris detail, dan kenaikan stok — dibungkus satu BEGIN … COMMIT sehingga tidak ada kemungkinan mutasi tercatat tetapi stok tidak naik.
-- mutasi-masuk.sql — barang datang dari supplier: satu transaksi utuh.
-- Header mutasi, item mutasi, dan kenaikan stok_lokasi terjadi semua
-- atau tidak sama sekali.
BEGIN;
-- 1) catat header mutasi masuk
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0001', 'masuk', DATE '2026-02-02', 'Budi',
'Penerimaan PO-1001 dari supplier'),
('MUT-0002', 'masuk', DATE '2026-02-10', 'Budi',
'Penerimaan PO-1007 dari supplier');
-- 2) catat item per barang, lengkap dengan lokasi tujuan
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_tujuan_id)
SELECT m.id, b.id, v.qty, l.id
FROM (VALUES
('MUT-0001', 'BRG-001', 20, 'G1-A-01'),
('MUT-0001', 'BRG-005', 15, 'G1-B-02'),
('MUT-0001', 'BRG-007', 40, 'G1-A-01'),
('MUT-0001', 'BRG-008', 12, 'G1-C-03'),
('MUT-0002', 'BRG-001', 10, 'G1-A-01'),
('MUT-0002', 'BRG-002', 30, 'G1-A-01'),
('MUT-0002', 'BRG-003', 40, 'G1-B-02'),
('MUT-0002', 'BRG-006', 18, 'G1-B-02'),
('MUT-0002', 'BRG-009', 45, 'G2-A-01')
) AS v(kode_mutasi, kode_barang, qty, kode_lokasi)
JOIN mutasi m ON m.kode = v.kode_mutasi
JOIN barang b ON b.kode = v.kode_barang
JOIN lokasi l ON l.kode = v.kode_lokasi;
-- 3) naikkan stok di lokasi tujuan; baris baru dibuat bila belum ada.
-- SUM diperlukan karena dua mutasi dapat menambah barang yang sama
-- di lokasi yang sama (BRG-001 ke G1-A-01 di MUT-0001 dan MUT-0002),
-- dan ON CONFLICT DO UPDATE tidak boleh menyentuh satu baris dua
-- kali dalam perintah yang sama.
INSERT INTO stok_lokasi (barang_id, lokasi_id, qty)
SELECT mi.barang_id, mi.lokasi_tujuan_id, SUM(mi.qty)
FROM mutasi_item mi
WHERE mi.mutasi_id IN (SELECT id FROM mutasi
WHERE kode IN ('MUT-0001', 'MUT-0002'))
GROUP BY mi.barang_id, mi.lokasi_tujuan_id
ON CONFLICT (barang_id, lokasi_id) DO UPDATE
SET qty = stok_lokasi.qty + EXCLUDED.qty;
COMMIT;
-- 4) verifikasi hasil
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
ORDER BY b.kode, l.kode;Output yang Dihasilkan:
BEGIN
INSERT 0 2
INSERT 0 9
INSERT 0 8
COMMIT
kode | nama | lokasi | qty
---------+-----------------------+---------+-----
BRG-001 | Kertas HVS A4 80gsm | G1-A-01 | 30
BRG-002 | Pulpen Gel Hitam | G1-A-01 | 30
BRG-003 | Buku Tulis 58 halaman | G1-B-02 | 40
BRG-005 | Sabun Cuci Cair 1L | G1-B-02 | 15
BRG-006 | Deterjen Bubuk 800g | G1-B-02 | 18
BRG-007 | Minyak Goreng 2L | G1-A-01 | 40
BRG-008 | Beras Premium 5kg | G1-C-03 | 12
BRG-009 | Kopi Sachet Instan | G2-A-01 | 45
(8 rows)Empat bagian perintah di atas bekerja sebagai satu kesatuan:
- Langkah 1 — kepala mutasi: dua baris untuk
MUT-0001danMUT-0002dengan tipemasuk, tanggal, petugas, dan keterangan nomor PO. Kolomtipeakan ditolakCHECKbila isinya bukanmasuk,keluar, ataupindah. - Langkah 2 — baris detail: daftar
VALUESberisi sembilan pasangan barang–lokasi di-join ke tabel master untuk menerjemahkan kode teks menjadiid. Menyimpanid, bukan teks kode, membuat relasi tetap benar meski suatu saat nama lokasi dirapikan. - Langkah 3 — kenaikan stok:
INSERT ... ON CONFLICT DO UPDATEmenangani dua keadaan sekaligus: pasangan barang–lokasi yang belum ada dibuat barisnya, yang sudah ada ditambah qty-nya.SUM(...)denganGROUP BYbukan hiasan — tanpa agregasi ini, perintah akan mengenai baris yang sama dua kali dan PostgreSQL menolaknya dengan pesan ON CONFLICT DO UPDATE command cannot affect row a second time, karenaBRG-001memang dikirim keG1-A-01oleh dua mutasi yang berbeda. - Langkah 4 — verifikasi: hasil akhir delapan baris stok, dengan
BRG-001diG1-A-01berjumlah 30 — gabungan 20 dariMUT-0001dan 10 dariMUT-0002.
Mutasi Keluar: Barang Dikirim ke Toko #
Seminggu kemudian gudang mengirim barang ke Toko Pusat lewat MUT-0003. Polanya mirip, tetapi arah stoknya terbalik: detail menulis lokasi_asal_id (tujuan dibiarkan NULL), dan UPDATE ... FROM menurunkan stok di lokasi asal.
-- mutasi-keluar.sql — barang keluar dari lokasi penyimpanan.
-- UPDATE pengurang dibiarkan "polos": CHECK qty >= 0 pada stok_lokasi
-- yang menjadi jaring pengaman bila stok tidak mencukupi.
BEGIN;
-- 1) header mutasi keluar
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0003', 'keluar', DATE '2026-02-15', 'Siti',
'Pengiriman ke Toko Pusat');
-- 2) item keluar: lokasi asal wajib, tujuan kosong
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_asal_id)
SELECT m.id, b.id, v.qty, l.id
FROM (VALUES
('MUT-0003', 'BRG-007', 12, 'G1-A-01'),
('MUT-0003', 'BRG-008', 5, 'G1-C-03')
) AS v(kode_mutasi, kode_barang, qty, kode_lokasi)
JOIN mutasi m ON m.kode = v.kode_mutasi
JOIN barang b ON b.kode = v.kode_barang
JOIN lokasi l ON l.kode = v.kode_lokasi;
-- 3) kurangi stok di lokasi asal
UPDATE stok_lokasi sl
SET qty = sl.qty - mi.qty
FROM mutasi_item mi
WHERE mi.mutasi_id = (SELECT id FROM mutasi WHERE kode = 'MUT-0003')
AND mi.barang_id = sl.barang_id
AND mi.lokasi_asal_id = sl.lokasi_id;
COMMIT;
-- 4) verifikasi: stok BRG-007 dan BRG-008 kini berkurang
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode IN ('BRG-007', 'BRG-008')
ORDER BY b.kode, l.kode;Output yang Dihasilkan:
BEGIN
INSERT 0 1
INSERT 0 2
UPDATE 2
COMMIT
kode | nama | lokasi | qty
---------+-------------------+---------+-----
BRG-007 | Minyak Goreng 2L | G1-A-01 | 28
BRG-008 | Beras Premium 5kg | G1-C-03 | 7
(2 rows)Perhatikan bahwa UPDATE pengurangnya sama sekali tidak memeriksa apakah stok cukup — tidak ada WHERE qty >= permintaan. Pemeriksaan itu dipercayakan sepenuhnya pada CHECK (qty >= 0) di tabel stok_lokasi. Konsekuensinya besar: klaim pengurangan yang melebihi stok digagalkan oleh constraint, dan karena seluruh perintah berada dalam satu transaksi, seluruh mutasi itu ikut dibatalkan.
Ketika Stok Tidak Mencukupi #
Mari kita buktikan klaim itu dengan sengaja melakukan kesalahan. MUT-0004 ingin mengirim 30 karung Beras dari G1-C-03, padahal di sana hanya tersisa 7 karung:
-- keluar-gagal.sql — pengeluaran yang melebihi stok tersedia.
-- Beras di G1-C-03 tinggal 7 karung, tetapi MUT-0004 meminta 30.
-- CHECK stok_qty_nonneg menolak UPDATE, transaksi batal seluruhnya.
BEGIN;
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0004', 'keluar', DATE '2026-02-18', 'Siti',
'Pengiriman ke Toko Cabang');
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_asal_id)
SELECT m.id, b.id, 30, l.id
FROM mutasi m
JOIN barang b ON b.kode = 'BRG-008'
JOIN lokasi l ON l.kode = 'G1-C-03'
WHERE m.kode = 'MUT-0004';
UPDATE stok_lokasi sl
SET qty = sl.qty - mi.qty
FROM mutasi_item mi
WHERE mi.mutasi_id = (SELECT id FROM mutasi WHERE kode = 'MUT-0004')
AND mi.barang_id = sl.barang_id
AND mi.lokasi_asal_id = sl.lokasi_id;
COMMIT;Output yang Dihasilkan:
psql:keluar-gagal.sql:23: ERROR: new row for relation "stok_lokasi" violates check constraint "stok_qty_nonneg"
DETAIL: Failing row contains (8, 3, -23).PostgreSQL membatalkan UPDATE dengan check_violation pada constraint stok_qty_nonneg — baris pengganti berisi (8, 3, -23): barang 8, lokasi 3, qty -23. Karena ON_ERROR_STOP membuat psql berhenti di titik itu, COMMIT tidak pernah sampai dieksekusi dan server mengembalikan seluruh transaksi. Berkas berikut membuktikan bahwa tidak ada jejak yang tertinggal:
-- cek-pulih.sql — bukti bahwa transaksi yang gagal tidak meninggalkan jejak:
-- stok Beras tidak berubah dan header MUT-0004 tidak pernah tersimpan.
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode = 'BRG-008'
ORDER BY l.kode;
SELECT count(*) AS jumlah_mutasi_tersimpan
FROM mutasi;Output yang Dihasilkan:
kode | nama | lokasi | qty
---------+-------------------+---------+-----
BRG-008 | Beras Premium 5kg | G1-C-03 | 7
(1 row)
jumlah_mutasi_tersimpan
-------------------------
3
(1 row)Beras masih 7 karung di G1-C-03 — angka yang sama seperti sebelum percobaan gagal — dan jumlah mutasi tersimpan tetap 3. Kepala MUT-0004 yang sempat di-INSERT di awal transaksi ikut lenyap; inilah atomisitas yang dijanjikan transaksi database: semua perubahan dari satu dokumen terjadi bersama-sama, atau tidak sama sekali.
Arus masuk dan keluar kini berjalan dengan disiplin yang teruji, termasuk kegagalannya. Satu jenis pergerakan masih tersisa dan justru yang paling berisiko membuat barang “hilang di antara dua rak”: perpindahan antarlokasi, yang menyentuh dua baris stok sekaligus. Bagian Transaksi Barang membuktikan bahwa perpindahan seperti itu tetap utuh atau batal seluruhnya.
Referensi #
- BEGIN — membuka blok transaksi yang akan dikomit atau dibatalkan sebagai satu kesatuan.
- INSERT
— sintaks
ON CONFLICT DO UPDATEdan aturan satu-baris-sekali-sentuh. - UPDATE
— pola
UPDATE ... FROMuntuk pengurangan stok berbasis detail mutasi. - Tutorial: Transactions — pengantar konsep atomisitas transaksi di PostgreSQL.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
cek-pulih.sql
Unduh berkas-- cek-pulih.sql — bukti bahwa transaksi yang gagal tidak meninggalkan jejak:
-- stok Beras tidak berubah dan header MUT-0004 tidak pernah tersimpan.
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode = 'BRG-008'
ORDER BY l.kode;
SELECT count(*) AS jumlah_mutasi_tersimpan
FROM mutasi;
keluar-gagal.sql
Unduh berkas-- keluar-gagal.sql — pengeluaran yang melebihi stok tersedia.
-- Beras di G1-C-03 tinggal 7 karung, tetapi MUT-0004 meminta 30.
-- CHECK stok_qty_nonneg menolak UPDATE, transaksi batal seluruhnya.
BEGIN;
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0004', 'keluar', DATE '2026-02-18', 'Siti',
'Pengiriman ke Toko Cabang');
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_asal_id)
SELECT m.id, b.id, 30, l.id
FROM mutasi m
JOIN barang b ON b.kode = 'BRG-008'
JOIN lokasi l ON l.kode = 'G1-C-03'
WHERE m.kode = 'MUT-0004';
UPDATE stok_lokasi sl
SET qty = sl.qty - mi.qty
FROM mutasi_item mi
WHERE mi.mutasi_id = (SELECT id FROM mutasi WHERE kode = 'MUT-0004')
AND mi.barang_id = sl.barang_id
AND mi.lokasi_asal_id = sl.lokasi_id;
COMMIT;
mutasi-keluar.sql
Unduh berkas-- mutasi-keluar.sql — barang keluar dari lokasi penyimpanan.
-- UPDATE pengurang dibiarkan "polos": CHECK qty >= 0 pada stok_lokasi
-- yang menjadi jaring pengaman bila stok tidak mencukupi.
BEGIN;
-- 1) header mutasi keluar
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0003', 'keluar', DATE '2026-02-15', 'Siti',
'Pengiriman ke Toko Pusat');
-- 2) item keluar: lokasi asal wajib, tujuan kosong
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_asal_id)
SELECT m.id, b.id, v.qty, l.id
FROM (VALUES
('MUT-0003', 'BRG-007', 12, 'G1-A-01'),
('MUT-0003', 'BRG-008', 5, 'G1-C-03')
) AS v(kode_mutasi, kode_barang, qty, kode_lokasi)
JOIN mutasi m ON m.kode = v.kode_mutasi
JOIN barang b ON b.kode = v.kode_barang
JOIN lokasi l ON l.kode = v.kode_lokasi;
-- 3) kurangi stok di lokasi asal
UPDATE stok_lokasi sl
SET qty = sl.qty - mi.qty
FROM mutasi_item mi
WHERE mi.mutasi_id = (SELECT id FROM mutasi WHERE kode = 'MUT-0003')
AND mi.barang_id = sl.barang_id
AND mi.lokasi_asal_id = sl.lokasi_id;
COMMIT;
-- 4) verifikasi: stok BRG-007 dan BRG-008 kini berkurang
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
WHERE b.kode IN ('BRG-007', 'BRG-008')
ORDER BY b.kode, l.kode;
mutasi-masuk.sql
Unduh berkas-- mutasi-masuk.sql — barang datang dari supplier: satu transaksi utuh.
-- Header mutasi, item mutasi, dan kenaikan stok_lokasi terjadi semua
-- atau tidak sama sekali.
BEGIN;
-- 1) catat header mutasi masuk
INSERT INTO mutasi (kode, tipe, tanggal, petugas, keterangan) VALUES
('MUT-0001', 'masuk', DATE '2026-02-02', 'Budi',
'Penerimaan PO-1001 dari supplier'),
('MUT-0002', 'masuk', DATE '2026-02-10', 'Budi',
'Penerimaan PO-1007 dari supplier');
-- 2) catat item per barang, lengkap dengan lokasi tujuan
INSERT INTO mutasi_item (mutasi_id, barang_id, qty, lokasi_tujuan_id)
SELECT m.id, b.id, v.qty, l.id
FROM (VALUES
('MUT-0001', 'BRG-001', 20, 'G1-A-01'),
('MUT-0001', 'BRG-005', 15, 'G1-B-02'),
('MUT-0001', 'BRG-007', 40, 'G1-A-01'),
('MUT-0001', 'BRG-008', 12, 'G1-C-03'),
('MUT-0002', 'BRG-001', 10, 'G1-A-01'),
('MUT-0002', 'BRG-002', 30, 'G1-A-01'),
('MUT-0002', 'BRG-003', 40, 'G1-B-02'),
('MUT-0002', 'BRG-006', 18, 'G1-B-02'),
('MUT-0002', 'BRG-009', 45, 'G2-A-01')
) AS v(kode_mutasi, kode_barang, qty, kode_lokasi)
JOIN mutasi m ON m.kode = v.kode_mutasi
JOIN barang b ON b.kode = v.kode_barang
JOIN lokasi l ON l.kode = v.kode_lokasi;
-- 3) naikkan stok di lokasi tujuan; baris baru dibuat bila belum ada.
-- SUM diperlukan karena dua mutasi dapat menambah barang yang sama
-- di lokasi yang sama (BRG-001 ke G1-A-01 di MUT-0001 dan MUT-0002),
-- dan ON CONFLICT DO UPDATE tidak boleh menyentuh satu baris dua
-- kali dalam perintah yang sama.
INSERT INTO stok_lokasi (barang_id, lokasi_id, qty)
SELECT mi.barang_id, mi.lokasi_tujuan_id, SUM(mi.qty)
FROM mutasi_item mi
WHERE mi.mutasi_id IN (SELECT id FROM mutasi
WHERE kode IN ('MUT-0001', 'MUT-0002'))
GROUP BY mi.barang_id, mi.lokasi_tujuan_id
ON CONFLICT (barang_id, lokasi_id) DO UPDATE
SET qty = stok_lokasi.qty + EXCLUDED.qty;
COMMIT;
-- 4) verifikasi hasil
SELECT b.kode, b.nama, l.kode AS lokasi, sl.qty
FROM stok_lokasi sl
JOIN barang b ON b.id = sl.barang_id
JOIN lokasi l ON l.id = sl.lokasi_id
ORDER BY b.kode, l.kode;
siapkan.sql
Unduh berkas-- siapkan.sql — fondasi Bagian 2: skema dari Bagian 1 + data master.
-- Bagian ini dimaksudkan dapat dijalankan sendiri, sehingga seluruh
-- struktur dan data master dipasang ulang dari nol.
CREATE TABLE barang (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori text NOT NULL,
satuan text NOT NULL,
stok_minimum integer NOT NULL CHECK (stok_minimum >= 0),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0)
);
CREATE TABLE lokasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
gudang text NOT NULL,
rak text NOT NULL,
UNIQUE (gudang, rak)
);
CREATE TABLE stok_lokasi (
barang_id integer NOT NULL REFERENCES barang (id) ON DELETE CASCADE,
lokasi_id integer NOT NULL REFERENCES lokasi (id),
qty integer NOT NULL CONSTRAINT stok_qty_nonneg CHECK (qty >= 0),
PRIMARY KEY (barang_id, lokasi_id)
);
CREATE TABLE mutasi (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
tipe text NOT NULL CHECK (tipe IN ('masuk', 'keluar', 'pindah')),
tanggal date NOT NULL,
petugas text NOT NULL,
keterangan text
);
CREATE TABLE mutasi_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mutasi_id integer NOT NULL REFERENCES mutasi (id) ON DELETE CASCADE,
barang_id integer NOT NULL REFERENCES barang (id),
qty integer NOT NULL CHECK (qty > 0),
lokasi_asal_id integer REFERENCES lokasi (id),
lokasi_tujuan_id integer REFERENCES lokasi (id),
CONSTRAINT arah_lokasi_terisi
CHECK (num_nonnulls(lokasi_asal_id, lokasi_tujuan_id) >= 1)
);
CREATE INDEX idx_mutasi_tanggal ON mutasi (tanggal);
CREATE INDEX idx_mutasi_item_barang ON mutasi_item (barang_id);
CREATE INDEX idx_mutasi_item_mutasi ON mutasi_item (mutasi_id);
INSERT INTO barang (kode, nama, kategori, satuan, stok_minimum, harga_beli) VALUES
('BRG-001', 'Kertas HVS A4 80gsm', 'Alat Tulis', 'box', 10, 185000.00),
('BRG-002', 'Pulpen Gel Hitam', 'Alat Tulis', 'box', 20, 42000.00),
('BRG-003', 'Buku Tulis 58 halaman', 'Alat Tulis', 'lusin', 30, 38500.00),
('BRG-004', 'Tinta Printer Hitam', 'Alat Tulis', 'pcs', 8, 95000.00),
('BRG-005', 'Sabun Cuci Cair 1L', 'Rumah Tangga','dus', 12, 87000.00),
('BRG-006', 'Deterjen Bubuk 800g', 'Rumah Tangga','dus', 15, 72000.00),
('BRG-007', 'Minyak Goreng 2L', 'Pangan', 'dus', 25, 115000.00),
('BRG-008', 'Beras Premium 5kg', 'Pangan', 'karung', 10, 320000.00),
('BRG-009', 'Kopi Sachet Instan', 'Pangan', 'dus', 40, 96000.00);
INSERT INTO lokasi (kode, gudang, rak) VALUES
('G1-A-01', 'Gudang Utama', 'Rak A-01'),
('G1-B-02', 'Gudang Utama', 'Rak B-02'),
('G1-C-03', 'Gudang Utama', 'Rak C-03'),
('G2-A-01', 'Gudang Cadangan', 'Rak A-01'),
('G2-B-02', 'Gudang Cadangan', 'Rak B-02');
SELECT (SELECT count(*) FROM barang) AS jumlah_barang,
(SELECT count(*) FROM lokasi) AS jumlah_lokasi,
(SELECT count(*) FROM stok_lokasi) AS baris_stok;