Merancang Laporan Master-Detail Tanpa Subreport

Pengenalan

Pada artikel ini, saya akan membuat sebuah laporan master-detail dengan iReport. Sebagai latihan, laporan master-detail yang akan saya buat adalah laporan piutang pelanggan dimana dalam setiap piutang disertakan informasi pembayaran yang telah dilakukan. Laporan akan dikelompokkan berdasarkan setiap pelanggan. Sebagai contoh, berikut ini adalah domain model yang sudah ada pada aplikasi:

Domain model yang akan dipakai dalam laporan

Domain model yang akan dipakai dalam laporan

Karena saya sudah memiliki domain model, maka saya akan memakai instance dari domain model tersebut secara langsung pada laporan.

Langkah pertama yang saya lakukan adalah membuat laporan baru di iReport dengan memilih menu File, New. Saya akan memberinya nama berupa laporan_sisa_piutang. Setelah laporan baru selesai dibuat, saya perlu membuat data source yang mewakili data yang akan ditampilkan dalam laporan nantinya. Tapi sebelumnya, saya memastikan terlebih dahulu bahwa JAR untuk proyek saya sudah terdaftar dengan memilih menu Tools, Options, Classpath.

Menyiapkan Data Source

Saya menambahkan data source baru dengan men-klik tombol Report Datasources seperti pada gambar berikut ini:

Membuat data source baru

Membuat data source baru

Pada dialog yang muncul, saya men-klik tombol New. Saya kemudian memilih JavaBeans set datasource. Saya akan menggunakan kode program Groovy berikut ini untuk menghasilkan data random yang dipakai dalam men-preview laporan:

import domain.penjualan.*
import domain.faktur.*
import org.joda.time.*
import java.util.Random

void tambahFaktur(Konsumen k) {
   Random r = new Random()
   (r.nextInt(10)+1).times {
       FakturJualOlehSales f = new FakturJualOlehSales(tanggal: LocalDate.now(), status: StatusFakturJual.DITERIMA)
       f.nomor = "F${r.nextInt(1000)}"
       def jumlahPiutang = r.nextInt(10000000)
       f.piutang = new KewajibanPembayaran(jumlah: jumlahPiutang)
       def jumlahPembayaran = r.nextInt(10)
       (r.nextInt(10)+1).times {
          Pembayaran p = new Pembayaran(tanggal: LocalDate.now(), 
             jumlah: r.nextInt((int)f.sisaPiutang(false)))
          if (r.nextInt(10) > 5)  {
             p.bilyetGiro = new BilyetGiro(nomorSeri: 'AA-123')
          }
          if (r.nextInt(10) > 8) {
             p.potongan = true
          }
          f.piutang.bayar(p)   
       }

       k.listFakturBelumLunas << f
    }
}

Konsumen k1 = new Konsumen(nama: 'Konsumen A', listFakturBelumLunas: [])
Konsumen k2 = new Konsumen(nama: 'Konsumen B', listFakturBelumLunas: [])
Konsumen k3 = new Konsumen(nama: 'Konsumen C', listFakturBelumLunas: [])

tambahFaktur(k1)
tambahFaktur(k2)
tambahFaktur(k3)

[k1,k2,k3]

