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: Menampilkan atau Mencetak

Pada artikel merancang laporan,  saya sudah membuat sebuah laporan dan memastikan bahwa laporan tersebut dapat ditampilkan dengan baik.   Langkah berikutnya adalah memakai laporan tersebut di aplikasi.   Saya akan mencoba melakukannya dari sebuah aplikasi yang memakai framework Griffon dan plugin simple-jpa.   Untuk memakai JasperReports di Griffon, sebenarnya sudah ada plugin jasperreports.  Tapi pada tulisan ini, saya akan mencoba cara manual.

Langkah pertama yang harus saya lakukan pada program saya adalah menambahkan dependency JasperReports.  Untuk itu saya perlu membuka file BuildConfig.groovy dan menambahkan baris seperti berikut ini:

griffon.project.dependency.resolution = {
    inherits("global") {
    }
    log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
    repositories {
        griffonHome()
        mavenLocal()
        mavenCentral()
        mavenRepo "http://jasperreports.sourceforge.net/maven2"
    }
    dependencies {
        compile 'org.hibernate:hibernate-entitymanager:4.2.0.Final'
        compile('net.sf.jasperreports:jasperreports:5.1.0') {
           exclude 'commons-collections'
        }
    }
}

Saya perlu menambahkan repository dari JasperReports karena mereka memakai versi iText yang telah di-patch dan belum tersedia di repository central Maven.  Setelah perubahan di atas, bila saya menjalankan aplikasi dan terhubung ke internet, maka Griffon akan secara otomatis men-download JAR yang dibutuhkan.

Langkah berikutnya adalah meletakkan laporan JasperReports yang telah di-compile (file berekstensi *.jasper bukan *.jrxml) ke lokasi proyek, misalnya griffon-app/resources/report.  File *.jasper dapat diperoleh dengan men-preview laporan atau men-click icon compile di iReport.

Langkah terakhir, saya perlu menampilkan laporan tersebut di aplikasi saya.   JasperReports memiliki sebuah komponen visual, JRViewer, yang dapat dimasukkan ke dalam view Griffon untuk menampilkan laporan. JRViewer adalah turunan dari JPanel.  Dengan demikian, JRViewer dapat dipakai disemua tempat dimana sebuah JPanel dapat dipakai.

Agar lebih leluasa, saya akan membuat sebuah MVCGroup baru yang khusus dipakai untuk menampilkan preview laporan, dengan memberikan perintah berikut:

griffon create-mvc PreviewLaporan

Setelah Griffon selesai membuat MVCGroup yang dibutuhkan, saya membuka dan mengubah isi file PreviewLaporanView.groovy menjadi seperti berikut ini:

application(title: 'Report Viewer') {

    borderLayout()
    panel(id: 'mainPanel', constraints: CENTER) {
        borderLayout()
    }

}

Saya juga menambahkan sebuah mvcGroupInit() pada PreviewLaporanController.groovy, yang isinya seperti berikut ini:

void mvcGroupInit(Map args) {
  JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(args.'data')
  JasperPrint jp = JasperFillManager.fillReport(getResourceAsStream("report/faktur.jasper"), [:], ds)
  JPanel panel = view.mainPanel
  panel.clear()
  panel.add(new JRViewer(jp), BorderLayout.CENTER)
}

MVCGroup di atas membutuhkan sebuah parameter dengan nama data.   Parameter ini adalah sebuah Collection(misalnya List), yang berisi object yang akan ditampilkan.

MVCGroup ini akan dipanggil melalui MVCGroup lain.    Misalnya, saya memiliki sebuah MVCGroup lain yang menampilkan tabel berisi daftar faktur yang ada.  Bagaimana caranya menambahkan sebuah tombol yang bila di-klik akan menampilkan MVCGroup lain? Plugin simple-jpa memiliki sebuah node bernama mvcPopupButton() untuk keperluan tersebut.   Sebagai contoh, saya menambahkan mvcPopupButton() pada view yang menampilkan faktur:

mvcPopupButton('Cetak Faktur', mvcGroup: 'previewLaporan', args: {[data: ([] <<
    model.fakturSelection.selected[0])]},
    dialogProperties: [title: 'Preview Faktur', size: new Dimension(800,600)])

Pada kode program di atas, bila tombol ‘Cetak Faktur‘ di-klik, maka faktur yang sedang dipilih di JTable (nilai kembalian dari model.fakturSelection.selected[0]) akan dijadikan sebagai parameter dalam memanggil MVCGroup previewLaporan.   Contoh tampilan MVCGroup previewLaporan akan terlihat seperti pada gambar berikut ini:

Contoh Tampilan Komponen JRViewer

Contoh Tampilan Komponen JRViewer

Terlihat bahwa komponen JRViewer telah dilengkapi tombol Save untuk menyimpan dan men-ekspor laporan ke berbagai format lain seperti PDF, HTML, Excel, dan sebagainya.   Selain itu, juga terdapat tombol Print yang dapat dipakai untuk mencetak laporan.

Untuk formulir seperti faktur, biasanya pengguna tidak membutuhkan preview dan ingin langsung melakukan percetakan ke printer.  Bagaimana caranya supaya bila tombol Cetak Faktur di-klik, maka proses percetakan laporan langsung dimulai tanpa memunculkan preview?  Apakah hal tersebut mungkin?

Yup, bisa!   Untuk mencetak sebuah report secara langsung, saya dapat memakai class JasperPrintManager.   Sebagai contoh, berikut ini adalah sebuah tombol Cetak Faktur yang bila di-klik akan langsung mencetak object Faktur yang sedang terpilih di JTable ke printer:

button('Cetak Faktur', actionPerformed: {
  JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(
      [] << model.fakturSelection.selected[0])
  JasperPrint jp = JasperFillManager.fillReport(
      getResourceAsStream('report/faktur.jasper'), [:], ds)
  JasperPrintManager.printReport(jp, false)
})

Proses percetakan ke printer default akan langsung dimulai saat tombol di-klik.   Bila ingin menampilkan dialog dimana pengguna bisa memilih printer dan melakukan konfigurasi (misalnya mengganti ukuran halaman atau mengubah kualitas percetakan), maka saya perlu mengubah argumen false menjadi true pada saat memanggil printReport() seperti:

JasperPrintManager.printReport(jp, true)

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