Developing Jasper Report Solutions for Opentaps
Contents
Overview
In this document we will cover the full development of a Jasper Report solution for opentaps. The goal is to cover enough information so that you can create any kind of report or document desired.
Example to be Used
There is a customer statement in the financials application that was made using Jasper Reports. Here is an example of its PDF output,
To generate it, go to the Balances by Customer report in the receivables tab. You must have several invoices with unpaid amounts that are in the ready state. Run the balances as of today and you should be able to select a list of customers and print out statements for all of them.
There are many features built into this particular document which makes it an ideal candidate for study. Each statement contains the following information,
- Customer's billing address (or print not on file)
- Company's logo, address, phone number and other contact information
- A list of invoices that have open (unpaid) amounts
- If the invoice has a due date, then show the number of days since (age date)
- All invoice payments made in the 30 days prior to the statement date
- If the payment is to an invoice that was already paid off, then display the closed invoice for reference
- Total open amount, which must be paid by customer (sum of open amounts)
- Date brackets showing how much is past due and when.
- If any amount is past due, a large PAST DUE will be displayed on the statement
- Layout designed for perforated page with perforation running down the right side
- Information repeated on both sides for purposes of customer and return receipt
As you can see, this is a complex set of information. It is further complicated by the fact that we have to print one statement for each customer in the same document. Yet, it is not a difficult task to do using Jasper Reports in conjunction with Opentaps. We will cover all aspects of developing this document. It should provide a sufficient foundation from which to develop any kind of report desired.
Development Process Overview
The first step in the development is to create a Jasper Report (.jrxml) document using JasperSoft iReport. The goal should be to create the basic layout and formatting for the report using a good set of mock data.
Once you have a report, you can write the form for it and a Java or Beanshell method that reads the form via an HTTP request. The method will be responsible for generating the report parameters and data set. Next, we tell the controller to treat this request using the "jasperreports" view handler. At this stage, the jasper report should be generated by submitting the form.
After this, you will be going back and forth between iReports and opentaps until the report is finished.
Next, we will be covering this development process in detail.
Creating the Initial Jasper Report Document
This section assumes basic familiarity with iReports. You may want to read our introductory tutorial to iReports first.
Initially we should set up enough mock data so we can format the various fields ahead of time. The simplest technique is to set up mock parameters that either have default values or are set up for prompting so you can enter values when the report is run.
Of course, some of the parameters will be used in the final report. These should be set up for prompting. For instance, the customer statement takes the date of the statement and a set of fields that produce the organization logo and address.
For reference, you may wish to load the Jasper Report document for the customer statement in iReports. It can be found in your opentaps installation,
hot-deploy/financials/webapp/financials/reports/repository/CustomerStatement.jrxml
Advanced iReport Tips
Before we begin, there are several general points about iReports that are not well documented:
- If not specified, the dimensions of an item are in pixels
- iReports uses a fixed 72 pixel per inch conversion
- Example: Edit the band properties and the height box will be in pixels
- This means you should convert your measurements to pixels using the 72 pixel an inch rate
- Elements
- All elements can be positioned and sized precisely using the properties window -> Common tab.
- You can find all features supported by an element by clicking on the All tab of the element's properties window
Generating a Logo Image
One way to place the logo on a report is to have Jasper Reports fetch it from the internet. (Only works if your opentaps instance has internet access!) The logo URL is specified as a String parameter. In the case of the customer statement, the parameter is logo_url and by default we pass it the opentaps logo http://www.opentaps.org/images/opentaps_logo.png.
To place the image on the document, use the Image tool and draw a box. This box will be the boundaries of the image.
Next, edit the properties of the image and change the width and height to the desired size. The image will be scaled to fit. Once you do this, you can align the image with respect to the other document elements.
Finally, we need to tell the image to use our logo_url parameter. In the properties box, click on the Image tab. We are interested in creating a URL object. The image element will use this to fetch the image automatically.
Next time you run the report the images should be generated.
Mock Data Set for Detail Band
A basic Jasper Report will use a SQL query to extract a data set for the detail band. In the case of customer statement, the data for the detail band is too complex for a SQL query. Later on we will create a program that will build this data set, but for now we can rely on a SQL query that returns a mock data set. The query will simply select data from the Invoice table while grouping the results by the customer partyId.
SELECT party_id, 'INV' AS type, invoice_id, invoice_date AS transaction_date, due_date, 100.0 AS invoice_total, 100.0 AS open_amount, 3 AS age_date, invoice_id AS invoice_id_2 FROM invoice ORDER BY party_id
Note that the query does an order by party_id instead of grouping. There are two reasons for this. First, the list we are going to be generating from our program will not be grouped due to the nature of the interface we will be using. Second, Jasper Reports can group ordered data on its own as we will see shortly.
Also note we are selecting some made up data for some of the fields. Our data generation program will be handling these fields in the future, but we need to see the data now so we can format it.
Finally, the invoice ID is selected twice into different field names to simplify some logic. A closed invoice will have is invoiceId printed on the receipt but not on the return. If we have two fields, then we can leave one null to make this easy.
If you have opened the customer statement in iReport, go ahead and enter this query in. You will need to change the fields that are BigDecimal to Double, and those that are Integer to Long. (The data set generation program creates Doubles and Longs for these fields.)
Grouping an Ordered Data Set
If you have a data set that is ordered according to one field, such as party_id in this case, then we can group them in Jasper Reports. Click on the Report Groups tool,
Add a new group and specify the party_id field as the expression.
There are also options that control whether page numbers should be reset each group and so on. They were checked according to what the statement report needs.
The report should now be grouped by party_id.
Opentaps Integration
In this section, we will cover how to integrate a Jasper Report solution with opentaps. The primary components of an integration are,
- A form which provides the User Interface for the report
- A controller.xml request map for the form's action
- A Java or Beanshell method invoked by the request map to generate parameters and a data set for the Jasper Report
- A controller.xml view map using the "jasperreports" view handler that connects the request to the .jrxml file
Specifying Controller Request and View
Let's assume we wish to write our form processing code and report setup in Beanshell. Using a scripting language offers us the advantage of quick prototyping and development since we will not have to restart the server to load changes.
The request map for the customer statement invokes a Beanshell script in the following way,
<request-map uri="CustomerStatement.pdf"> <security https="true" auth="true"/> <event type="bsf" path="/reports/" invoke="jrCustomerStatementPDF.bsh"/> <response name="success" type="view" value="jrCustomerStatement"/> <response name="error" type="none"/> </request-map>
This request defines a URI CustomerStatement.pdf which we can call from a form action. It defines a Beanshell script that will be invoked and a view map to use when the script returns a "success" string. If there is a problem, the script can return "error". In this particular request, the error response of type "none" will produce a blank page. It would be more useful to print the error, but for now we will rely on the log file.
Beanshell events are invoked using the "bsf" type. They live in a path relative to the location of the web application's root. In this case, it will be in the financial application's webapp/financials/reports/ directory. Another way to think of it is the root of the path is the same directory where the WEB-INF directory lives. In this example, if the controller.xml file is located at ${path}/webapp/financials/WEB-INF/controller.xml, then the reports directory will be ${path}/webapp/financials/reports.
We will explore the contents of this script file in the next section.
The view map is defined as follows,
<view-map name="jrCustomerStatement" type="jasperreports" page="component://financials/webapp/financials/reports/repository/CustomerStatement.jrxml" content-type="application/pdf" encoding="none"/>
It uses a "jaspereports" view type which handles Jasper Report integration. The view identifies the location of the Jasper Report using the opentaps component:// path location notation. Since this report is always printing a PDF, we set the content type to PDF. The encoding for the report is specified as "none" because this parameter is generally only useful for HTML views.
This is all that is required to hook up a form to a Jasper Report document in the opentaps controller.
Java and Beanshell Event Methods
In opentaps, when a Java or Beanshell method is invoked in a request map, it is passed an HttpServletRequest object. It is also passed an HttpServletResponse object, but this is generally not used. From the request object, we can obtain an HttpSession, an opentaps delegator, dispatcher and all of our form parameters.
Note that there is essentially no difference between a Java and Beanshell version of the method. We call these Event methods.
Obtaining Data From The Request
Note: Remove any mock query expression from the .jrxml, otherwise the custom data set will not be used.
In our example, jrCustomerStatementPDF.bsh requires the delegator and the organization which is printing the statements. The financials application stores the organizationPartyId in the session. Both are obtained from the request as follows,
delegator = request.getAttribute("delegator"); session = request.getSession(); if (session == null) { UtilMessage.addError(request, "OpentapsError_MissingPaginator"); return "error"; } organizationPartyId = session.getAttribute("organizationPartyId");
If there is a problem, the script sets an error using a special UtilMessage method that provides localization support and then returns "error". In essence, the script must return a string according to the possible responses in the request map. It is possible to have more than just "success" or "error" if more options are desired for a response.
Getting data from a form is a matter of using response.getParameter(fieldName). However, the field name can sometimes return an empty string. There is a utility message that simplifies the logic of detecting empty strings and it is used to get the date of the statement,
asOfDateString = UtilCommon.getParameter(request, "asOfDate");
This method will return null if the string is in any way empty, such as if the user enters a space. Otherwise it returns a trimmed string value that is guaranteed to have some content. Thus if the user enters a value with a trailing space, the value without the space is the result.
Since our statement is grouped by partyId, our form must also submit data in the opentaps Multi Select Form Notation. This notation allows us to pass a list of similar values for processing. The notation will look something like this,
<input type="hidden" name="partyId_o_0" value="10009"> <input type="hidden" name="partyId_o_1" value="10010"> <input type="hidden" name="partyId_o_2" value="10011">
We can turn this set of inputs into a Collection of Maps with key "partyId" and values "10009", "10010", "10011" as follows,
params = UtilHttp.parseMultiFormData(UtilHttp.getParameterMap(request));
To provide checkboxes so that the user can pick particular rows, all we have to do is provide a checkbox field for each row. The parseMultiFormData function will return only those which are checked (have value "Y").
<input type="checkbox" name="_rowSubmit_o_${row_index}" value="Y"/>
Also available is a javascript function to toggle these checkboxes,
<input type="checkbox" name="selectAll" value="Y" onclick="javascript:toggleAll(this, 'CustomerStatement');"/>
Setting Data For The Jasper Report
In order to pass data to the Jasper Report, we need to organize it into two objects based on whether it is a Jasper Reports parameter or part of the data set.
All Jasper Report parameters (those that use $P{} notation) are put into a map. This map is then put into a request attribute named "jrParameters". Note that we can put any kind of object into the parameters. The only limitation is how easy it would be to evaluate the parameter in a one line Jasper Reports expression. We will use this capability to pass in a complex map of data shortly.
The report data set is constructed using a JRMapCollectionDataSource, which takes as an argument a list of Maps. You can pass it a list of GenericValues, which are Map based objects. However, we need to do a lot of processing in this report, so each row is a hash map built by one of two methods: createInvoiceRow or createPaymentRow. Once the list of data is constructed, we must put the JRMapCollectionDataSource in a request attribute named "jrDataSource",
request.setAttribute("jrDataSource", new JRMapCollectionDataSource(report));
It is very important that every row has the same fields.
Note further that in the Jasper Report, we do not use camel case for variable names. This is to avoid problems with case sensitivity.
Another important consideration is to be careful about the data type. Jasper Reports is sensitive to input. If the parameter is defined as a Long, do not pass an Integer. Cast it to Long first. Likewise, booleans should be passed as Boolean objects rather than primitives because Beanshell's notion of a boolean primitive is not what you'd expect.
Using a Map as a Jasper Report Parameter
In the statement, we are interested in printing out each party's billing address, the amounts they are past due, and other party based information. However, we cannot put this data in the JRMapCollectionDataSource since it is not part of a coherent list of data. Instead, we can create a partyId keyed Map and use this to get the correct data for each statement page.
However, any Map we pass into the JasperReport should be flat to avoid complex text field expressions. This means that for every key, there must be precisely one value that is used directly within Jasper Reports. Any more complex data structure will likely be difficult to work with.
For the statement, the keys of the map can be generated as a combination of the partyId and the field name:
- partyId + "billing_address" - key for the billing address string
- partyId + "over_30" - key for the amount past due 30 days
- partyId + "is_past_due" - key for a Boolean field that controls the large "PAST DUE" printed on the statement
We can then call this data from within the Jasper Report using the following text field expression,
$P{party_data}.get($F{party_id} + "billing_address")
This line will generate the billing address. Note that if the billing address does not exist, we have an "Address not on file." string instead. The key idea is to never let this expression throw a NullPointerException. Thus, we always set a billing address value for every party.
Troubleshooting and Debugging
There are two sources for errors in this system, the Event method or the JasperReport. Any exception from either is printed in the log. You may also wish to print the error message. This can be done in any number of ways, but the simplest is to use the following "error" response,
<response name="error" type="view" name="error"/>
If the mock data shows up instead of the expected data set, please check the .jrxml and ensure the queryExpression with the mock query is commented out or removed. You may wish to restore the query when using iReports to adjust the report layout and format.