Untuk kebutuhan pribadi, saya sempat melakukan modifikasi pada iReport (di https://github.com/JockiHendry/ireport-fork) agar bisa memasukkan kode program Groovy secara langsung seperti pada gambar berikut ini:

Memasukkan kode program pada saat membuat data source

Memasukkan kode program pada saat membuat data source

Bila memakai iReport yang resmi, saya harus membuat kode program Groovy di atas pada proyek yang sudah ada, kemudian menyertakan nama class dan nama method yang mengembalikan Collection di dialog ini.

Saya kemudian men-klik tombol Test untuk memastikan bahwa kode program dapat berjalan secara lancar. Setelah itu, saya men-klik tombol Save.

Berikutnya, saya men-klik tombol Report Query seperti pada gambar berikut ini:

gambar4

Pada dialog yang muncul, saya memilih tab JavaBean Datasource. Kemudian saya mengisi Class name dengan domain.penjualan.Konsumen dan men-klik tombol Read attributes. Karena saya merancang domain model berdasarkan aturan di domain driven design, maka saya hanya perlu men-query root aggregate secara langsung. Pada contoh ini, Konsumen adalah root aggregate yang memiliki informasi piutang dan pembayarannya (dalam bentuk atribut). Oleh sebab itu, saya menambahkan beberapa atribut yang dibutuhkan dengan memilihnya dan men-klik Add selected field(s) seperti pada gambar berikut ini:

Membuat report query

Membuat report query

Setelah selesai, saya men-klik tombol Ok.

Pada laporan ini, terdapat 3 hierarki informasi yang hendak ditampilkan: pelanggan (diwakili class Konsumen), piutang untuk masing-masing pelanggan (diwakili oleh Konsumen.listFakturJualBelumLunas), dan pembayaran untuk masing-masing piutang (diwakili oleh Konsumen.listFakturJualBelumLunas.piutang.listPembayaran). Bila menggunakan fasilitas subreport, saya perlu membuat 3 laporan berbeda dan memanggilnya dalam sebuah laporan. Cara ini cukup merepotkan dan sulit dikelola. Oleh sebab itu, pada artikel ini, saya akan memakai cara yang lebih sederhana tanpa perlu membuat subreport, dengan menggunakan fasilitas List component dan Table component dari JasperReports.

Menampilkan Pelanggan

Ini adalah tugas paling mudah. Saya hanya perlu men-drag beberapa field sehingga menghasilkan rancangan seperti pada gambar berikut ini:

Rancangan laporan untuk pelanggan

Rancangan laporan untuk pelanggan

Bila saya men-preview laporan, yang saya dapatkan hanya informasi pelanggan seperti pada gambar berikut ini:

Hasil preview laporan

Hasil preview laporan

Menampilkan Piutang Untuk Masing-Masing Pelanggan

Untuk menampilkan piutang per pelanggan, saya akan menggunakan List component yang dapat ditemukan di palette seperti berikut ini:

List component

List component

Setelah men-drag komponen tersebut ke laporan, iReport akan membuatkan sebuah dataset baru dengan nama dataset1 yang terlihat seperti pada gambar berikut ini:

Dataset yang dihasilkan iReport

Dataset yang dihasilkan iReport

Saya akan mengubah nama dataset tersebut menjadi dsPiutang dan menambahkan beberapa attribute milik class FakturJualOlehSales sehingga dataset tersebut terlihat seperti pada gambar berikut ini:

Dataset yang sudah diubah

Dataset yang sudah diubah

Berikutnya, saya men-klik kanan pada List component yang ada di laporan dan memilih menu Edit list datasource. Saya kemudian mengisi kotak dialog yang muncul sehingga terlihat seperti pada gambar berikut ini:

Mengisi expression untuk data source

Mengisi expression untuk data source

Setelah itu, saya bisa men-drag field yang dibutuhkan kedalam List component seperti yang terlihat pada gambar berikut ini:

Rancangan laporan dengan List component berisi piutang

Rancangan laporan dengan List component berisi piutang

Sekarang, bila saya men-preview laporan, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil preview laporan

Hasil preview laporan

Menampilkan Pembayaran Untuk Masing-Masing Piutang

Untuk menampilkan daftar pembayaran per piutang/faktur, saya akan menggunakan Table component. Untuk itu, saya men-drag Table component dari palette ke dalam List component yang sudah ada. Table component memiliki icon yang terlihat seperti pada gambar berikut ini:

Table component

Table component

Pada table wizard yang muncul, saya men-klik tombol New dataset. Saya kemudian mengisi nama dataset dengan dsPembayaran, memilih Create an empty dataset, lalu men-klik tombol Finish. Saya kemudian men-klik tombol Next dua kali. Pada step ke-3, saya memilih Use a JRDataSource expression dan mengisi ekspresi-nya dengan:

new net.sf.jasperreports.engine.data.
JRBeanCollectionDataSource($F{piutang}.listPembayaran)

Saya men-klik tombol Next dan melakukan pengaturan seadanya di step ke-4 sebelum akhirnya men-klik tombol Finish.

Selanjutnya, saya menambahkan attribut milik Pembayaran pada dsPembayaran sehingga dataset tersebut terlihat seperti pada gambar berikut ini:

Dataset yang telah diubah

Dataset yang telah diubah

Sekarang saya siap untuk merancang tabel. Agar dapat merancang tabel, saya perlu beralih ke rancangan tabel dengan men-klik Table 1 di bagian bawah layar seperti yang terlihat pada gambar berikut ini:

Mengubah ke modus rancangan tabel

Mengubah ke modus rancangan tabel

Setelah itu, saya membuat rancangan tabel seperti pada gambar berikut ini:

Contoh rancangan tabel

Contoh rancangan tabel

Setelah rancangan selesai, saya dapat kembali halaman utama laporan dengan men-klik tombol Main report di bagian bawah layar. Saya dapat melakukan perubahan lagi agar laporan terlihat rapi. Hasil akhirnya, bila saya men-preview laporan, sekarang laporan akan terlihat seperti:

Hasil preview laporan

Hasil preview laporan

Laporan master-detail berhasil ditampilkan dengan baik cukup dengan satu laporan tunggal tanpa harus melibatkan subreport.

Belajar Merancang Laporan JasperReports Dengan iReport

iReport membuat perancangan layout laporan JasperReports menjadi mudah.   Sebagai latihan, saya akan belajar membuat sebuah laporan penjualan yang dikelompokkan berdasarkan nama sales.   Pengguna dapat memilih untuk melihat laporan berdasarkan periode tertentu (tanggal mulai dan tanggal selesai).   Karena saya memakai JPA, maka saya akan melewatkan kumpulan object (entitas) yang telah di-query oleh JPA ke laporan.   Dengan demikian, laporan tidak akan mengerjakan SQL secara langsung.   Lalu, saya harus mulai dari mana?

Agar dapat melihat preview laporan di iReport,  saya perlu membuat sebuah method sederhana yang akan mengembalikan contoh object yang bisa dipakai untuk preview nantinya.   Sebagai contoh,  saya menambahkan method berikut ini di class bernama ReportTest.groovy:

static List getPenjualanPerSales() {
    Sales sales1 = new Sales(nama: 'Solid Snake')
    Sales sales2 = new Sales(nama: 'Liquid Snake')
    Sales sales3 = new Sales(nama: 'Big Boss')

    DateTimeFormatter format = DateTimeFormat.forPattern('dd-MM-yyyy')
    Barang barang1 = new Barang(kode: "BRG1")
    Barang barang2 = new Barang(kode: "BRG2")

    FakturJual jual1 = new FakturJual(nomor: "FJ-001", tanggal: LocalDate.parse('01-07-2013', format),
        sales: sales1)
    jual1.tambahItem(barang1, 10000, 10)
    jual1.tambahItem(barang2, 20000, 20)

    FakturJual jual2 = new FakturJual(nomor: "FJ-002", tanggal: LocalDate.parse('15-07-2013', format),
            sales: sales1)
    jual2.tambahItem(barang1, 10000, 30)
    jual2.tambahItem(barang2, 20000, 40)

    FakturJual jual3 = new FakturJual(nomor: "FJ-003", tanggal: LocalDate.parse('25-07-2013', format),
            sales: sales2)
    jual3.tambahItem(barang1, 10000, 50)
    jual3.tambahItem(barang2, 20000, 60)

    FakturJual jual4 = new FakturJual(nomor: "FJ-004", tanggal: LocalDate.parse('01-08-2013', format),
            sales: sales3)
    jual4.tambahItem(barang1, 10000, 70)
    jual4.tambahItem(barang2, 20000, 80)

    // Penjualan tanpa sales
    FakturJual jual5 = new FakturJual(nomor: "FJ-005", tanggal: LocalDate.parse('15-08-2013', format))
    jual5.tambahItem(barang1, 10000, 10)
    jual5.tambahItem(barang2, 20000, 20)

    [jual1, jual2, jual3, jual4, jual5]
}

Perhatikan bahwa method tersebut harus berupa method static.   Object yang dikembalikan tidak perlu diambil dari database; mereka hanya akan dipakai untuk men-preview laporan selama berada di iReport nanti.   Saya perlu membuat JAR dari kode program di atas lalu menambahkannya di iReport dengan memilih menu Tools, Options, ClassPath dan men-klik tombol Add JAR.   Lalu, saya men-klik tombol Report Datasources dan men-klik tombol New.   Pada kotak dialog yang muncul, saya memilih JavaBeans set data source dan men-klik tombol Next.   Saya kemudian mengisi kotak dialog berikutnya seperti pada gambar berikut ini:

Menambahkan data source baru

Menambahkan data source baru

Untuk memastikan tidak ada yang salah, saya men-klik tombol Test.   Setelah selesai,  saya men-klik tombol Save dan menutup dialog dengan men-klik tombol Close.

Selanjutnya, saya akan membuat laporan baru, dengan memilih menu File, New…, Blank A4 dan men-klik Open this Template.   Pada kotak dialog yang muncul, saya kemudian mengisi nama laporan dan lokasi penyimpan laporan.   Setelah itu, saya men-klik tombol Next dan Finish.

Saya kemudian men-klik tombol Report Query dan memilih tab JavaBean Datasource.   Saya mengisi seperti dengan yang terlihat pada gambar berikut ini sebelum men-klik tombol OK:

Mengisi report query

Mengisi report query

Agar dapat memanggil method pada object Barang, saya perlu menambahkan sebuah field baru dengan nama _THIS.   Ini adalah nama khusus untuk field dimana nilainya akan mewakili object yang sedang diproses saat ini (bayangkan keyword this di Java!).   Saya mulai dengan men-klik kanan Fields dan memilih Add Field.   Setelah itu saya mengisi properties-nya seperti pada gambar berikut ini:

Menambah field _THIS

Menambah field _THIS

Laporan perlu dikelompokkan berdasarkan nama sales sehingga saya dapat menampilkan subtotal untuk setiap sales nantinya.   Oleh sebab itu, saya men-klik kanan nama laporan, dan memilih Add Report Group seperti yang terlihat pada gambar berikut ini:

Membuat Report Group

Membuat Report Group

Saya mengisi dialog yang muncul seperti pada gambar berikut ini:

Menambah Group Sales

Menambah Group Sales

Saya kemudian men-klik tombol Next.   Pada halaman ini, saya menghilangkan tanda centang pada Add the group header.   Setelah itu, saya men-klik tombol Finish.

Sekarang saya dapat membuat judul laporan yang harus ada di setiap halaman.   Pada judul halaman ini, akan terdapat informasi periode laporan.   Karena nilai periode laporan adalah sesuatu yang tidak statis, maka saya perlu membuat parameter baru.   Caranya adalah dengan memilih Parameters di windows Report Inspector dan memilih menu Add Parameter.   Kemudian saya mengisi properties seperti yang terlihat pada gambar berikut ini:

Menambah parameter baru

Menambah parameter baru

Berikut ini adalah contoh rancangan pada band Page Header yang saya buat:

Rancangan PageHeader

Rancangan PageHeader

Untuk memberikan informasi halaman, saya menggunakan ekspresi Groovy berikut ini:

"Hal ${$V{PAGE_NUMBER}}"

Prefix $V adalah cara di JasperReports untuk mengakses sebuah variabel.   PAGE_NUMBER adalah nama sebuah variabel bawaan yang nilainya berupa nomor halaman saat ini.

Untuk menampilkan informasi periode, saya menggunakan ekspresi Groovy berikut ini:

"Periode: ${$P{tanggalMulaiCari}.toString('dd-MM-yyyy')} s/d ${$P{tanggalSelesaiCari}.toString('dd-MM-yyyy')}"

Prefix $P di JasperReports dipakai untuk mengakses nilai dari parameter.   Berbeda dengan variabel, parameter adalah nilai yang dilewatkan oleh pengguna saat menampilkan laporan.   Nilai dari sebuah parameter tidak akan bisa diketahui bila laporan belum di-eksekusi.

Berikutnya, saya mengisi band Column Header dan Detail 1 sehingga terlihat seperti pada gambar berikut ini:

Rancangan Detail Laporan

Rancangan Detail Laporan

Untuk mengakses nilai field di JasperReports, saya dapat menggunakan prefix $F.   Bila data source berupa JavaBean, maka $F akan mengembalikan nilai property dari object (sebuah object mewakili sebuah record/baris).   Sebagai contoh, $F{nomor} akan mengembalikan nilai dari getNomor() untuk object bersangkutan.

Saya memakai ekspresi Groovy berikut ini untuk menampilkan kolom sales:

$F{sales}? $F{sales}.nama: "(tanpa sales)"

Nilai property sales bisa saja berupa null untuk penjualan secara langsung.   Ekspresi di atas akan mengembalikan nama sales bila terdapat nilai sales; bila nilai sales adalah null, maka ia akan mengembalikan tulisan ‘(tanpa sales)’.

Saya memakai ekspresi Groovy berikut ini untuk menampilkan tanggal:

$F{tanggal}.toString('dd-MM-yyyy')

Karena property tanggal di class FakturJual bertipe org.joda.time.LocalDate, maka saya dapat memanggil method toString() (bawaan Joda Time) untuk men-format tanggal.

Class FakturJual memiliki sebuah method bernama total() yang akan mengembalikan nilai transaksi untuk faktur tersebut.   Untuk memanggil method dari object yang sedang diproses, saya dapat memakai $F{_THIS}.   Ekspresi ini mirip seperti keyword this di Java yang akan mengembalikan object pada konteks yang sedang aktif.   Dengan demikian, untuk memanggil method total(), saya dapat menggunakan ekspresi berikut ini:

$F{_THIS}.total()

Kolom total berisi angka dengan tipe BigDecimal.   Alangkah baiknya bila angka ini diformat, misalnya dengan pemisah ribuan berupa titik.   Untuk itu, saya men-klik kanan pada field total, kemudian memilih Field Pattern.   Pada dialog yang muncul, saya memilih Number, mengisi Decimal places dengan 0, dan memberikan tanda centang pada Use 1000 separator.   Setelah itu, saya men-klik tombol Apply.

Tujuan awal saya melakukan grouping berdasarkan sales adalah agar dapat menampilkan subtotal untuk masing-masing sales.   Untuk menghitung subtotal per sales, saya perlu menambahkan variables baru.   Saya men-klik kanan pada Variables, kemudian memilih Add Variables. Gambar berikut ini memperlihatkan variables yang saya tambahkan:

Menambahkan variabel subtotal

Menambahkan variabel subtotal

Setelah itu saya menambahkan rancangan untuk band sales Group Footer 1 seperti yang terlihat pada gambar berikut ini:

Rancangan Group Footer

Rancangan Group Footer

Band Group Footer 1 tersebut hanya memiliki dua TextField.   Yang pertama berisi label keterangan, dan yang satunya lagi menampilkan isi variabel $V{subtotal}.   Khusus untuk TextField yang berisi label keterangan, saya memakai ekspresi seperti berikut ini:

$F{sales}?"Subtotal Untuk Sales ${$F{sales}.nama}: ":
"Subtotal Untuk Penjualan Tanpa Sales: "

Terakhir, saya akan menampilkan nilai grand total penjualan.   Untuk itu saya kembali membuat sebuah Variables baru seperti yang ditunjukkan pada gambar berikut ini:

Menambahkan variabel grandTotal

Menambahkan variabel grandTotal

Setelah itu, saya menampilkan rancangan berikut ini pada band Summary:

Rancangan Summary

Rancangan Summary

Bila saya men-preview laporan, saya akan memperoleh tampilan seperti berikut ini:

Hasil Preview Laporan

Hasil Preview Laporan

Perhatikan bahwa untuk setiap group, nama sales yang sama akan tetap akan dicetak di kolom sales.   Bila hal ini dirasa menganggu, saya dapat menghilangkan nama sales yang sama dengan memilih field tersebut, kemudian pada window Properties, saya menghilangkan tanda centang pada Print Repeated Values.   Sekarang hasil preview laporan akan terlihat seperti pada gambar berikut ini:

Hasil Preview Laporan  Setelah Menghilangkan Nilai Duplikat Pada Sales

Hasil Preview Laporan Setelah Menghilangkan Nilai Duplikat Pada Sales

Sekarang saya hanya perlu menampilkan laporan tersebut di aplikasi.   Bila seandainya saya memakai Griffon dan simple-jpa, maka cara terbaik adalah meletakkan query sebagai JPA named query.   Sebagai contoh, pada class FakturJual, saya menambahkan named query berikut ini:

@DomainModel @Entity @Canonical
@NamedQuery(name="FakturJual.RekapPenjualanSales", query='''
  FROM FakturJual f
  WHERE (f.tanggal BETWEEN :tanggalMulaiCari AND :tanggalSelesaiCari)
    AND (f.hapus <> TRUE) AND TYPE(f) IS FakturJual ORDER BY f.sales, f.tanggal
''')
class FakturJual {
   ...

}

Untuk mengerjakan di atas melalui simple-jpa, saya perlu memanggil method dengan nama doRekapPenjualanSalesOnFakturJual dan melewatkan sebuah Map yang berisi nilai parameter tanggalMulaiCari dan tanggalSelesaiCari.

Karena biasanya jenis laporan ditampilkan melalui sebuah ComboBox yang dapat dipilih, maka saya dapat membuat enumeration seperti berikut ini:

enum JenisLaporan {
    REKAP_PEMBELIAN("Rekap Pembelian", "laporan_rekap_pembelian", "FakturBeli", "RekapPembelian"),
    REKAP_PENJUALAN("Rekap Penjualan", "laporan_rekap_penjualan", "FakturJual", "RekapPenjualan"),
    REKAP_PENJUALAN_SALES("Rekap Penjualan Per Sales", "laporan_rekap_penjualan_sales", "FakturJual", "RekapPenjualanSales"),

    String keterangan
    String namaLaporan
    String domainClass
    String reportMethod

    public JenisLaporan(String keterangan, String namaLaporan, String domainClass, String reportMethod) {
        this.keterangan = keterangan
        this.namaLaporan = namaLaporan
        this.domainClass = domainClass
        this.reportMethod = reportMethod
    }

    @Override
    String toString() {
        keterangan
    }
}

SwingX memiliki EnumComboBoxModel yang memungkinkan untuk mengisi sebuah ComboBox berdasarkan elemen yang ada di sebuah Enumeration:

class ReportModel {

    @Bindable LocalDate tanggalMulaiCari
    @Bindable LocalDate tanggalSelesaiCari

    EnumComboBoxModel<JenisLaporan> jenisLaporanSearch = new EnumComboBoxModel<JenisLaporan>(JenisLaporan.class)
}
Tampilan View Untuk Memilih Laporan

Tampilan View Untuk Memilih Laporan

Pada controller, saya dapat membuat sebuah method universal yang akan menampilkan laporan berdasarkan elemen yang terpilih di ComboBox, misalnya seperti berikut ini:

def search = {
    JenisLaporan jenisLaporan = model.jenisLaporanSearch.selectedItem
    Map parameter = ['tanggalMulaiCari': model.tanggalMulaiCari, 'tanggalSelesaiCari': model.tanggalSelesaiCari]
    List source = "do${jenisLaporan.reportMethod}On${jenisLaporan.domainClass}"(parameter)
    JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(source)
    JasperPrint jasperPrint = JasperFillManager.fillReport(
        getResourceAsStream("report/${jenisLaporan.namaLaporan}.jasper"),
        parameter, dataSource)
    execInsideUIAsync {
        view.content.clear()
        view.content.add(new JRViewer(jasperPrint), BorderLayout.CENTER)
    }
}

Pada kode program di atas, saya memakai fasilitas dinamis dari Groovy untuk memanggil method simple-jpa berdasarkan String yang dibentuk dari JenisLaporan terpilih.   Misalnya, bila user memilih REKAP_PENJUALAN, maka method simple-jpa yang dikerjakan adalah doRekapPenjualanOnFakturJual(); bila user memilih REKAP_PEMBELIAN, maka method simple-jpa yang dikerjakan adalah doRekapPembelianOnFakturBeli(); dan sebagainya.   Dengan cara seperti ini, bila saya ingin menambahkan sebuah laporan baru, maka saya hanya perlu menambahkan sebuah elemen baru di enumeration JenisLaporan tanpa mengubah kode program di controller dan view.

Mencetak ‘Object’ Dengan JasperReports: Merancang Laporan

Sesuai dengan paradigma pemograman berorientasi object (OOP),  saya meletakkan business logic ke dalam class.   Sebagai contoh, class Faktur dan ItemFaktur memiliki Diskon yang didalamnya mengandung rumus perhitungan diskon (baik untuk diskon berupa persen maupun potongan langsung).  UML Class Diagram berikut ini menunjukkan contoh rancangannya:

Contoh Rancangan OOP

Contoh Rancangan OOP

Selain business logic, sebuah sistem juga memiliki application logic seperti mencetak laporan, mengirim email, scheduling, dsb.  Saat ini saya perlu mencetak sebuah object Faktur ke printer, misalnya sebagai bukti pembayaran.   Apakah saya bisa langsung mencetak sebuah object?  Biasanya sebuah laporan selalu identik dengan hasil query dari database.   Tapi bila memakai SQL, maka saya tidak bisa memakai perhitungan yang sudah ada di class.   Seandainya saya bisa langsung mencetak object, maka akan lebih baik lagi karena business logic yang berkaitan dengan object tersebut akan ikut terbawa.  Apakah bisa?

Yup, bisa!  JasperReports dapat dipakai untuk keperluan ini.  Apa itu JasperReports?  JasperReports adalah sebuah library Java yang memungkinkan untuk menghasilkan dokumen pixel-perfect yang nantinya dapat ditampilkan, dicetak atau di-ekspor ke dalam format lain (seperti PDf atau Excel).   Pixel-perfect berarti dokumen yang dihasilkan memiliki elemen dengan posisi dan ukuran sesuai yang ditentukan; pixel-perfect merupakan kebalikan dari grid-based layout HTML <table> dimana posisi dan ukuran bersifat relatif.

Gambar berikut ini menunjukkan cara kerja JasperReports (diambil dari dokumentasi resminya):

Arsitektur JasperReports (Dari JasperReports Ultimate Guide)

Arsitektur JasperReports (Dari JasperReports Ultimate Guide)

Sebuah rancangan laporan ditulis dalam format XML yang disebut JRXML.   File XML ini akan menghasilkan sebuah JasperDesign.   Untuk laporan yang layoutnya sangat dinamis, JasperDesign dapat dibuat langsung dari kode program tanpa harus berdasarkan file XML.   JasperDesign perlu di-compile menjadi sebuah JasperReport.   Tujuannya untuk melakukan validasi dan memastikan tidak ada kesalahan pada JasperDesign.   Berikutnya, untuk menampilkan sebuah JasperReport, developer perlu menentukan data yang akan dipakai oleh JasperReport tersebut.   Setelah melakukan proses pengisian data dengan JasperFillManager,  hasilnya adalah sebuah dokumen pixel-perfect yang dapat ditampilkan, dicetak, atau di-export ke berbagai format lain.

Yang menarik disini adalah proses ‘pengisian data’ bukan bagian dari rancangan struktur laporan.   Sebuah JasperReport yang sama dapat di-isi dengan data yang berbeda berkali-kali untuk menghasilkan output laporan yang berbeda.   Menariknya, tidak ada yang mensyaratkan bahwa sebuah JasperReport harus selalu di-isi dengan data dari database.   JasperReport dapat di-isi melalui apa saja yang mengimplementasikan interface JRDataSource yang mewajibkan dua method, yakni  next() dan getFieldValue().

Contoh implementasi JRDataSource bawaan adalah:

  • JRResultSetDataSource: mengisi berdasarkan hasil query SQL melalui JDBC. Ini yang sering dijumpai.
  • JRBeanArrayDataSource: mengisi berdasarkan sebuah array yang berisi object-object JavaBean.
  • JRBeanCollectionDataSource: mengisi berdasarkan sebuah Collection yang berisi object-object JavaBean.
  • JRMapArrayDataSource: mengisi berdasarkan sebuah array yang berisi Map yang mewakili sebuah ‘baris’.
  • JRTableModelDataSource: mengisi berdasarkan TableModel dari Swing, dengan kata lain, berdasarkan isi dari sebuah JTable.
  • JRXmlDataSource: mengisi berdasarkan dokumen XML.
  • JRCsvDataSource: mengisi berdasarkan file teks dalam format CSV (comma-separated value).
  • JRXlsDataSource: mengisi berdasarkan isi file Microsoft Excel (*.xls).
  • JREmptyDataSource: mengisi dengan baris yang seluruhnya datanya bernilai null.

Berdasarkan daftar di atas, untuk mencetak sebuah ‘object‘, saya memiliki banyak pilihan.   Pada tulisan ini, saya akan memakai JRBeanCollectionDataSource untuk mengisi laporan.   Data source tersebut membutuhkan sebuah Collection seperti List yang didalamnya berisi satu atau lebih object JavaBean.  Apa yang dimaksud sebagai JavaBean?   JavaBean adalah sebuah class di Java yang mengikuti beberapa syarat:

  • Harus memiliki default constructor yaitu sebuah constructor tanpa parameter.
  • Property yang bisa diakses oleh pihak luar harus dipublikasikan melalui setter seperti setXXX() dan getter seperti getXXX() atau isXXX().   Sebagai contoh, sebuah property String namaLengkap harus memiliki setNamaLengkap() dan getNamaLengkap().   Beberapa pelajar yang tidak mengetahui tentang JavaBean mungkin merasa heran melihat saya suka menggabungkan bahasa Inggris dan bahasa Indonesia.   Beberapa bahkan melakukan improvisasi dengan mengubah method menjadi ambilNamaLengkap() dan tulisNamaLengkap(), tapi ini bukan lagi sebuah JavaBean.   Groovy menyederhanakan proses yang ada dimana bila saya mendeklarasikan sebuah String namaLengkap, maka getter dan setter secara otomatis akan dibuat, dan variabel namaLengkap tersebut akan selalu diakses berdasarkan getter/setter-nya.
  • JPA Entity yang dihasilkan oleh simple-jpa mengikuti aturan JavaBean, sehingga saya dapat memakai JRBeanCollectionDataSource.

Untuk merancang laporan, saya tidak akan mengetik JRXML secara manual.  Sebagai gantinya, saya memakai editor berbasis GUI yang disebut iReport.   Dengan iReport, saya dapat membuat isi JRXML, men-compile laporan, mengisi data dan men-preview laporan secara mudah (visual).

Langkah pertama yang saya lakukan adalah membuat laporan baru dengan memilih File, New.   Setelah itu, saya memiliki template Blank A4.   Pada dialog berikutnya, saya menentukan lokasi penyimpanan file JRXML.   Setelah itu saya men-klik tombol Next dan memilih Finish.

Langkah kedua adalah menambahkan classpath sehingga iReport mengetahui isi file class JavaBean yang akan dipakai.  Dalam hal ini, class yang dibutuhkan adalah Faktur.class, ItemFaktur.class, Diskon.class dan sebagainya.   Karena saya memakai Griffon, maka saya memberikan perintah:

griffon package jar

untuk menghasilkan file Jar dari aplikasi saya.  Di dalam file Jar tersebut sudah berisi seluruh domain class.

Kemudian kembeli ke iReport, saya memilih menu Tools, Options.   Pada tab Classpath, saya menklik tombol Add JAR.   Kemudian saya men-browse ke lokasi file JAR yang dihasilkan Griffon, yaitu di folder dist/jar di lokasi proyek saya.

Langkah ketiga adalah men-klik icon Report Query dan memilih tab JavaBean Datasource.   Pada Class name, saya mengisi dengan domain.Faktur,  kemudian men-klik tombol Read attributes.  Setelah itu, saya memilih field yang perlu dicetak dan menambahkannya dengan men-klik tombol Add selected field(s), seperti yang terlihat pada gambar berikut ini:

Membaca field dari JavaBean

Membaca field dari JavaBean

Setelah ini, saya dapat mulai merancang faktur yang akan dicetak.   Saya akan membuang beberapa band yang tidak dibutuhkan seperti Title, Column Header, Column Footer, dan Page Footer.   Cara untuk membuang sebuah band adalah dengan men-klik kanan pada wilayah band tersebut dan memilih Delete Band.   Saya menyisakan band Summary untuk menampilkan total di akhir laporan.

Pada band Page Header akan terdapat judul dan informasi nomor halaman.  Lalu, pada band Detail akan terdapat isi laporan yang terlihat seperti pada gambar berikut ini:

Rancangan 'master' laporan

Rancangan ‘master’ laporan

Sebuah faktur pada dasarnya adalah sebuah formulir master-detail.   Saya telah menambahkan informasi master, lalu bagaimana dengan detail-nya? Detail untuk sebuah faktur adalah setiap line item yang berisi barang, jumlah, dan harga.  Bagaimana cara menambahkan detail tersebut?

Pada JasperReports versi lama, saya harus membuat report baru dan menyisipkannya sebagai subreport.   Beruntungnya, sejak JasperReports memperkenalkan konsep component di laporan, saya dapat menggunakan table component.   Sebuah table component hampir mirip seperti subreport tetapi ia merupakan bagian dari laporan yang sama.   Keuntungannya?  Table component lebih mudah dipakai karena tidak perlu repot membuat report baru dan menggabungkannya.

Saya segera men-drag icon table component ke band Detail:

Menambahkan table component ke detail

Menambahkan table component ke detail

Akan muncul sebuah Table wizard.   Saya men-klik tombol New dataset.   Pada Dataset name, saya mengisi dengan itemFakturDataset. Kemudian saya memilih Create an empty dataset dan men-klik tombol Finish.   Setelah itu, saya men-klik tombol Next untuk memasuki halaman Table Style.   Saya menghilangkan tanda centang pada Create a new set of styles for this table, Add Table Header dan Add Table Footer.   Setelah itu, saya men-klik tombol Finish.

Sekarang, saya bisa berpindah antara Main report untuk mengubah laporan secara keseluruhan, atau Table 1 khusus untuk mengubah detail faktur, seperti yang terlihat pada gambar berikut ini:

Table component merupakan bagian dari report yang sama.

Table component merupakan bagian dari report yang sama.

Saya akan melakukan beberapa pengaturan terlebih dahulu pada itemFakturDataset dengan men-klik dataset tersebut dan memilih Edit Query seperti yang terlihat pada gambar berikut ini:

Mengubah Dataset

Mengubah Dataset

Pada dialog Report query yang muncul, saya memilih tab JavaBean Datasource.   Setiap line item yang perlu ditampilkan diwakili oleh sebuah object ItemFaktur, sehingga saya mengisi domain.ItemFaktur pada Class name.   Setelah itu, saya men-klik tombol Read attributes, memilih property yang perlu ditampilkan, men-klik tombol Add selected field(s), lalu menekan tombol Ok untuk selesai.

Berikutnya saya perlu memberi tahu iReport bahwa data source untuk table component ini diambil dari property itemList milik Faktur.   Untuk itu, saya men-klik kanan di bagian yang kosong dan memilih menu Edit table datasource.   Pada Connection/Datasource exp, saya mengisi data source expression seperti pada gambar berikut ini:

Mengisi datasource expression

Mengisi datasource expression

Ekspresi di atas akan membuat sebuah JRBeanCollectionDataSource baru yang datanya diambil dari field itemList.  Bila dilihat dari class diagram, itemList adalah sebuah List yang berisi satu atau lebih ItemFaktur (relasi one-to-many).  Nantinya, table component akan menampilkan masing-masing ItemFaktur yang dimiliki oleh Faktur yang sedang diproses.

Langkah terakhir sebelum mulai merancang adalah menentukan field apa saja yang perlu dipakai.  Berikut ini adalah definisi field untuk table component ini:

Definisi fields untuk itemFakturDataset

Definisi fields untuk itemFakturDataset

Class ItemFaktur memiliki relasi many-to-one dengan class Barang.   Untuk mengakses property milik class Barang dari ItemFaktur, saya dapat menggunakan dengan operator titik.   Misalnya, barang.nama akan memanggil method getNama() dari atribut barang yang dimiliki oleh ItemFaktur yang sedang diproses.   Sebuah Barang juga memiliki hubungan many-to-one dengan class Jenis.   Property milik class Jenis juga dapat diakses dengan tanda titik, misalnya seperti barang.jenis.nama.

Berikutnya saya akan merancang isi detail untuk table component dimana hasil akhirnya akan terlihat seperti pada gambar berikut ini:

Rancangan untuk table component

Rancangan untuk table component

Untuk menambah kolom baru, saya men-klik bagian kosong dan memilih Add Column To The End.

Saya memakai variabel $V{REPORT_COUNT} untuk mendapatkan nomor line item yang berurut.  Tapi bagaimana dengan perhitungan subtotal?  Bukankah class ItemFaktur sudah memiliki method total() yang akan menghitung total setelah dikurangi diskon?  Bagaimana cara memanggilnya?

Method total() tidak mengikuti standar penamaan getter atau setter JavaBean sehingga ia bukanlah sebuah property yang dapat diakses sebagai field.   Tapi ada cara lain untuk mengaksesnya.   JasperReports memungkinkan pengguna untuk mendefinisikan sebuah field dengan nama _THIS.   Nilai dari field _THIS selalu merupakan instance dari objek yang sedang diproses (dalam hal ini adalah sebuah objek ItemFaktur).   Dengan demikian, ekspresi $F{_THIS}.total() akan memanggil method total() dari objek ItemFaktur yang sedang diproses.

Mendefinisikan field _THIS

Mendefinisikan field _THIS

Memanggil method total()

Memanggil method total()

Setelah ini, saya kembali ke Main Report.   Saya juga mendefinisikan field _THIS serta menambahkan informasi summary (total Faktur secara keseluruhan), seperti yang terlihat pada gambar berikut ini:

Contoh rancangan akhir report

Contoh rancangan akhir report

Pada total gross, saya memakai ekspresi Groovy berikut ini untuk menghitung total seluruh item faktur:

$F{_THIS}.itemList*.total().sum()

Hasil perhitungan total net sebenarnya sudah dikalkulasi oleh method total() milik class Faktur.   Saya dapat memanggil method tersebut dengan menggunakan ekspresi seperti:

$F{_THIS}.total()

Dengan demikian, bila saya melakukan perubahan rumus kalkulasi pada method total() di domain class, maka selain diterapkan di aplikasi (tampilan form), perubahan juga akan langsung mempengaruhi laporan (atau percetakan).

Sampai disini, saya dapat langsung memakai report di aplikasi.   Tapi akan lebih baik bila saya melihat seperti apa tampilan report di iReport terlebih dahulu.   Caranya adalah dengan membuat sebuah class dengan method static yang menghasilkan data dummy sebagai informasi yang akan ditampilkan.  Class ini terletak di JAR yang sama dengan yang diberikan di classpath.   Sebagai contoh, saya membuat class dengan isi seperti ini:

package util

import ...

class ReportTest {

    static List getDataFaktur() {
        List hasil = []

        Faktur faktur = new Faktur(nomor: '12345', tanggal: LocalDate.now())
        Jenis jenis1 = new Jenis("JENIS1", "JENIS1")
        (1..20).each {
            Barang barang = new Barang("BRG1$it", "Barang1$it", jenis1, 12345)
            faktur.tambahItem(barang, new BigDecimal(Math.random() * 10000000), (Math.random() * 1000).intValue(),
                    new Diskon(Math.random()*50, Math.random() * 1000))
        }
        Jenis jenis2 = new Jenis("JENIS2", "JENIS2")
        (1..20).each {
            Barang barang = new Barang("BRG2$it", "Barang2$it", jenis2, 54321)
            faktur.tambahItem(barang, new BigDecimal(Math.random() * 10000000), (Math.random() * 1000).intValue(),
                    new Diskon(Math.random()*50, Math.random() * 1000))
        }
        faktur.potonganRetur = 300000
        faktur.proses(false, LocalDate.now().plusDays(30), 3, null)

        hasil << faktur
        hasil
    }

}

Pada kode program di atas, agar cepat, saya membuat instance objek secara langsung tanpa mengambil dari database.   Tujuan utamanya hanya menciptakan objek yang akan ditampilkan di laporan nantinya.   Proses pengambilan object dari database pada aplikasi nanti tetap dilakukan melalui JPA atau simple-jpa.

Berikutnya, saya menambahkan sumber data tersebut dengan men-klik icon Report Datasources, dan men-klik New pada dialog yang muncul.

Menambahkan datasource baru

Menambahkan datasource baru

Pada kotak dialog Datasource yang muncul, saya memilih JavaBeans set datasource dan men-klik tombol Next.   Saya mengisi dialog sesuai dengan informasi class saya, kemudian men-klik tombol Test untuk mengujinya.   Setelah semua berjalan dengan benar, saya men-klik tombol Save untuk menyimpan perubahan.

Mendefinisikan source JavaBean yang dipakai untuk preview laporan di iReport

Mendefinisikan source JavaBean yang dipakai untuk preview laporan di iReport

Untuk melihat seperti apa tampilan report, saya men-klik tombol Preview, seperti yang terlihat pada gambar berikut ini:

Contoh hasil preview laporan

Contoh hasil preview laporan

 

Contoh hasil preview laporan dengan summary

Contoh hasil preview laporan dengan summary