Pada Bagian 1 kita mencatat transaksi tetapi tidak tahu berapa banyak yang seharusnya dibelanjakan per kategori. Bagian 2 menambahkan anggaran bulanan per kategori, lalu membandingkannya dengan realisasi. Dengan anggaran, pengguna dapat melihat kategori mana yang melampaui batas dan menyesuaikan kebiasaan belanja.

Konsep Anggaran #

Anggaran adalah pasangan (bulan, kategori) dengan batas nominal. Contoh: anggaran makan bulan 2026-01 adalah 1.000.000. Setiap transaksi pengeluaran dengan kategori makan di bulan tersebut akan mengurangi sisa anggaran. Jika total pengeluaran melebihi batas, status menjadi LEBIH.

Implementasi Buku Keuangan dengan Anggaran #

Kode berikut memperluas BukuKeuangan dengan anggaran dan laporan perbandingan:

kotlin
// Bagian 2: Kategori transaksi dan anggaran bulanan.
// Mendemonstrasikan pengelompokan transaksi berdasarkan kategori dan
// perbandingan realisasi vs anggaran.

enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }

data class Transaksi(
    val id: String,
    val jenis: JenisTransaksi,
    val jumlah: Long,
    val kategori: String,
    val bulan: String
)

data class Anggaran(val bulan: String, val kategori: String, val batas: Long)

class BukuKeuangan {
    private val transaksi = mutableListOf<Transaksi>()
    private val anggaran = mutableMapOf<Pair<String, String>, Long>()

    fun tambahTransaksi(t: Transaksi) { transaksi.add(t) }

    fun tetapkanAnggaran(bulan: String, kategori: String, batas: Long) {
        require(batas > 0) { "Batas anggaran harus positif" }
        anggaran[bulan to kategori] = batas
    }

    fun realisasiPerKategori(bulan: String): Map<String, Long> {
        return transaksi
            .filter { it.bulan == bulan && it.jenis == JenisTransaksi.PENGELUARAN }
            .groupBy { it.kategori }
            .mapValues { (_, list) -> list.sumOf { it.jumlah } }
    }

    fun laporanAnggaran(bulan: String): List<Triple<String, Long, Long>> {
        val realisasi = realisasiPerKategori(bulan)
        val kategoriTerpakai = (anggaran.keys.map { it.second }.toSet() + realisasi.keys).sorted()
        return kategoriTerpakai.map { k ->
            Triple(k, anggaran[bulan to k] ?: 0L, realisasi[k] ?: 0L)
        }
    }
}

fun formatRupiah(nilai: Long): String {
    val tanda = if (nilai < 0) "-" else ""
    val str = kotlin.math.abs(nilai).toString()
    return "${tanda}Rp" + str.reversed().chunked(3).joinToString(".").reversed()
}

fun main() {
    val buku = BukuKeuangan()
    val bulan = "2026-01"
    buku.tambahTransaksi(Transaksi("T001", JenisTransaksi.PEMASUKAN, 5_000_000, "gaji", bulan))
    buku.tambahTransaksi(Transaksi("T002", JenisTransaksi.PENGELUARAN, 750_000, "makan", bulan))
    buku.tambahTransaksi(Transaksi("T003", JenisTransaksi.PENGELUARAN, 1_200_000, "transport", bulan))
    buku.tambahTransaksi(Transaksi("T004", JenisTransaksi.PENGELUARAN, 500_000, "makan", bulan))
    buku.tambahTransaksi(Transaksi("T005", JenisTransaksi.PENGELUARAN, 600_000, "hiburan", bulan))

    buku.tetapkanAnggaran(bulan, "makan", 1_000_000)
    buku.tetapkanAnggaran(bulan, "transport", 1_000_000)
    buku.tetapkanAnggaran(bulan, "hiburan", 400_000)

    println("=== Laporan Anggaran Bulan $bulan ===")
    println("%-12s %-15s %-15s %s".format("Kategori", "Anggaran", "Realisasi", "Sisa"))
    buku.laporanAnggaran(bulan).forEach { (k, anggaran, realisasi) ->
        val sisa = anggaran - realisasi
        val tanda = if (sisa >= 0) "OK" else "LEBIH"
        println("%-12s %-15s %-15s %-10s %s".format(
            k, formatRupiah(anggaran), formatRupiah(realisasi), formatRupiah(sisa), tanda
        ))
    }
    println()
    println("=== Kategori tanpa anggaran ===")
    buku.realisasiPerKategori(bulan).forEach { (k, v) ->
        if (buku.laporanAnggaran(bulan).find { it.first == k }?.second == 0L) {
            println("$k: ${formatRupiah(v)} (tanpa anggaran)")
        }
    }
}

Output yang Dihasilkan:

text
=== Laporan Anggaran Bulan 2026-01 ===
Kategori     Anggaran        Realisasi       Sisa
hiburan      Rp400.000       Rp600.000       -Rp200.000 LEBIH
makan        Rp1.000.000     Rp1.250.000     -Rp250.000 LEBIH
transport    Rp1.000.000     Rp1.200.000     -Rp200.000 LEBIH

=== Kategori tanpa anggaran ===

Mari kita bedah komponen kunci:

  1. mutableMapOf<Pair<String, String>, Long>(): key berupa Pair bulan dan kategori. Map memungkinkan pencarian cepat berdasarkan kombinasi tersebut.
  2. bulan to kategori: bentuk ringkas Pair(bulan, kategori) menggunakan infix function to. Lebih mudah dibaca daripada Pair(bulan, kategori).
  3. groupBy { it.kategori }.mapValues { ... }: dua langkah agregasi. groupBy menghasilkan Map<String, List<Transaksi>>, mapValues menjumlahkan setiap list.
  4. (anggaran.keys.map { it.second }.toSet() + realisasi.keys).sorted(): union kategori dari anggaran dan realisasi, agar laporan menampilkan keduanya meski salah satu tidak punya pasangan.
  5. anggaran[bulan to k] ?: 0L: operator Elvis memberi nol jika tidak ada anggaran untuk kategori tersebut.
  6. "%-12s %-15s".format(...): format string dengan lebar kolom tetap, menghasilkan tabel rapi.

Mengapa Map<Pair, Long> #

Penyimpanan anggaran sebagai Map<Pair<String, String>, Long> membuat lookup cepat: anggaran["2026-01" to "makan"] langsung memberi nilai. Alternatif Map<String, Map<String, Long>> (bulan -> kategori -> batas) lebih nested tetapi memerlukan dua langkah lookup. Pilihan tergantung preferensi; untuk belajar, Pair lebih sederhana.

Laporan Anggaran vs Realisasi #

Laporan menampilkan tiga kolom utama: anggaran, realisasi, dan sisa. Status OK atau LEBIH ditambahkan agar pengguna langsung melihat masalah. Untuk anggaran tanpa realisasi, sisa sama dengan anggaran (tidak ada pengeluaran). Untuk realisasi tanpa anggaran, kolom anggaran kosong dan laporan menandai “tanpa anggaran”.

Referensi Lanjutan #

Ringkasan Bagian 2 #

Anggaran bulanan per kategori kini berfungsi: tetapkanAnggaran() menyimpan batas, realisasiPerKategori() menghitung pengeluaran aktual, dan laporanAnggaran() menampilkan perbandingan. Status OK/LEBIH memberi sinyal cepat. Pada Bagian 3 kita menambah analisis persentase untuk melihat proporsi pengeluaran per kategori.