Aplikasi inventaris yang hanya bisa menampilkan seluruh tabel akan menyusahkan begitu jumlah produk melewati satu layar. Pada bagian ini kita menambahkan kemampuan membaca data secara selektif: mencari produk berdasarkan potongan nama, menyaring satu kategori, serta tiga laporan — stok menipis, nilai inventaris per kategori, dan produk bernilai terbesar. database.py tidak berubah pada bagian ini; seluruh query baru sengaja kita tulis di main.py dulu agar fokus tetap pada SQL-nya, lalu Bagian 6 memindahkan semuanya ke lapisan data sekaligus menjadikannya mudah diuji.
Pencarian dengan LIKE yang Aman #
Operator LIKE memakai dua karakter wildcard: % cocok dengan deretan karakter apa pun, dan _ cocok dengan tepat satu karakter. Masalahnya, pengguna toko sungguhan mengetik nama seperti Coklat Batang 80% — dan wildcard di dalam kata kunci pencarian akan mengubah makna query. Solusinya ada dua bagian yang bekerja bersama:
BATAS_STOK_MENIPIS = 5
JUMLAH_TOP_PRODUK = 5
def escape_like(keyword: str) -> str:
"""Amankan karakter wildcard LIKE (persen dan garis bawah)."""
return (
keyword.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
def jalankan_kueri(sql: str,
params: tuple = ()) -> list[sqlite3.Row]:
"""Jalankan statement SELECT dan kembalikan seluruh baris hasilnya."""
conn = database.koneksi()
try:
return conn.execute(sql, params).fetchall()
finally:
conn.close()
def cari_produk(keyword: str) -> list[sqlite3.Row]:
pola = f"%{escape_like(keyword)}%"
sql = "SELECT * FROM produk WHERE nama LIKE ? ESCAPE '\\'"
return jalankan_kueri(sql, (pola,))Mari kita bedah mekanismenya:
escape_like(): setiap karakter wildcard milik pengguna diberi awalan backslash —%menjadi\%,_menjadi\_— sehingga SQLite memperlakukannya sebagai karakter biasa. Backslash milik pengguna sendiri di-escape paling awal (\menjadi\\) agar tidak berbenturan dengan penanda escape yang baru saja kita pakai; urutan penggantian ini penting.pola = f"%{escape_like(keyword)}%": wildcard%yang kita tambahkan sendiri tetap berfungsi sebagai “apa pun di kiri dan kanan”, sehingga pencariancelupmenemukanTeh Celup.LIKE ? ESCAPE '\\': clauseESCAPEmemberi tahu SQLite bahwa backslash adalah karakter escape pola. Dalam string Python,'\\'adalah satu karakter backslash.jalankan_kueri(): helper kecil yang membuka koneksi lewatdatabase.koneksi(), menjalankan SELECT dengan parameter, lalu menutup koneksi — seluruh query laporan pada bagian ini memakainya.
Filter Kategori dan Laporan Stok Menipis #
Filter kategori memakai perbandingan biasa = ?, sedangkan laporan stok menipis memakai perbandingan < ? dengan batas sebagai parameter:
def filter_kategori(kategori: str) -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE kategori = ? ORDER BY nama"
return jalankan_kueri(sql, (kategori,))
def laporan_stok_menipis() -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE stok < ? ORDER BY stok, nama"
return jalankan_kueri(sql, (BATAS_STOK_MENIPIS,))Urutan ORDER BY stok, nama membuat produk paling kritis tampil paling atas; kolom nama menjadi pemecah kedudukan agar urutan laporan stabil dan dapat direproduksi.
Nilai Inventaris dan Top Produk #
Dua laporan terakhir memakai aggregate function. Nilai inventaris menghitung harga * stok per baris lalu menjumlahkannya per kategori:
def nilai_inventaris_per_kategori() -> list[sqlite3.Row]:
sql = """
SELECT kategori,
COUNT(*) AS jumlah_produk,
SUM(harga * stok) AS nilai_total
FROM produk
GROUP BY kategori
ORDER BY nilai_total DESC
"""
return jalankan_kueri(sql)
def top_produk(jumlah: int = JUMLAH_TOP_PRODUK) -> list[sqlite3.Row]:
sql = """
SELECT id, nama, kategori, harga, stok,
harga * stok AS nilai
FROM produk
ORDER BY nilai DESC, nama
LIMIT ?
"""
return jalankan_kueri(sql, (jumlah,))GROUP BY kategori: mengelompokkan baris produk per kategori sehinggaCOUNT(*)danSUM(...)dihitung per kelompok, bukan untuk seluruh tabel.SUM(harga * stok) AS nilai_total: nilai sebuah produk adalah harga dikali jumlah stok; aliasnilai_totalmembuat kolom hasil mudah diakses lewatrow["nilai_total"].ORDER BY nilai_total DESC: kategori bernilai terbesar tampil lebih dulu.LIMIT ?: jumlah baris top produk dikirim sebagai parameter, sama seperti nilai lainnya — bahkan konstanta pun bisa menjadi parameter bila nilainya ingin mudah diubah.
main.py Lengkap #
Berikut isi lengkap main.py setelah semua menu baru tersambung. Bagian tengah (menu 1 sampai 6) tidak berubah dari Bagian 4, jadi cukup perhatikan fungsi-fungsi baru di bagian atas serta menu 7 sampai 11:
"""Aplikasi Inventaris SQLite - antarmuka pengguna."""
import sqlite3
import database
BATAS_STOK_MENIPIS = 5
JUMLAH_TOP_PRODUK = 5
def escape_like(keyword: str) -> str:
"""Amankan karakter wildcard LIKE (persen dan garis bawah)."""
return (
keyword.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
def jalankan_kueri(sql: str,
params: tuple = ()) -> list[sqlite3.Row]:
"""Jalankan statement SELECT dan kembalikan seluruh baris hasilnya."""
conn = database.koneksi()
try:
return conn.execute(sql, params).fetchall()
finally:
conn.close()
def cari_produk(keyword: str) -> list[sqlite3.Row]:
pola = f"%{escape_like(keyword)}%"
sql = "SELECT * FROM produk WHERE nama LIKE ? ESCAPE '\\' ORDER BY nama"
return jalankan_kueri(sql, (pola,))
def filter_kategori(kategori: str) -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE kategori = ? ORDER BY nama"
return jalankan_kueri(sql, (kategori,))
def laporan_stok_menipis() -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE stok < ? ORDER BY stok, nama"
return jalankan_kueri(sql, (BATAS_STOK_MENIPIS,))
def nilai_inventaris_per_kategori() -> list[sqlite3.Row]:
sql = """
SELECT kategori,
COUNT(*) AS jumlah_produk,
SUM(harga * stok) AS nilai_total
FROM produk
GROUP BY kategori
ORDER BY nilai_total DESC
"""
return jalankan_kueri(sql)
def top_produk(jumlah: int = JUMLAH_TOP_PRODUK) -> list[sqlite3.Row]:
sql = """
SELECT id, nama, kategori, harga, stok,
harga * stok AS nilai
FROM produk
ORDER BY nilai DESC, nama
LIMIT ?
"""
return jalankan_kueri(sql, (jumlah,))
def format_rupiah(nilai: int) -> str:
"""Format angka menjadi teks rupiah, contoh 15000 menjadi Rp15.000."""
return "Rp" + f"{nilai:,}".replace(",", ".")
def cetak_tabel(rows: list) -> None:
"""Cetak daftar produk sebagai tabel rapi."""
print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Harga':>10} {'Stok':>4}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 10} {'-' * 4}")
for row in rows:
print(
f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} "
f"{format_rupiah(row['harga']):>10} {row['stok']:>4}"
)
def baca_angka(prompt: str) -> int:
"""Minta masukan angka bulat, ulangi sampai masukan sah."""
while True:
teks = input(prompt).strip()
try:
return int(teks)
except ValueError:
print("Masukkan angka bulat, misalnya 15000.")
def menu_tambah() -> None:
nama = input("Nama produk: ").strip()
kategori = input("Kategori: ").strip()
harga = baca_angka("Harga (Rp): ")
stok = baca_angka("Stok awal: ")
try:
database.validasi_produk(nama, kategori, harga, stok)
except ValueError as galat:
print(f"Gagal: {galat}")
return
try:
id_baru = database.tambah_produk(nama, kategori, harga, stok)
except sqlite3.IntegrityError:
print(f"Produk dengan nama '{nama}' sudah terdaftar.")
return
print(f"Produk '{nama}' tersimpan dengan id {id_baru}.")
def menu_lihat() -> None:
rows = database.lihat_produk()
if not rows:
print("Belum ada produk.")
return
cetak_tabel(rows)
def menu_cari() -> None:
keyword = input("Kata kunci nama: ").strip()
if not keyword:
print("Kata kunci tidak boleh kosong.")
return
hasil = cari_produk(keyword)
if not hasil:
print(f"Tidak ada produk yang mengandung '{keyword}'.")
return
print(f"Hasil pencarian untuk '{keyword}':")
cetak_tabel(hasil)
def menu_filter() -> None:
kategori = input("Kategori: ").strip()
if not kategori:
print("Kategori tidak boleh kosong.")
return
hasil = filter_kategori(kategori)
if not hasil:
print(f"Tidak ada produk pada kategori '{kategori}'.")
return
print(f"Produk pada kategori '{kategori}':")
cetak_tabel(hasil)
def menu_ubah_harga() -> None:
produk_id = baca_angka("ID produk: ")
harga_baru = baca_angka("Harga baru (Rp): ")
if database.perbarui_harga(produk_id, harga_baru):
print("Harga berhasil diperbarui.")
else:
print(f"Tidak ada produk dengan id {produk_id}.")
def menu_sesuaikan_stok() -> None:
produk_id = baca_angka("ID produk: ")
perubahan = baca_angka("Perubahan stok (boleh negatif): ")
keterangan = input("Keterangan: ").strip() or "penyesuaian manual"
try:
database.sesuaikan_stok(produk_id, perubahan, keterangan)
except ValueError as galat:
print(f"Gagal: {galat}")
return
except sqlite3.IntegrityError:
print("Gagal: penyesuaian membuat stok negatif. Transaksi dibatalkan.")
return
print("Stok berhasil disesuaikan.")
def menu_hapus() -> None:
produk_id = baca_angka("ID produk: ")
if database.hapus_produk(produk_id):
print("Produk berhasil dihapus.")
else:
print(f"Tidak ada produk dengan id {produk_id}.")
def menu_riwayat() -> None:
produk_id = baca_angka("ID produk: ")
produk = database.ambil_produk(produk_id)
if produk is None:
print(f"Tidak ada produk dengan id {produk_id}.")
return
rows = database.lihat_riwayat(produk_id)
if not rows:
print(f"Belum ada riwayat untuk '{produk['nama']}'.")
return
print(f"Riwayat untuk '{produk['nama']}':")
print(f"{'Waktu':<21} {'Perubahan':>9} Keterangan")
for row in rows:
print(f"{row['waktu']:<21} {row['perubahan']:>+9} {row['keterangan']}")
def menu_stok_menipis() -> None:
hasil = laporan_stok_menipis()
if not hasil:
print(f"Tidak ada produk dengan stok di bawah {BATAS_STOK_MENIPIS}.")
return
print(f"Produk dengan stok di bawah {BATAS_STOK_MENIPIS}:")
print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Stok':>4}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 4}")
for row in hasil:
print(f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} {row['stok']:>4}")
def menu_nilai_inventaris() -> None:
hasil = nilai_inventaris_per_kategori()
if not hasil:
print("Belum ada produk untuk dihitung.")
return
print(f"{'Kategori':<12} {'Jumlah':>7} {'Nilai Total':>13}")
print(f"{'-' * 12} {'-' * 7} {'-' * 13}")
total_nilai = 0
total_produk = 0
for row in hasil:
print(
f"{row['kategori']:<12} {row['jumlah_produk']:>7} "
f"{format_rupiah(row['nilai_total']):>13}"
)
total_nilai += row["nilai_total"]
total_produk += row["jumlah_produk"]
print(f"{'-' * 12} {'-' * 7} {'-' * 13}")
print(f"{'TOTAL':<12} {total_produk:>7} {format_rupiah(total_nilai):>13}")
def menu_top_produk() -> None:
hasil = top_produk()
if not hasil:
print("Belum ada produk untuk dihitung.")
return
print(f"Top {JUMLAH_TOP_PRODUK} produk berdasarkan nilai stok:")
print(f"{'#':<3} {'Nama':<18} {'Harga':>10} {'Stok':>4} {'Nilai':>12}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 4} {'-' * 12}")
for peringkat, row in enumerate(hasil, start=1):
print(
f"{peringkat:<3} {row['nama']:<18} "
f"{format_rupiah(row['harga']):>10} {row['stok']:>4} "
f"{format_rupiah(row['nilai']):>12}"
)
def tampilkan_menu() -> None:
print()
print("=== Aplikasi Inventaris ===")
print("1. Tambah produk")
print("2. Lihat semua produk")
print("3. Ubah harga")
print("4. Sesuaikan stok (+/-)")
print("5. Hapus produk")
print("6. Riwayat produk")
print("7. Cari produk")
print("8. Filter per kategori")
print("9. Laporan stok menipis")
print("10. Nilai inventaris per kategori")
print("11. Top produk")
print("0. Keluar")
def main() -> None:
database.inisialisasi()
print("Database siap. Selamat datang!")
while True:
tampilkan_menu()
try:
pilihan = input("Pilih menu: ").strip()
except EOFError:
print()
break
if pilihan == "0":
print("Sampai jumpa!")
break
match pilihan:
case "1":
menu_tambah()
case "2":
menu_lihat()
case "3":
menu_ubah_harga()
case "4":
menu_sesuaikan_stok()
case "5":
menu_hapus()
case "6":
menu_riwayat()
case "7":
menu_cari()
case "8":
menu_filter()
case "9":
menu_stok_menipis()
case "10":
menu_nilai_inventaris()
case "11":
menu_top_produk()
case _:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()Perhatikan pola menu laporan: query mengembalikan list, list kosong ditangani dengan pesan khusus, dan baris total pada menu_nilai_inventaris() dijumlahkan di Python — bukan lewat query tambahan WITH ROLLUP yang tidak didukung SQLite — sehingga kode tetap sederhana.
Mencari dan Membaca Laporan #
Kita mulai dari database bersih, menambahkan produk yang namanya mengandung karakter %, lalu mencari dengan kata kunci kopi. Perintah printf menulis % sebagai %% karena % adalah penentu format milik printf itu sendiri:
printf '1\nCoklat Batang 80%%\nMakanan\n12000\n10\n7\nkopi\n0\n' | python3 main.pyOutput yang Dihasilkan:
Database siap. Selamat datang!
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Nama produk: Kategori: Harga (Rp): Stok awal: Produk 'Coklat Batang 80%' tersimpan dengan id 7.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Kata kunci nama: Hasil pencarian untuk 'kopi':
ID Nama Kategori Harga Stok
--- ------------------ ---------- ---------- ----
1 Kopi Tubruk Minuman Rp15.000 24
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Sampai jumpa!Kata kunci kopi menemukan Kopi Tubruk tanpa memedulikan huruf besar. Sekarang ujian sebenarnya bagi escape_like() — mencari 80% yang seharusnya hanya menemukan produk mengandung karakter % literal, kata kunci tanpa hasil, dan filter kategori:
printf '7\n80%%\n7\nz\n8\nMinuman\n0\n' | python3 main.pyOutput yang Dihasilkan:
Database siap. Selamat datang!
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Kata kunci nama: Hasil pencarian untuk '80%':
ID Nama Kategori Harga Stok
--- ------------------ ---------- ---------- ----
7 Coklat Batang 80% Makanan Rp12.000 10
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Kata kunci nama: Tidak ada produk yang mengandung 'z'.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Kategori: Produk pada kategori 'Minuman':
ID Nama Kategori Harga Stok
--- ------------------ ---------- ---------- ----
1 Kopi Tubruk Minuman Rp15.000 24
2 Teh Celup Minuman Rp8.000 3
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Sampai jumpa!Tanpa escape, pencarian 80% akan diartikan “apa pun yang mengandung 80 diikuti karakter apa saja” dan ikut mencocokkan nama-nama lain. Laporan stok menipis menampilkan tiga produk kritis dari data awal, urut dari yang paling mendesak:
printf '9\n0\n' | python3 main.pyOutput yang Dihasilkan:
Database siap. Selamat datang!
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Produk dengan stok di bawah 5:
ID Nama Kategori Stok
--- ------------------ ---------- ----
4 Keripik Singkong Makanan 2
2 Teh Celup Minuman 3
6 Pulpen Gel Alat Tulis 4
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Sampai jumpa!Terakhir, dua laporan agregat sekaligus — nilai per kategori beserta totalnya, lalu lima produk bernilai terbesar:
printf '10\n11\n0\n' | python3 main.pyOutput yang Dihasilkan:
Database siap. Selamat datang!
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Kategori Jumlah Nilai Total
------------ ------- -------------
Minuman 2 Rp384.000
Makanan 3 Rp320.000
Alat Tulis 2 Rp214.000
------------ ------- -------------
TOTAL 7 Rp918.000
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Top 5 produk berdasarkan nilai stok:
# Nama Harga Stok Nilai
--- ------------------ ---------- ---- ------------
1 Kopi Tubruk Rp15.000 24 Rp360.000
2 Buku Tulis Rp5.000 40 Rp200.000
3 Roti Tawar Rp12.000 15 Rp180.000
4 Coklat Batang 80% Rp12.000 10 Rp120.000
5 Teh Celup Rp8.000 3 Rp24.000
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
7. Cari produk
8. Filter per kategori
9. Laporan stok menipis
10. Nilai inventaris per kategori
11. Top produk
0. Keluar
Pilih menu: Sampai jumpa!Angka pada laporan bisa diperiksa manual: Minuman bernilai 24 × Rp15.000 + 3 × Rp8.000 = Rp384.000, sesuai kolom Nilai Total. Berkas main.py hasil bagian ini tersedia pada berkas sumber di bawah halaman. Aplikasi kini lengkap secara fitur, tetapi belum ada satu pun test otomatis yang membuktikannya — Bagian 6 menutup proyek dengan refactor dan rangkaian test pytest.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.py
Unduh berkas"""Aplikasi Inventaris SQLite - antarmuka pengguna."""
import sqlite3
import database
BATAS_STOK_MENIPIS = 5
JUMLAH_TOP_PRODUK = 5
def escape_like(keyword: str) -> str:
"""Amankan karakter wildcard LIKE (persen dan garis bawah)."""
return (
keyword.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
def jalankan_kueri(sql: str,
params: tuple = ()) -> list[sqlite3.Row]:
"""Jalankan statement SELECT dan kembalikan seluruh baris hasilnya."""
conn = database.koneksi()
try:
return conn.execute(sql, params).fetchall()
finally:
conn.close()
def cari_produk(keyword: str) -> list[sqlite3.Row]:
pola = f"%{escape_like(keyword)}%"
sql = "SELECT * FROM produk WHERE nama LIKE ? ESCAPE '\\' ORDER BY nama"
return jalankan_kueri(sql, (pola,))
def filter_kategori(kategori: str) -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE kategori = ? ORDER BY nama"
return jalankan_kueri(sql, (kategori,))
def laporan_stok_menipis() -> list[sqlite3.Row]:
sql = "SELECT * FROM produk WHERE stok < ? ORDER BY stok, nama"
return jalankan_kueri(sql, (BATAS_STOK_MENIPIS,))
def nilai_inventaris_per_kategori() -> list[sqlite3.Row]:
sql = """
SELECT kategori,
COUNT(*) AS jumlah_produk,
SUM(harga * stok) AS nilai_total
FROM produk
GROUP BY kategori
ORDER BY nilai_total DESC
"""
return jalankan_kueri(sql)
def top_produk(jumlah: int = JUMLAH_TOP_PRODUK) -> list[sqlite3.Row]:
sql = """
SELECT id, nama, kategori, harga, stok,
harga * stok AS nilai
FROM produk
ORDER BY nilai DESC, nama
LIMIT ?
"""
return jalankan_kueri(sql, (jumlah,))
def format_rupiah(nilai: int) -> str:
"""Format angka menjadi teks rupiah, contoh 15000 menjadi Rp15.000."""
return "Rp" + f"{nilai:,}".replace(",", ".")
def cetak_tabel(rows: list) -> None:
"""Cetak daftar produk sebagai tabel rapi."""
print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Harga':>10} {'Stok':>4}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 10} {'-' * 4}")
for row in rows:
print(
f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} "
f"{format_rupiah(row['harga']):>10} {row['stok']:>4}"
)
def baca_angka(prompt: str) -> int:
"""Minta masukan angka bulat, ulangi sampai masukan sah."""
while True:
teks = input(prompt).strip()
try:
return int(teks)
except ValueError:
print("Masukkan angka bulat, misalnya 15000.")
def menu_tambah() -> None:
nama = input("Nama produk: ").strip()
kategori = input("Kategori: ").strip()
harga = baca_angka("Harga (Rp): ")
stok = baca_angka("Stok awal: ")
try:
database.validasi_produk(nama, kategori, harga, stok)
except ValueError as galat:
print(f"Gagal: {galat}")
return
try:
id_baru = database.tambah_produk(nama, kategori, harga, stok)
except sqlite3.IntegrityError:
print(f"Produk dengan nama '{nama}' sudah terdaftar.")
return
print(f"Produk '{nama}' tersimpan dengan id {id_baru}.")
def menu_lihat() -> None:
rows = database.lihat_produk()
if not rows:
print("Belum ada produk.")
return
cetak_tabel(rows)
def menu_cari() -> None:
keyword = input("Kata kunci nama: ").strip()
if not keyword:
print("Kata kunci tidak boleh kosong.")
return
hasil = cari_produk(keyword)
if not hasil:
print(f"Tidak ada produk yang mengandung '{keyword}'.")
return
print(f"Hasil pencarian untuk '{keyword}':")
cetak_tabel(hasil)
def menu_filter() -> None:
kategori = input("Kategori: ").strip()
if not kategori:
print("Kategori tidak boleh kosong.")
return
hasil = filter_kategori(kategori)
if not hasil:
print(f"Tidak ada produk pada kategori '{kategori}'.")
return
print(f"Produk pada kategori '{kategori}':")
cetak_tabel(hasil)
def menu_ubah_harga() -> None:
produk_id = baca_angka("ID produk: ")
harga_baru = baca_angka("Harga baru (Rp): ")
if database.perbarui_harga(produk_id, harga_baru):
print("Harga berhasil diperbarui.")
else:
print(f"Tidak ada produk dengan id {produk_id}.")
def menu_sesuaikan_stok() -> None:
produk_id = baca_angka("ID produk: ")
perubahan = baca_angka("Perubahan stok (boleh negatif): ")
keterangan = input("Keterangan: ").strip() or "penyesuaian manual"
try:
database.sesuaikan_stok(produk_id, perubahan, keterangan)
except ValueError as galat:
print(f"Gagal: {galat}")
return
except sqlite3.IntegrityError:
print("Gagal: penyesuaian membuat stok negatif. Transaksi dibatalkan.")
return
print("Stok berhasil disesuaikan.")
def menu_hapus() -> None:
produk_id = baca_angka("ID produk: ")
if database.hapus_produk(produk_id):
print("Produk berhasil dihapus.")
else:
print(f"Tidak ada produk dengan id {produk_id}.")
def menu_riwayat() -> None:
produk_id = baca_angka("ID produk: ")
produk = database.ambil_produk(produk_id)
if produk is None:
print(f"Tidak ada produk dengan id {produk_id}.")
return
rows = database.lihat_riwayat(produk_id)
if not rows:
print(f"Belum ada riwayat untuk '{produk['nama']}'.")
return
print(f"Riwayat untuk '{produk['nama']}':")
print(f"{'Waktu':<21} {'Perubahan':>9} Keterangan")
for row in rows:
print(f"{row['waktu']:<21} {row['perubahan']:>+9} {row['keterangan']}")
def menu_stok_menipis() -> None:
hasil = laporan_stok_menipis()
if not hasil:
print(f"Tidak ada produk dengan stok di bawah {BATAS_STOK_MENIPIS}.")
return
print(f"Produk dengan stok di bawah {BATAS_STOK_MENIPIS}:")
print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Stok':>4}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 4}")
for row in hasil:
print(f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} {row['stok']:>4}")
def menu_nilai_inventaris() -> None:
hasil = nilai_inventaris_per_kategori()
if not hasil:
print("Belum ada produk untuk dihitung.")
return
print(f"{'Kategori':<12} {'Jumlah':>7} {'Nilai Total':>13}")
print(f"{'-' * 12} {'-' * 7} {'-' * 13}")
total_nilai = 0
total_produk = 0
for row in hasil:
print(
f"{row['kategori']:<12} {row['jumlah_produk']:>7} "
f"{format_rupiah(row['nilai_total']):>13}"
)
total_nilai += row["nilai_total"]
total_produk += row["jumlah_produk"]
print(f"{'-' * 12} {'-' * 7} {'-' * 13}")
print(f"{'TOTAL':<12} {total_produk:>7} {format_rupiah(total_nilai):>13}")
def menu_top_produk() -> None:
hasil = top_produk()
if not hasil:
print("Belum ada produk untuk dihitung.")
return
print(f"Top {JUMLAH_TOP_PRODUK} produk berdasarkan nilai stok:")
print(f"{'#':<3} {'Nama':<18} {'Harga':>10} {'Stok':>4} {'Nilai':>12}")
print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 4} {'-' * 12}")
for peringkat, row in enumerate(hasil, start=1):
print(
f"{peringkat:<3} {row['nama']:<18} "
f"{format_rupiah(row['harga']):>10} {row['stok']:>4} "
f"{format_rupiah(row['nilai']):>12}"
)
def tampilkan_menu() -> None:
print()
print("=== Aplikasi Inventaris ===")
print("1. Tambah produk")
print("2. Lihat semua produk")
print("3. Ubah harga")
print("4. Sesuaikan stok (+/-)")
print("5. Hapus produk")
print("6. Riwayat produk")
print("7. Cari produk")
print("8. Filter per kategori")
print("9. Laporan stok menipis")
print("10. Nilai inventaris per kategori")
print("11. Top produk")
print("0. Keluar")
def main() -> None:
database.inisialisasi()
print("Database siap. Selamat datang!")
while True:
tampilkan_menu()
try:
pilihan = input("Pilih menu: ").strip()
except EOFError:
print()
break
if pilihan == "0":
print("Sampai jumpa!")
break
match pilihan:
case "1":
menu_tambah()
case "2":
menu_lihat()
case "3":
menu_ubah_harga()
case "4":
menu_sesuaikan_stok()
case "5":
menu_hapus()
case "6":
menu_riwayat()
case "7":
menu_cari()
case "8":
menu_filter()
case "9":
menu_stok_menipis()
case "10":
menu_nilai_inventaris()
case "11":
menu_top_produk()
case _:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()