CRUD di Bagian 3 bekerja untuk masukan yang sopan, tetapi pengguna dunia nyata mengosongkan prompt, mengetik harga minus, dan menambahkan produk yang sama dua kali. Pada bagian ini kita menghadapi semuanya: validasi ditolak sebelum menyentuh database, nama duplikat diterjemahkan menjadi pesan yang jelas, dan penyesuaian stok dijalankan sebagai transaksi yang utuh — dua penulisan yang harus terjadi bersamaan, atau tidak terjadi sama sekali.
Validasi Sebelum Menyimpan #
Validasi paling awal sebaiknya berupa function murni yang mudah dipanggil ulang. Tambahkan validasi_produk() di database.py:
def validasi_produk(nama: str, kategori: str, harga: int, stok: int) -> None:
"""Periksa masukan produk; angkat ValueError bila tidak sah."""
if not nama.strip():
raise ValueError("Nama produk tidak boleh kosong.")
if not kategori.strip():
raise ValueError("Kategori tidak boleh kosong.")
if isinstance(harga, bool) or not isinstance(harga, int):
raise ValueError("Harga harus berupa bilangan bulat.")
if harga < 0:
raise ValueError("Harga tidak boleh negatif.")
if isinstance(stok, bool) or not isinstance(stok, int):
raise ValueError("Stok harus berupa bilangan bulat.")
if stok < 0:
raise ValueError("Stok tidak boleh negatif.")Tiga keputusan pada function tersebut:
not nama.strip(): menolak string kosong sekaligus string yang hanya berisi spasi, karena" ".strip()menghasilkan string kosong.isinstance(harga, bool) or not isinstance(harga, int): pemeriksaanbooldilakukan lebih dulu karenaTruedanFalseadalah subclass dariint—isinstance(True, int)bernilaiTrue. Tanpa pengecualian ini, hargaTrueakan lolos sebagai angka1.- Mengangkat
ValueError, bukanreturn False: exception membawa pesan penyebab sehingga pemanggil cukup satu jalurtry/except, dan function tetap bekerja untuk caller mana pun.
Validasi di Python adalah lini pertama; constraint CHECK dan UNIQUE di database tetap lini terakhir. Keduanya saling melengkapi, bukan saling menggantikan.
Transaksi Penyesuaian Stok #
Menu “ubah stok” dari Bagian 3 memasang stok ke nilai mutlak — cara yang rawan salah ketik dan tidak meninggalkan jejak. Kita menggantinya dengan penyesuaian delta: pengguna mengetik +5 atau -2, stok berubah sebesar selisih itu, dan setiap perubahan tercatat di tabel riwayat. Dua penulisan ini wajib terjadi bersamaan:
def sesuaikan_stok(produk_id: int, perubahan: int, keterangan: str,
db_path: str = NAMA_DATABASE) -> None:
"""Ubah stok sebesar perubahan (+/-) dan catat ke riwayat secara atomik."""
conn = koneksi(db_path)
try:
with conn: # commit bila sukses, rollback bila ada galat
kursor = conn.execute(
"UPDATE produk SET stok = stok + ? WHERE id = ?",
(perubahan, produk_id),
)
if kursor.rowcount == 0:
raise ValueError(f"Produk dengan id {produk_id} tidak ditemukan.")
conn.execute(
"INSERT INTO riwayat (produk_id, perubahan, keterangan)"
" VALUES (?, ?, ?)",
(produk_id, perubahan, keterangan),
)
finally:
conn.close()
def lihat_riwayat(produk_id: int,
db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
"""Ambil riwayat penyesuaian stok sebuah produk, terbaru lebih dulu."""
conn = koneksi(db_path)
try:
return conn.execute(
"SELECT * FROM riwayat WHERE produk_id = ? ORDER BY id DESC",
(produk_id,),
).fetchall()
finally:
conn.close()Bagian pentingnya ada pada satu baris: with conn:. Perilaku context manager pada objek koneksi sqlite3 berbeda dari with open(...) pada berkas:
- Saat blok normal selesai, koneksi otomatis menjalankan
commit()— kedua penulisan (UPDATE stok dan INSERT riwayat) dikunci bersama sebagai satu transaksi. - Saat exception lolos dari blok, koneksi otomatis menjalankan
rollback()— seluruh perubahan di dalam blok dibatalkan, tidak ada yang setengah jadi. - Koneksi tidak ditutup oleh
with. Berbeda dari berkas, keluar dari blok hanya menyelesaikan transaksi; itulah mengapafinally: conn.close()tetap diperlukan.
Skenario gagalnya nyata: penyesuaian -999 pada stok 8 membuat stok bernilai negatif, sehingga constraint CHECK (stok >= 0) dari Bagian 1 mengangkat sqlite3.IntegrityError pada UPDATE. Rollback membatalkan transaksi, dan baris riwayat tidak pernah tersimpan — persis perilaku yang kita inginkan.
database.py Lengkap #
Function perbarui_stok() dari Bagian 3 sengaja dihapus dari versi ini: menetapkan stok mutlak telah digantikan oleh sesuaikan_stok() yang lebih aman dan meninggalkan jejak. Berikut isi lengkap database.py setelah seluruh perubahan bagian ini:
"""Lapisan akses data untuk aplikasi inventaris."""
import sqlite3
from pathlib import Path
NAMA_DATABASE = "inventaris.db"
BERKAS_SKEMA = Path(__file__).with_name("schema.sql")
def koneksi(db_path: str = NAMA_DATABASE) -> sqlite3.Connection:
"""Buka koneksi baru ke database inventaris."""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA foreign_keys = ON")
conn.row_factory = sqlite3.Row
return conn
def inisialisasi(db_path: str = NAMA_DATABASE) -> None:
"""Terapkan schema.sql ke database. Aman dijalankan berulang."""
conn = koneksi(db_path)
try:
conn.executescript(BERKAS_SKEMA.read_text(encoding="utf-8"))
conn.commit()
finally:
conn.close()
def validasi_produk(nama: str, kategori: str, harga: int, stok: int) -> None:
"""Periksa masukan produk; angkat ValueError bila tidak sah."""
if not nama.strip():
raise ValueError("Nama produk tidak boleh kosong.")
if not kategori.strip():
raise ValueError("Kategori tidak boleh kosong.")
if isinstance(harga, bool) or not isinstance(harga, int):
raise ValueError("Harga harus berupa bilangan bulat.")
if harga < 0:
raise ValueError("Harga tidak boleh negatif.")
if isinstance(stok, bool) or not isinstance(stok, int):
raise ValueError("Stok harus berupa bilangan bulat.")
if stok < 0:
raise ValueError("Stok tidak boleh negatif.")
def tambah_produk(nama: str, kategori: str, harga: int, stok: int,
db_path: str = NAMA_DATABASE) -> int:
"""Simpan produk baru, kembalikan id-nya."""
sql = "INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)"
conn = koneksi(db_path)
try:
kursor = conn.execute(sql, (nama, kategori, harga, stok))
conn.commit()
return int(kursor.lastrowid)
finally:
conn.close()
def lihat_produk(db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
"""Ambil seluruh produk, urut berdasarkan nama."""
conn = koneksi(db_path)
try:
return conn.execute("SELECT * FROM produk ORDER BY nama").fetchall()
finally:
conn.close()
def ambil_produk(produk_id: int,
db_path: str = NAMA_DATABASE) -> sqlite3.Row | None:
"""Ambil satu produk berdasarkan id; None bila tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute("SELECT * FROM produk WHERE id = ?", (produk_id,))
return kursor.fetchone()
finally:
conn.close()
def perbarui_harga(produk_id: int, harga_baru: int,
db_path: str = NAMA_DATABASE) -> bool:
"""Set harga produk menjadi nilai baru. False bila id tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute(
"UPDATE produk SET harga = ? WHERE id = ?", (harga_baru, produk_id)
)
conn.commit()
return kursor.rowcount > 0
finally:
conn.close()
def sesuaikan_stok(produk_id: int, perubahan: int, keterangan: str,
db_path: str = NAMA_DATABASE) -> None:
"""Ubah stok sebesar perubahan (+/-) dan catat ke riwayat secara atomik."""
conn = koneksi(db_path)
try:
with conn: # commit bila sukses, rollback bila ada galat
kursor = conn.execute(
"UPDATE produk SET stok = stok + ? WHERE id = ?",
(perubahan, produk_id),
)
if kursor.rowcount == 0:
raise ValueError(f"Produk dengan id {produk_id} tidak ditemukan.")
conn.execute(
"INSERT INTO riwayat (produk_id, perubahan, keterangan)"
" VALUES (?, ?, ?)",
(produk_id, perubahan, keterangan),
)
finally:
conn.close()
def lihat_riwayat(produk_id: int,
db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
"""Ambil riwayat penyesuaian stok sebuah produk, terbaru lebih dulu."""
conn = koneksi(db_path)
try:
return conn.execute(
"SELECT * FROM riwayat WHERE produk_id = ? ORDER BY id DESC",
(produk_id,),
).fetchall()
finally:
conn.close()
def hapus_produk(produk_id: int, db_path: str = NAMA_DATABASE) -> bool:
"""Hapus produk beserta riwayatnya. False bila id tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute("DELETE FROM produk WHERE id = ?", (produk_id,))
conn.commit()
return kursor.rowcount > 0
finally:
conn.close()main.py dengan Penanganan Galat yang Jujur #
Menu 4 berganti tugas menjadi “Sesuaikan stok”, menu 6 baru menampilkan riwayat, dan menu_tambah() kini memvalidasi lalu menangani nama duplikat. Berikut isi lengkap main.py yang baru:
"""Aplikasi Inventaris SQLite - antarmuka pengguna."""
import sqlite3
import database
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_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 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("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 _:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()Perhatikan pola penanganan galat menu_tambah(): validasi format ditangani sebagai ValueError dari validasi_produk(), sedangkan pelanggaran constraint database ditangani sebagai sqlite3.IntegrityError dari tambah_produk(). Keduanya diterjemahkan menjadi kalimat yang bisa ditindaklanjuti pengguna, bukan traceback.
Menguji Batas Aplikasi #
Kita mulai dari database bersih lalu mencoba tiga masukan tidak sopan secara berurutan: nama yang sudah terdaftar, nama kosong, dan harga negatif:
printf '1\nKopi Tubruk\nMinuman\n15000\n5\n1\n\nMinuman\n100\n5\n1\nTeh Botol\nMinuman\n-100\n5\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
0. Keluar
Pilih menu: Nama produk: Kategori: Harga (Rp): Stok awal: Produk dengan nama 'Kopi Tubruk' sudah terdaftar.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: Nama produk: Kategori: Harga (Rp): Stok awal: Gagal: Nama produk tidak boleh kosong.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: Nama produk: Kategori: Harga (Rp): Stok awal: Gagal: Harga tidak boleh negatif.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: Sampai jumpa!Ketiganya ditolak tanpa traceback dan tanpa data tersimpan. Sekarang sisi transaksi: Teh Celup (id 2, stok 3) kita tambah 5, lalu riwayatnya diperiksa:
printf '4\n2\n5\nrestock mingguan\n6\n2\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
0. Keluar
Pilih menu: ID produk: Perubahan stok (boleh negatif): Keterangan: Stok berhasil disesuaikan.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: ID produk: Riwayat untuk 'Teh Celup':
Waktu Perubahan Keterangan
2026-10-12 23:44:48 +5 restock mingguan
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: Sampai jumpa!Kolom waktu terisi CURRENT_TIMESTAMP dalam zona waktu UTC, jadi nilainya berbeda pada mesin dan waktu Anda. Ujian terakhir adalah kegagalan di tengah transaksi: kita mencoba menyesuaikan stok dengan -999 sehingga UPDATE melanggar CHECK (stok >= 0):
printf '4\n2\n-999\nsalah input\n2\n6\n2\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
0. Keluar
Pilih menu: ID produk: Perubahan stok (boleh negatif): Keterangan: Gagal: penyesuaian membuat stok negatif. Transaksi dibatalkan.
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: ID Nama Kategori Harga Stok
--- ------------------ ---------- ---------- ----
5 Buku Tulis Alat Tulis Rp5.000 40
4 Keripik Singkong Makanan Rp10.000 2
1 Kopi Tubruk Minuman Rp15.000 24
6 Pulpen Gel Alat Tulis Rp3.500 4
3 Roti Tawar Makanan Rp12.000 15
2 Teh Celup Minuman Rp8.000 8
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: ID produk: Riwayat untuk 'Teh Celup':
Waktu Perubahan Keterangan
2026-10-12 23:44:48 +5 restock mingguan
=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Sesuaikan stok (+/-)
5. Hapus produk
6. Riwayat produk
0. Keluar
Pilih menu: Sampai jumpa!Hasilnya membuktikan atomikitas: stok Teh Celup tetap 8 (bukan 8 - 999), dan riwayatnya masih hanya berisi satu baris +5. UPDATE yang gagal tidak meninggalkan INSERT riwayat yatim karena rollback membatalkan keduanya. Kedua module hasil bagian ini tersedia pada berkas sumber di bawah halaman; data kini tidak hanya tersimpan, tetapi juga terjaga konsistensinya. Bagian 5 melanjutkan ke sisi pembacaan: pencarian dan laporan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
database.py
Unduh berkas"""Lapisan akses data untuk aplikasi inventaris."""
import sqlite3
from pathlib import Path
NAMA_DATABASE = "inventaris.db"
BERKAS_SKEMA = Path(__file__).with_name("schema.sql")
def koneksi(db_path: str = NAMA_DATABASE) -> sqlite3.Connection:
"""Buka koneksi baru ke database inventaris."""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA foreign_keys = ON")
conn.row_factory = sqlite3.Row
return conn
def inisialisasi(db_path: str = NAMA_DATABASE) -> None:
"""Terapkan schema.sql ke database. Aman dijalankan berulang."""
conn = koneksi(db_path)
try:
conn.executescript(BERKAS_SKEMA.read_text(encoding="utf-8"))
conn.commit()
finally:
conn.close()
def validasi_produk(nama: str, kategori: str, harga: int, stok: int) -> None:
"""Periksa masukan produk; angkat ValueError bila tidak sah."""
if not nama.strip():
raise ValueError("Nama produk tidak boleh kosong.")
if not kategori.strip():
raise ValueError("Kategori tidak boleh kosong.")
if isinstance(harga, bool) or not isinstance(harga, int):
raise ValueError("Harga harus berupa bilangan bulat.")
if harga < 0:
raise ValueError("Harga tidak boleh negatif.")
if isinstance(stok, bool) or not isinstance(stok, int):
raise ValueError("Stok harus berupa bilangan bulat.")
if stok < 0:
raise ValueError("Stok tidak boleh negatif.")
def tambah_produk(nama: str, kategori: str, harga: int, stok: int,
db_path: str = NAMA_DATABASE) -> int:
"""Simpan produk baru, kembalikan id-nya."""
sql = "INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)"
conn = koneksi(db_path)
try:
kursor = conn.execute(sql, (nama, kategori, harga, stok))
conn.commit()
return int(kursor.lastrowid)
finally:
conn.close()
def lihat_produk(db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
"""Ambil seluruh produk, urut berdasarkan nama."""
conn = koneksi(db_path)
try:
return conn.execute("SELECT * FROM produk ORDER BY nama").fetchall()
finally:
conn.close()
def ambil_produk(produk_id: int,
db_path: str = NAMA_DATABASE) -> sqlite3.Row | None:
"""Ambil satu produk berdasarkan id; None bila tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute("SELECT * FROM produk WHERE id = ?", (produk_id,))
return kursor.fetchone()
finally:
conn.close()
def perbarui_harga(produk_id: int, harga_baru: int,
db_path: str = NAMA_DATABASE) -> bool:
"""Set harga produk menjadi nilai baru. False bila id tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute(
"UPDATE produk SET harga = ? WHERE id = ?", (harga_baru, produk_id)
)
conn.commit()
return kursor.rowcount > 0
finally:
conn.close()
def sesuaikan_stok(produk_id: int, perubahan: int, keterangan: str,
db_path: str = NAMA_DATABASE) -> None:
"""Ubah stok sebesar perubahan (+/-) dan catat ke riwayat secara atomik."""
conn = koneksi(db_path)
try:
with conn: # commit bila sukses, rollback bila ada galat
kursor = conn.execute(
"UPDATE produk SET stok = stok + ? WHERE id = ?",
(perubahan, produk_id),
)
if kursor.rowcount == 0:
raise ValueError(f"Produk dengan id {produk_id} tidak ditemukan.")
conn.execute(
"INSERT INTO riwayat (produk_id, perubahan, keterangan)"
" VALUES (?, ?, ?)",
(produk_id, perubahan, keterangan),
)
finally:
conn.close()
def lihat_riwayat(produk_id: int,
db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
"""Ambil riwayat penyesuaian stok sebuah produk, terbaru lebih dulu."""
conn = koneksi(db_path)
try:
return conn.execute(
"SELECT * FROM riwayat WHERE produk_id = ? ORDER BY id DESC",
(produk_id,),
).fetchall()
finally:
conn.close()
def hapus_produk(produk_id: int, db_path: str = NAMA_DATABASE) -> bool:
"""Hapus produk beserta riwayatnya. False bila id tidak ada."""
conn = koneksi(db_path)
try:
kursor = conn.execute("DELETE FROM produk WHERE id = ?", (produk_id,))
conn.commit()
return kursor.rowcount > 0
finally:
conn.close()
main.py
Unduh berkas"""Aplikasi Inventaris SQLite - antarmuka pengguna."""
import sqlite3
import database
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_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 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("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 _:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()