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.

Perihal Solid Snake
I'm nothing...

9 Responses to Belajar Merancang Laporan JasperReports Dengan iReport

  1. Shaleh Ar-Rahmat mengatakan:

    Gan gimana kalo subtotal untuk sales solid snake dijumlahkan dengan subtotal untuk sales liquid snake lalu dikurangi subtotal untuk sales big bos? gimana caranya? saya sekarang sedang menghadapi kasus seperti itu.. mohon bantuannya… thanks..🙂

    • Solid Snake mengatakan:

      Caranya bisa dengan membuat tiga variables baru, yaitu salesSnake, salesBigBoss, dan salesLiquidSnake seperti yang ditunjukkan pada gambar dibawah ini:

      Membuat tiga variables baru yang mewakili total

      Atur properties supaya terlihat seperti pada gambar berikut ini:

      Properties untuk variable yang menampung subtotal

      Pada variable salesSnake, gunakan ekspresi berikut ini:

      $F{sales}.nama == "Solid Snake"? $F{_THIS}.total(): 0
      

      Pada variable salesLiquidSnake, gunakan ekspresi berikut ini:

      $F{sales}.nama == "Liquid Snake"? $F{_THIS}.total(): 0
      

      Pada variable salesBigBoss, gunakan ekspresi berikut ini:

      $F{sales}.nama == "Big Boss"? $F{_THIS}.total(): 0
      

      Operator ternary ?: bekerja seperti if. Ketiga ekspresi di atas akan mengembalikan 0 bila nama sales tidak sesuai sehingga hasil kalkulasi sum hanya menghitung total untuk nama sales tertentu.

      Variables akan terisi dengan nilai yang sesuai, untuk menampilkan hasil kalkulasi, buat sebuah text field dengan ekspresi berupa:

      $V{salesSnake} + $V{salesLiquidSnake} - $V{salesBigBoss}
      

      Ini merupakan contoh kasus yang aneh. Laporan tidak selalu memiliki sales dengan nama tersebut dan nama sales bisa berubah kapan saja (oleh user)!.

      Pada OOP, umumnya bila terdapat pengkategorian sales yang strict (statis), bisa dengan inheritance, misalnya menurunkan class Sales menjadi SalesInternal atau SalesOutsourcing.

      Bila pengkategorian sales bersifat dinamis, bisa dengan membuat knowledge level class seperti JenisSales yang berasosiasi dengan Sales. Contoh object untuk class JenisSales misalnya “SalesRegionA”, “SalesRegionB”, dsb. Grouping nantinya bisa dilakukan berdasarkan object-object JenisSales tersebut.

  2. Komang Hendra Santosa mengatakan:

    Mas, kira2 ini ada source codenya ya?soalnya saya blom mengerti benar..thx u..

    • Solid Snake mengatakan:

      Source code secara keseluruhan proyek tidak tersedia karena program di atas bagian dari yang lebih kompleks lagi. Akan tetapi, source code yang berhubungan dengan JasperReport sudah ada di atas.

  3. Tolhah Hamzah mengatakan:

    Mas ini kenapa ya kok muncul error begini

    Uncaught Exception
    groovy.lang.MissingMethodException: No signature of method: gfsimlkm6.LaporanController.doRekapAnggotaOnAnggota() is applicable for argument types: (java.util.L
    inkedHashMap) values: [[tanggalMulaiCari:2014-05-04, tanggalSelesaiCari:2014-05-
    18]]

    berikut saya sertakan controller LaporanController.groovy saya

    import domain.*
    import net.sf.jasperreports.engine.JRDataSource
    import net.sf.jasperreports.engine.JasperFillManager
    import net.sf.jasperreports.engine.JasperPrint
    import net.sf.jasperreports.engine.data.JRMapCollectionDataSource
    import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
    import net.sf.jasperreports.swing.JRViewer

    import javax.imageio.ImageIO
    import org.joda.time.LocalDate
    import simplejpa.swing.DialogUtils
    import simplejpa.transaction.Transaction

    import javax.swing.JOptionPane
    import javax.swing.JPanel
    import java.awt.BorderLayout

    class LaporanController {
    // these will be injected by Griffon
    LaporanModel model
    def view

    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)
        }
    }
    

    }

    saya masih bingung mas sama method ini, sebenernya ada dimana sih mas?

    doRekapAnggotaOnAnggota()

    atau ada kesalahan di List source nya ya?
    Terima kasih mas, mohon bantuannya mas ):

    • Solid Snake mengatakan:

      doRekapAnggotaOnAnggota() adalah cara lama untuk memanggil named query dengan nama Anggota.RekapAnggota. Named query adalah query yang didefinisikan pada JPA Entity (domain class) dengan menggunakan @NamedQuery. Pada versi simple-jpa yang lebih baru, ini setara dengan memanggil executeNamedQuery('Anggota.RekapAnggota').

      Untuk informasi dan contoh kode program, coba baca dokumentasi simple-jpa tentang executeNamedQuery() di https://jockihendry.github.io/simple-jpa/v0.6.html#executeNamedQuery.

      Pada contoh kode program di artikel ini, saya menganggap query didefinisikan pada masing-masing domain class. Tapi tidak perlu selalu demikian. Kamu juga boleh mengerjakan query JP QL disini dengan executeQuery(), memakai finders seperti findByDsl(), dan sebagainya.

  4. Tolhah Hamzah mengatakan:

    berhasil mas, terima kasih.

    saya menggunakan findAllAnggota(…….), dan sedang mencoba menggunakan yang lainnya😀

  5. Raden Denny mengatakan:

    Permisi kk ,sebelumnya makasi tutorialnya .
    Tapi saya masih bingung gan, saya newbie baru belajar java ,saya ingin mencetak sebuah laporan sesuai tanggal tertentu .
    Sudah mencoba mengikuti cara di atas namun masih gagal,Apakah ada contoh program nya gan ?

    Mohon bantuannya,Makasi sebelumnya.

  6. Ayyu Cute mengatakan:

    makasih ka atas tutorialnya, saya mencoba input nominal di sebsar 200.000.000 tapi yang muncul malah 2.0E8 ya, itu masalahnya kenapa mohon bantuannnya ka masih newbie nih

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: