Membuat Aplikasi Inventaris Gudang dengan Java dan SQLite: Dokumen Mutasi, Kartu Stok, Opname, dan Barcode Code 128
Membuat Aplikasi Inventaris Gudang dengan Java dan SQLite: Dokumen Mutasi, Kartu Stok, Opname, dan Barcode Code 128
Banyak aplikasi stok sederhana menyimpan stok sebagai satu angka di tabel barang lalu mengubahnya langsung lewat form edit. Hasilnya cepat dibuat, tetapi begitu angka di sistem tidak sama dengan isi rak, tidak ada yang bisa menjelaskan kenapa. Gudang sungguhan butuh jejak: setiap unit yang masuk dan keluar harus punya dokumen. Artikel ini membahas lima teknik inti dari aplikasi Stok Gudang berbasis Java Swing dan SQLite.
1. Stok hanya berubah lewat dokumen, dan setiap perubahan dicatat
Desain tabelnya dibagi tiga lapis: dokumen (kepala: nomor, jenis, tanggal, supplier/tujuan), dokumen_item (baris barang), dan mutasi (buku besar per barang). Kolom barang.stok tetap ada supaya tampilan cepat, tetapi tidak pernah diubah dari form master.
CREATE TABLE mutasi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barang_id INTEGER NOT NULL REFERENCES barang(id),
dokumen_id INTEGER REFERENCES dokumen(id),
tanggal TEXT NOT NULL,
jenis TEXT NOT NULL CHECK (jenis IN ('AWAL','MASUK','KELUAR','KOREKSI','BATAL')),
qty INTEGER NOT NULL, -- positif = masuk, negatif = keluar
keterangan TEXT
);
Dengan aturan ini berlaku invarian sederhana yang bisa diuji kapan saja: stok setiap barang sama dengan jumlah seluruh qty mutasinya. Di tes, invarian ini diperiksa setelah ratusan dokumen data contoh dibuat:
SELECT COUNT(*) FROM barang b
WHERE b.stok <> (SELECT COALESCE(SUM(qty),0) FROM mutasi m WHERE m.barang_id = b.id)
Hasilnya harus nol. Kalau suatu hari tidak nol, berarti ada kode yang mengubah stok di luar jalur dokumen.
2. Dokumen multi-baris: semua atau tidak sama sekali
Surat jalan biasanya berisi beberapa barang. Bila baris ketiga ternyata kekurangan stok, baris pertama dan kedua tidak boleh terlanjur berkurang. Kuncinya: satu dokumen = satu transaksi database, dan pengurangan stok memakai UPDATE bersyarat.
case Dokumen.KELUAR -> {
if (Database.ubah(c, "UPDATE barang SET stok = stok - ? WHERE id=? AND stok >= ?", qty, bid, qty) == 0) {
throw new Gagal("Stok " + b[0] + " tidak cukup (tersedia " + b[1] + " " + b[4] + ").");
}
Database.ubah(c, "INSERT INTO dokumen_item(dokumen_id,barang_id,qty,harga) VALUES(?,?,?,?)", id, bid, qty, b[2]);
mutasi(c, bid, id, tgl, "KELUAR", -qty, nomor);
}
Exception Gagal ditangkap oleh helper transaksi, lalu ROLLBACK membatalkan kepala dokumen, baris sebelumnya, dan mutasinya sekaligus. Transaksi dibuka dengan BEGIN IMMEDIATE agar kunci tulis SQLite diambil sejak awal; dua komputer yang memakai file database yang sama akan antre, bukan saling timpa. Nomor dokumen BK-yyMMdd-001 juga dibuat di dalam transaksi ini sehingga tidak bisa dobel.
Pembatalan memakai pola yang sama. Membatalkan barang keluar cukup menambah stok kembali, tetapi membatalkan barang masuk harus mengurangi stok, dan itu bisa gagal bila barangnya sudah terlanjur dikirim. UPDATE ... WHERE stok >= qty menjaga kasus ini tanpa kode tambahan.
3. Kartu stok dengan saldo berjalan
Kartu stok menampilkan saldo awal periode, setiap mutasi, dan saldo setelah tiap baris. Saldo awal cukup dijumlahkan dari mutasi sebelum tanggal awal; sisanya dihitung berurutan di Java.
long awal = db.angka("SELECT COALESCE(SUM(m.qty),0) FROM mutasi m WHERE m.barang_id=? AND m.tanggal < ? AND " + SAH,
barangId, dari.toString());
long[] saldo = {awal};
db.daftar("... WHERE m.barang_id=? AND m.tanggal >= ? AND m.tanggal <= ? AND " + SAH + " ORDER BY m.tanggal, m.id",
rs -> {
long q = rs.getLong(5);
saldo[0] += q;
out.add(new BarisKartu(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4),
q > 0 ? q : 0, q < 0 ? -q : 0, saldo[0]));
return null;
}, barangId, dari.toString(), sampai.toString());
Konstanta SAH menyaring dokumen yang dibatalkan beserta mutasi pembaliknya. Karena keduanya bernilai bersih nol, membuang keduanya tidak mengubah saldo, tetapi membuat kolom "masuk" dan "keluar" di laporan hanya berisi transaksi yang benar-benar terjadi:
private static final String SAH = " m.jenis <> 'BATAL' AND (m.dokumen_id IS NULL OR "
+ "(SELECT status FROM dokumen d WHERE d.id=m.dokumen_id)='AKTIF') ";
Laporan mutasi persediaan memakai rumus yang sama dengan SUM(CASE WHEN ...) per kolom, sehingga saldo akhir di laporan selalu sama dengan stok di master barang.
4. Stok opname yang jujur
Opname yang baik punya dua aturan. Pertama, petugas hitung tidak boleh melihat angka sistem, supaya tidak tergoda menulis angka yang "seharusnya". Karena itu lembar hitung yang dicetak hanya berisi rak, kode, nama, satuan, dan kolom kosong. Kedua, angka sistem pada saat penyesuaian harus disimpan sebagai pembanding, karena stok bisa terus berubah setelahnya.
default -> { // OPNAME
int sistem = (int) b[1];
Database.ubah(c, "INSERT INTO dokumen_item(dokumen_id,barang_id,qty,stok_sistem,harga) VALUES(?,?,?,?,?)",
id, bid, qty, sistem, b[2]);
if (qty != sistem) {
Database.ubah(c, "UPDATE barang SET stok=? WHERE id=?", qty, bid);
mutasi(c, bid, id, tgl, "KOREKSI", qty - sistem, "Opname " + nomor);
}
}
Selisih disimpan sebagai mutasi KOREKSI, bukan menimpa stok diam-diam. Nilai rupiah selisih (selisih x harga satuan) langsung masuk ke berita acara yang ditandatangani petugas, saksi, dan kepala gudang.
5. Barcode Code 128 tanpa library
Label rak dengan barcode mempercepat input karena scanner USB bekerja seperti keyboard: mengetik kode lalu Enter. Code 128 set B cukup sederhana untuk dibuat sendiri. Setiap karakter ASCII 32-126 bernilai kode - 32, setiap nilai punya pola lebar garis-spasi 6 digit yang totalnya 11 modul, lalu ada karakter start, checksum, dan stop.
public static List<Integer> code128(String teks) {
List<Integer> nilai = new ArrayList<>();
nilai.add(104); // start B
int cek = 104;
for (int i = 0; i < teks.length(); i++) {
int v = teks.charAt(i) - 32;
nilai.add(v);
cek += v * (i + 1); // bobot = posisi karakter
}
nilai.add(cek % 103); // checksum
nilai.add(106); // stop
List<Integer> modul = new ArrayList<>();
for (int v : nilai) for (char c : POLA[v].toCharArray()) modul.add(c - '0');
return modul;
}
Menggambarnya di Graphics2D tinggal membagi lebar label dengan jumlah modul (ditambah zona sepi 10 modul di kiri-kanan), lalu mengisi persegi panjang untuk setiap indeks genap. Karena digambar sebagai vektor, hasil cetaknya tajam di printer apa pun. Hasil encoder ini diverifikasi dengan membandingkannya terhadap pustaka barcode lain untuk beberapa kode contoh, dan polanya juga diuji di JUnit.
Ringkasan
- Jadikan dokumen satu-satunya jalan untuk mengubah stok, dan catat setiap perubahan di tabel mutasi.
- Satu dokumen satu transaksi; pakai
UPDATE ... WHERE stok >= qtyagar baris yang gagal membatalkan seluruh dokumen. - Hitung kartu stok dari saldo awal ditambah mutasi berurutan, dan saring dokumen batal bersama mutasi pembaliknya.
- Cetak lembar opname tanpa angka sistem, simpan stok sistem saat penyesuaian, dan catat selisih sebagai koreksi.
- Code 128 bisa dibuat sendiri dengan tabel pola, checksum berbobot posisi, lalu digambar sebagai vektor.
Download source code lengkapnya
Source code lengkapnya berisi dasbor, barang masuk dan keluar multi-baris, surat jalan dan bukti terima A4, stok opname dengan lembar hitung dan berita acara, kartu stok, laporan mutasi persediaan, analisis fast moving dan dead stock, saran restok, label barcode, manajemen pengguna, cadangan database, data contoh 60 hari, dan 17 tes JUnit.
Download Source Code Aplikasi Inventaris Gudang Java Swing + SQLite di Papicode
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026