Membuat Aplikasi Kasir Desktop dengan Java Swing dan SQLite: Stok Anti Minus, Nomor Nota, dan Struk Thermal
Membuat Aplikasi Kasir Desktop dengan Java Swing dan SQLite: Stok Anti Minus, Nomor Nota, dan Struk Thermal
Aplikasi kasir terlihat sederhana: pilih barang, hitung total, terima uang. Masalahnya baru muncul saat dipakai sungguhan. Stok bisa minus karena dua kasir menjual barang terakhir bersamaan, nomor nota dobel, harga di layar ternyata sudah basi, atau struk terpotong di printer 58 mm. Artikel ini membahas lima teknik inti yang dipakai di aplikasi Kasir Toko berbasis Java Swing dan SQLite, lengkap dengan potongan kodenya.
1. Satu pintu untuk transaksi database
SQLite cocok untuk aplikasi desktop karena tidak perlu server: database hanyalah satu file. Supaya kode service tetap bersih, semua operasi tulis yang saling bergantung dibungkus dalam satu helper transaksi.
public <T> T tx(Kerja<T> kerja) {
try (Connection c = koneksi()) {
try (Statement st = c.createStatement()) { st.execute("BEGIN IMMEDIATE"); }
try {
T hasil = kerja.jalan(c);
try (Statement st = c.createStatement()) { st.execute("COMMIT"); }
return hasil;
} catch (RuntimeException | SQLException e) {
try (Statement st = c.createStatement()) { st.execute("ROLLBACK"); } catch (SQLException ignored) { }
throw e;
}
} catch (SQLException e) {
throw new DbException(e);
}
}
Kuncinya ada di BEGIN IMMEDIATE. Transaksi biasa (BEGIN) di SQLite baru meminta kunci tulis saat perintah tulis pertama dijalankan. Akibatnya dua proses bisa sama-sama membaca stok 1, lalu sama-sama menulis. Dengan IMMEDIATE, kunci tulis diambil sejak awal sehingga urutan "baca, cek, tulis" berjalan bergiliran. Tambahkan juga PRAGMA busy_timeout = 5000 agar proses kedua menunggu, bukan langsung gagal.
2. Hitung ulang harga dan kurangi stok secara atomik
Keranjang di layar hanya menyimpan id produk dan jumlah. Harga, harga beli, dan stok selalu diambil ulang dari database saat tombol bayar ditekan. Ini mencegah dua masalah: harga yang baru diubah admin tidak terpakai, dan angka di layar yang dimanipulasi.
for (Map.Entry<Long, Integer> e : gabung.entrySet()) {
Object[] p = Database.satu(c, "SELECT nama, harga_jual, harga_beli, stok, aktif FROM produk WHERE id=?",
rs -> new Object[]{rs.getString(1), rs.getLong(2), rs.getLong(3), rs.getInt(4), rs.getInt(5)}, e.getKey());
if (p == null || (int) p[4] != 1) throw new Gagal("Ada produk yang sudah tidak dijual.");
if ((int) p[3] < e.getValue()) throw new Gagal("Stok " + p[0] + " tinggal " + p[3] + ".");
subtotal += (long) p[1] * e.getValue();
modal += (long) p[2] * e.getValue();
}
Lalu stok dikurangi dengan UPDATE bersyarat. Kalau baris yang ter-update nol, berarti stok sudah tidak cukup dan seluruh transaksi dibatalkan oleh tx():
int n = Database.ubah(c, "UPDATE produk SET stok = stok - ? WHERE id=? AND stok >= ?", qty, pid, qty);
if (n == 0) throw new Gagal("Stok " + nama + " baru saja habis.");
Pengecekan di Java dan di SQL saling melengkapi: yang pertama memberi pesan ramah, yang kedua menjadi jaring pengaman terakhir. Tambahkan juga CHECK (stok >= 0) di skema tabel. Di tes, delapan thread yang mewakili delapan komputer kasir mencoba membeli 2 unit dari stok 5 secara bersamaan; hasilnya selalu tepat dua transaksi yang berhasil dan sisa stok 1.
3. Nomor nota harian yang tidak pernah dobel
Format TK-yyMMdd-0001 mudah dibaca kasir dan mudah dicari. Karena dibuat di dalam transaksi BEGIN IMMEDIATE yang sama dengan penyimpanan nota, tidak ada dua transaksi yang bisa membaca nomor terakhir pada saat bersamaan.
static String nomorBaru(Connection c, LocalDate tgl) throws SQLException {
String prefix = "TK-" + tgl.format(DateTimeFormatter.ofPattern("yyMMdd")) + "-";
String last = Database.satu(c, "SELECT nomor FROM transaksi WHERE nomor LIKE ? ORDER BY nomor DESC LIMIT 1",
rs -> rs.getString(1), prefix + "%");
int urut = last == null ? 1 : Integer.parseInt(last.substring(prefix.length())) + 1;
return prefix + String.format("%04d", urut);
}
Kolom nomor tetap diberi UNIQUE sebagai lapisan kedua. Karena tanggal diambil dari Clock yang disuntikkan ke service, tes bisa memajukan waktu satu hari dan memastikan nomor kembali ke 0001.
4. Password PBKDF2 dan kunci akun tanpa library tambahan
JDK sudah menyediakan PBKDF2, jadi tidak perlu dependensi bcrypt. Simpan algoritma, jumlah iterasi, salt, dan hash dalam satu string agar iterasi bisa dinaikkan di masa depan tanpa merusak password lama.
public static String hash(String pw) {
byte[] salt = new byte[16];
RNG.nextBytes(salt);
byte[] h = pbkdf2(pw.toCharArray(), salt, ITERASI);
Base64.Encoder b = Base64.getEncoder();
return "pbkdf2$" + ITERASI + "$" + b.encodeToString(salt) + "$" + b.encodeToString(h);
}
public static boolean cocok(String pw, String simpanan) {
String[] p = simpanan.split("\\$");
byte[] uji = pbkdf2(pw.toCharArray(), Base64.getDecoder().decode(p[2]), Integer.parseInt(p[1]));
return MessageDigest.isEqual(Base64.getDecoder().decode(p[3]), uji);
}
MessageDigest.isEqual membandingkan dalam waktu konstan. Saat login, penghitung gagal disimpan per akun; setelah lima kali salah, kolom kunci_sampai diisi waktu sekarang ditambah lima menit. Untuk username yang tidak ada, aplikasi tetap menjalankan satu kali hitung PBKDF2 terhadap hash palsu dan menampilkan pesan yang sama, sehingga penyerang tidak bisa menebak username mana yang valid dari pesan atau lama respons.
5. Struk thermal dengan teks lebar tetap
Printer thermal 58 mm muat sekitar 32 karakter huruf monospace per baris, sedangkan 80 mm sekitar 48. Daripada mengatur posisi piksel, susun struk sebagai daftar baris teks, lalu gunakan daftar yang sama untuk pratinjau di layar dan untuk dicetak.
static String kiriKanan(String kiri, String kanan, int w) {
int sisa = w - kanan.length() - 1;
if (kiri.length() > sisa) kiri = kiri.substring(0, Math.max(0, sisa));
return kiri + " ".repeat(Math.max(1, w - kiri.length() - kanan.length())) + kanan;
}
Nama barang yang panjang dibungkus per kata, sedangkan baris harga memakai kiriKanan. Saat mencetak, ukuran kertas diatur sendiri dan ukuran huruf dihitung agar satu baris pas selebar area cetak:
Paper paper = new Paper();
double lebar = lebarMm / 25.4 * 72; // milimeter ke point
double tinggi = (baris.size() + 4) * 11.0 + 36;
paper.setSize(lebar, tinggi);
paper.setImageableArea(6, 10, lebar - 12, tinggi - 20);
...
double lebarBaris = fm.charWidth('M') * kolom;
size = (float) Math.min(9.5, size * pf.getImageableWidth() / lebarBaris);
Dengan begitu struk yang sama rapi di printer 58 mm maupun 80 mm, dan pemilik toko cukup memilih lebar kertas di menu Pengaturan.
Bonus: CSV yang aman dibuka di Excel
Nama produk atau nama pelanggan bisa saja diawali =, +, -, atau @. Excel akan menganggapnya rumus. Saat ekspor, sel teks seperti itu diberi awalan petik satu, sementara angka asli dibiarkan sebagai angka agar tetap bisa dijumlahkan. Gunakan pemisah titik koma dan BOM UTF-8 supaya Excel berbahasa Indonesia langsung membaca kolom dengan benar.
Ringkasan
- Bungkus operasi tulis dalam
BEGIN IMMEDIATEagar urutan baca-cek-tulis di SQLite tidak saling tabrak. - Jangan percaya angka dari layar: ambil ulang harga dan stok dari database, lalu kurangi stok dengan
UPDATE ... WHERE stok >= qty. - Buat nomor nota di dalam transaksi yang sama dengan penyimpanan nota, dan pasang
UNIQUEsebagai pengaman. - PBKDF2 dari JDK sudah cukup untuk hash password; tambahkan kunci akun dan pesan galat yang seragam.
- Susun struk sebagai teks lebar tetap, lalu pakai untuk pratinjau dan cetak dengan ukuran huruf yang menyesuaikan kertas.
Download source code lengkapnya
Source code lengkap aplikasi ini berisi layar kasir dengan scan barcode, keranjang, diskon, pembayaran tunai/QRIS/debit, struk thermal, manajemen produk dan stok, opname, riwayat transaksi, laporan omzet dan laba dengan grafik, ekspor CSV, manajemen pengguna, cadangan database, data contoh, serta 18 tes JUnit.
Download Source Code Aplikasi Kasir Toko 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