Skema dari Bagian 1 sudah menolak data sampah, tetapi belum ada cara memasukkan data yang sah. Pada bagian ini kita membangun program utamanya: menu interaktif yang menunggu perintah pengguna, mengumpulkan masukan, lalu menyimpannya lewat fungsi di db.py. Validasinya bekerja dua lapis — format diperiksa oleh Python sebelum query dijalankan, sedangkan aturan bisnis tetap dijaga CHECK constraint di database — sehingga salah satu lapis gagal pun data tetap aman.
Dua Fungsi Baru di db.py #
Program utama butuh dua operasi data: menyimpan satu transaksi dan mengambil yang terbaru. Keduanya ditambahkan ke db.py, lengkap dengan satu pembantu kecil tampilan:
def format_rupiah(jumlah):
"""Format angka menjadi teks rupiah, mis. 5500000 menjadi Rp5.500.000."""
return f"Rp{jumlah:,.0f}".replace(",", ".")
def insert_transaksi(tanggal, jenis, kategori, jumlah, catatan="", path=NAMA_DB):
"""Simpan satu transaksi, kembalikan id baris baru."""
with closing(get_connection(path)) as con:
with con:
cur = con.execute(
"INSERT INTO transaksi (tanggal, jenis, kategori, jumlah, catatan)"
" VALUES (?, ?, ?, ?, ?)",
(tanggal, jenis, kategori, jumlah, catatan),
)
return cur.lastrowid
def transaksi_terakhir(batas=10, path=NAMA_DB):
"""Ambil sejumlah transaksi terbaru, ditata dari yang paling baru."""
with closing(get_connection(path)) as con:
return con.execute(
"SELECT id, tanggal, jenis, kategori, jumlah, catatan"
" FROM transaksi ORDER BY id DESC LIMIT ?",
(batas,),
).fetchall()Mari kita bedah bagian pentingnya:
- Fungsi
insert_transaksi(): seluruh nilai pengguna mengalir lewat placeholder?— prinsip utama Chapter Query Berparameter. String SQL tidak pernah dirangkai dari masukan pengguna, jadi tanda kutip atau--pada catatan tidak bisa mengubah makna query. with con:di dalamclosing(): transaction dikelola otomatis — commit saat sukses, rollback saatsqlite3.IntegrityErrorterjadi, seperti dibahas pada Chapter Transaksi dengan Python. Koneksi tetap ditutup olehclosing()di kedua kemungkinan itu.return cur.lastrowid: cursor menyimpan id baris yang baru disisipkan; nilai ini kita kembalikan agar program bisa melaporkannya ke pengguna.- Fungsi
transaksi_terakhir():ORDER BY id DESCmenata baris terbaru di depan,LIMIT ?membatasi jumlahnya — batas pun dikirim sebagai parameter, bukan digabung ke string SQL. - Fungsi
format_rupiah(): pembantu kecil yang tidak menyentuh SQL sama sekali — ia hanya mengubah5500000menjadiRp5.500.000dengan format ribuan gaya Indonesia. Kita letakkan didb.pykarena akan dipakai dua modul tampilan:main.pysekarang danlaporan.pypada Bagian 3.
main.py: Menu dan Validasi #
Berikut seluruh isi main.py untuk tahap ini. Menu mengikuti pola loop yang sama dengan program terminal pada umumnya: tampilkan pilihan, baca satu baris, arahkan ke tindakannya:
"""Pencatat Keuangan Pribadi — aplikasi CLI berbasis SQLite."""
import sqlite3
from datetime import date
import db
def tampilkan_menu():
print("=== Pencatat Keuangan ===")
print("1. Catat pemasukan")
print("2. Catat pengeluaran")
print("3. Lihat transaksi terakhir")
print("4. Keluar")
def baca_tanggal():
while True:
teks = input("Tanggal (YYYY-MM-DD, Enter = hari ini): ").strip()
if not teks:
return date.today().isoformat()
try:
return date.fromisoformat(teks).isoformat()
except ValueError:
print("Format tanggal harus YYYY-MM-DD, mis. 2025-06-15.")
def baca_jumlah():
while True:
teks = input("Jumlah (Rp): ").strip()
try:
return float(teks)
except ValueError:
print("Jumlah harus berupa angka, mis. 25000.")
def baca_teks(label):
while True:
teks = input(label).strip()
if teks:
return teks
print("Masukan tidak boleh kosong.")
def catat_transaksi(jenis):
if jenis == "masuk":
print("--- Catat Pemasukan ---")
else:
print("--- Catat Pengeluaran ---")
tanggal = baca_tanggal()
kategori = baca_teks("Kategori: ")
jumlah = baca_jumlah()
catatan = input("Catatan (opsional): ").strip()
try:
id_baru = db.insert_transaksi(tanggal, jenis, kategori, jumlah, catatan)
except sqlite3.IntegrityError as e:
print(f"Transaksi ditolak database: {e}")
return
print(f"Transaksi tersimpan dengan id {id_baru}.")
def tampilkan_transaksi():
daftar = db.transaksi_terakhir()
if not daftar:
print("Belum ada transaksi.")
return
print(f"{'id':>3} {'tanggal':<10} {'jenis':<7} {'kategori':<14} {'jumlah':>12}")
for t in daftar:
print(
f"{t['id']:>3} {t['tanggal']:<10} {t['jenis']:<7} "
f"{t['kategori']:<14} {db.format_rupiah(t['jumlah']):>12}"
)
def main():
db.init_db()
while True:
tampilkan_menu()
pilihan = input("Pilihan: ").strip()
if pilihan == "1":
catat_transaksi("masuk")
elif pilihan == "2":
catat_transaksi("keluar")
elif pilihan == "3":
tampilkan_transaksi()
elif pilihan == "4":
print("Sampai jumpa!")
break
else:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()Beberapa keputusan penting di dalamnya:
- Fungsi
baca_tanggal():date.fromisoformat()menolak teks seperti'15-06-2025'maupun'2025-6-5'denganValueError, dan nilai baliknya di-isoformat()ulang agar bentuknya pastiYYYY-MM-DD. Menekan Enter tanpa mengetik apa pun memakaidate.today().isoformat()— pada sesi berikut kita selalu mengetik tanggal tetap agar hasilnya dapat direproduksi. - Fungsi
baca_jumlah():float()menolak teks sepertilima ribu, tetapi menerima-5000. Angka negatif bukan masalah format — ia masalah aturan bisnis. except sqlite3.IntegrityErrorpadacatat_transaksi(): inilah lapis kedua. Saatfloat()menerima-5000, query berjalan sampai database dan CHECK constraintjumlah > 0yang menolaknya. Program menangkapsqlite3.IntegrityError(Chapter Penanganan Error sqlite3), mencetak pesannya, lalu kembali ke menu.- Fungsi
tampilkan_transaksi(): hasilfetchall()berupa daftarsqlite3.Row, sehinggat['kategori']mengambil kolom berdasarkan nama — keuntungan langsung darirow_factoryyang dipasangget_connection(). db.init_db()di awalmain(): berkatIF NOT EXISTS, program bisa dijalankan di folder kosong sekalipun — tabel dibuat pada eksekusi pertama dan dibiarkan utuh pada eksekusi berikutnya.
Menjalankan Program #
Karena program interaktif sulit ditangkap layar, kita meniru ketikan pengguna lewat pipe. Sesi berikut mencatat tiga transaksi — satu pemasukan dan dua pengeluaran — sambil menyisipkan tiga kesalahan ketik: tanggal salah format, jumlah bukan angka, dan jumlah negatif yang ditolak database:
rm -f keuangan.db
echo "1
2025-06-01
Gaji
5500000
Gaji bulanan
2
15-06-2025
2025-06-03
Makan
45000
Makan siang
2
2025-06-05
Transportasi
lima ribu
25000
2
2025-06-07
Hiburan
-5000
3
4" | python3 main.pyOutput yang Dihasilkan:
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: --- Catat Pemasukan ---
Tanggal (YYYY-MM-DD, Enter = hari ini): Kategori: Jumlah (Rp): Catatan (opsional): Transaksi tersimpan dengan id 1.
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: --- Catat Pengeluaran ---
Tanggal (YYYY-MM-DD, Enter = hari ini): Format tanggal harus YYYY-MM-DD, mis. 2025-06-15.
Tanggal (YYYY-MM-DD, Enter = hari ini): Kategori: Jumlah (Rp): Catatan (opsional): Transaksi tersimpan dengan id 2.
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: --- Catat Pengeluaran ---
Tanggal (YYYY-MM-DD, Enter = hari ini): Kategori: Jumlah (Rp): Jumlah harus berupa angka, mis. 25000.
Jumlah (Rp): Catatan (opsional): Transaksi tersimpan dengan id 3.
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: --- Catat Pengeluaran ---
Tanggal (YYYY-MM-DD, Enter = hari ini): Kategori: Jumlah (Rp): Catatan (opsional): Transaksi ditolak database: CHECK constraint failed: jumlah > 0
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: id tanggal jenis kategori jumlah
3 2025-06-05 keluar Transportasi Rp25.000
2 2025-06-03 keluar Makan Rp45.000
1 2025-06-01 masuk Gaji Rp5.500.000
=== Pencatat Keuangan ===
1. Catat pemasukan
2. Catat pengeluaran
3. Lihat transaksi terakhir
4. Keluar
Pilihan: Sampai jumpa!Perhatikan baris keempat: masukan -5000 diterima float(), lolos dari format check Python, lalu ditolak oleh CHECK constraint di database — dua lapis validasi bekerja tepat pada wilayahnya masing-masing. Setelah program berakhir, berkas keuangan.db menyimpan tiga transaksi; eksekusi berikutnya menambah baris baru tanpa mengulang sesi ini.
Program pencatat kini hidup: menu berjalan, transaksi tersimpan, dan tampilan daftar rapi. Seluruh berkas tahap ini tersedia pada bagian Berkas Sumber di bawah halaman. Bagian 3 mengubah tumpukan baris tersebut menjadi jawaban: berapa saldo bulan ini dan ke mana uangnya pergi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
db.py
Unduh berkas"""Modul akses data Pencatat Keuangan Pribadi.
Satu-satunya tempat yang membuka koneksi database dan menjalankan SQL;
modul lain cukup memanggil fungsi-fungsi di bawah.
"""
import sqlite3
from contextlib import closing
from pathlib import Path
NAMA_DB = "keuangan.db"
BERKAS_SKEMA = Path(__file__).with_name("schema.sql")
def get_connection(path=NAMA_DB):
"""Buka koneksi baru dengan pengaturan seragam untuk seluruh aplikasi."""
con = sqlite3.connect(path)
con.row_factory = sqlite3.Row
con.execute("PRAGMA foreign_keys = ON")
con.execute("PRAGMA busy_timeout = 5000")
return con
def init_db(path=NAMA_DB):
"""Jalankan schema.sql; aman dipanggil berulang karena memakai IF NOT EXISTS."""
skema = BERKAS_SKEMA.read_text(encoding="utf-8")
with closing(get_connection(path)) as con:
con.executescript(skema)
def format_rupiah(jumlah):
"""Format angka menjadi teks rupiah, mis. 5500000 menjadi Rp5.500.000."""
return f"Rp{jumlah:,.0f}".replace(",", ".")
def insert_transaksi(tanggal, jenis, kategori, jumlah, catatan="", path=NAMA_DB):
"""Simpan satu transaksi, kembalikan id baris baru."""
with closing(get_connection(path)) as con:
with con:
cur = con.execute(
"INSERT INTO transaksi (tanggal, jenis, kategori, jumlah, catatan)"
" VALUES (?, ?, ?, ?, ?)",
(tanggal, jenis, kategori, jumlah, catatan),
)
return cur.lastrowid
def transaksi_terakhir(batas=10, path=NAMA_DB):
"""Ambil sejumlah transaksi terbaru, ditata dari yang paling baru."""
with closing(get_connection(path)) as con:
return con.execute(
"SELECT id, tanggal, jenis, kategori, jumlah, catatan"
" FROM transaksi ORDER BY id DESC LIMIT ?",
(batas,),
).fetchall()
main.py
Unduh berkas"""Pencatat Keuangan Pribadi — aplikasi CLI berbasis SQLite."""
import sqlite3
from datetime import date
import db
def tampilkan_menu():
print("=== Pencatat Keuangan ===")
print("1. Catat pemasukan")
print("2. Catat pengeluaran")
print("3. Lihat transaksi terakhir")
print("4. Keluar")
def baca_tanggal():
while True:
teks = input("Tanggal (YYYY-MM-DD, Enter = hari ini): ").strip()
if not teks:
return date.today().isoformat()
try:
return date.fromisoformat(teks).isoformat()
except ValueError:
print("Format tanggal harus YYYY-MM-DD, mis. 2025-06-15.")
def baca_jumlah():
while True:
teks = input("Jumlah (Rp): ").strip()
try:
return float(teks)
except ValueError:
print("Jumlah harus berupa angka, mis. 25000.")
def baca_teks(label):
while True:
teks = input(label).strip()
if teks:
return teks
print("Masukan tidak boleh kosong.")
def catat_transaksi(jenis):
if jenis == "masuk":
print("--- Catat Pemasukan ---")
else:
print("--- Catat Pengeluaran ---")
tanggal = baca_tanggal()
kategori = baca_teks("Kategori: ")
jumlah = baca_jumlah()
catatan = input("Catatan (opsional): ").strip()
try:
id_baru = db.insert_transaksi(tanggal, jenis, kategori, jumlah, catatan)
except sqlite3.IntegrityError as e:
print(f"Transaksi ditolak database: {e}")
return
print(f"Transaksi tersimpan dengan id {id_baru}.")
def tampilkan_transaksi():
daftar = db.transaksi_terakhir()
if not daftar:
print("Belum ada transaksi.")
return
print(f"{'id':>3} {'tanggal':<10} {'jenis':<7} {'kategori':<14} {'jumlah':>12}")
for t in daftar:
print(
f"{t['id']:>3} {t['tanggal']:<10} {t['jenis']:<7} "
f"{t['kategori']:<14} {db.format_rupiah(t['jumlah']):>12}"
)
def main():
db.init_db()
while True:
tampilkan_menu()
pilihan = input("Pilihan: ").strip()
if pilihan == "1":
catat_transaksi("masuk")
elif pilihan == "2":
catat_transaksi("keluar")
elif pilihan == "3":
tampilkan_transaksi()
elif pilihan == "4":
print("Sampai jumpa!")
break
else:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()
schema.sql
Unduh berkas-- Skema Pencatat Keuangan Pribadi.
-- Satu tabel transaksi menampung pemasukan dan pengeluaran sekaligus.
CREATE TABLE IF NOT EXISTS transaksi (
id INTEGER PRIMARY KEY,
tanggal TEXT NOT NULL CHECK (tanggal IS date(tanggal)),
jenis TEXT NOT NULL CHECK (jenis IN ('masuk', 'keluar')),
kategori TEXT NOT NULL,
jumlah REAL NOT NULL CHECK (jumlah > 0),
catatan TEXT NOT NULL DEFAULT ''
);