JasperReportsClassic

Aus iDempiere de
Zur Navigation springen Zur Suche springen

[Jasperreports] ist ein Reportgenerator, dessen Möglichkeiten weit über die der in Adempiere eingebauten Report-Fähigkeiten hinausgehen. Die klassische Integration geht über das Plugin "org.adempiere.report.jasper", das hier beschrieben wird. Es gibt eine neuere Implementierung JasperReportsFreiBier, die etwas modularer aufgebaut ist.

Allgemeine Informationen gibt es auf der Seite JasperReports.


Aufruf in iDempiere

Man erzeugt einen neuen Datensatz im Fenster "Bericht & Prozess". Dort schreibt man in das Feld "Jasperbericht" den Namen seines Berichtes. Dieser Name muss den Pfad enthalten und auf ".jasper" enden. (Die Endung ".jrxml" sollte grundsätzlich auch gehen - einerseits verlangsamt das den Start unnötig andererseits kann aber eine *.jasper-Datei, die mit einer anderen Version (also z.B. aus einem neueren iReport) übersetzt ist, zu Ärger führen). Der Name kann auch mit "http:", "https:", "attachment:", "resource:", "file:" anfangen, um zu erklären, wo er herkommt. Je nach Auswahl kann es übrigens zu einigen Besonderheiten kommen, was das Nachladen von Unterdateien (Subreports, Bilder) betrifft. In dem Fall empfehle ich das Studium des Sourcecodes von ReportStarter.java.

Man muss noch bei "Bericht" ein Häkchen machen. Dieses Häkchen führt einerseits dazu, das immer die Klasse org.compiere.report.ReportStarter.java automatisch ausgeführt wird. Es muss also keine Prozess-Klasse mehr angegeben werden. Andererseits wird hierdurch der Bericht in einigen Pull-Down-Menüs überhaupt erst angezeigt, wo man Berichte auswählen kann.

Man kann übrigens auch noch eine Prozess-Klasse angeben, diese wird dann zusätzlich ausgeführt. Das ist eigentlich blöde, wenn man einen Bericht auf eine andere Weise als mit dem ReportStarter starten will, weil man dazu "Bericht" ausschalten muss und dann nicht mehr in den Pull-Down-Menüs steht (Es hilft wahrscheinlich, es erst einzuschalten, dann auszuwählen und dann auszuschalten).

Ein solcher "Bericht & Prozess" kann nun im Fenster "Fenster, Register & Feld" in einem Register als "Vorgang" angegeben werden. Ist das geschehen, ist im entsprechenden Fenster der "Print Preview" und der "Print" Button aktiviert und der Report wird gestartet.

Nach der Philosophie von ADempiere ist der Report, den man mit dem Print-Button aufruft übrigens immer einer, der einen einzelnen Datensatz ausgibt. Will man eine Liste ausgeben, nimmt man dazu den Bericht-Button (links daneben). Dieser startet normalerweise den ADempiere-internen Berichtsgenerator. Der ist auch ganz nett, was hier aber nicht das Thema ist. Man kann dort daher mit dem Werkzeug-Icon den Bericht konfigurieren und im sich öffnenden Fenster "Druckformat" einen "Jasperprozess" auswählen (also einen Prozess, bei dem "Bericht" eingeschaltet ist). Ab diesem Moment wird man nicht mehr vom internen Berichtsgenerator behelligt.

Wer das später anpassen oder vorher direkt konfigurieren will, kann das im Fenster "Druck Format" machen. Die aus dem Programm heraus konfigurierten Einträge werden nicht global gespeichert und können auch nur von einem Mandanten aus wieder geändert werden. Ich denke jedoch, das man auch im System-Mandanten globale Druck-Formate einstellen kann. Ich habe noch nicht raus, wie ein bestimmtes Register-Tab nun mit einem bestimmten Druckformat verbunden ist. Vielleicht geht das nur über die gemeinsame Tabelle.

Wer will, das ein Report möglichst dem Inhalt des Fensters ähnlich sieht, sollte sich JasperReports with Window Sql-Clauses ansehen.


Subreports

Ein Sureport ist ein "Report im Report", also eine Art Unterliste. Wenn ich z.B. einen Report erzeuge, der meine Rechnungen ausgibt (und dazu über Datensätze aus der Tabelle C_Order iteriert), braucht man einen solchen Subreport, um die Liste der Artikelzeilen anzuzeigen.

Wer komplexe Dinge mit Subreports und anderen externen Resourcen (Bilder, Properties-DAteien, etc.) machen möchte, möchte sich vielleicht das neuere Plugin JasperReportsFreiBier ansehen.


Subreport iDempiere-freundlich einrichten

Siehe hierzu zuerst einmal die entsprechende Dokumentation von iReport. Die Subreport Expression ist:

 $P{SUBREPORT_DIR} + "Kundendaten_subreport1.jasper"

und hat die Class java.lang.String. Dieser Ausdruck sorgt dafür, das der Subreport im gleichen Verzeichnis gesucht wird wie der Hauptreport - unabhängig davon, ob es sich um einen Pfad bei dem einen oder dem anderen Entwickler, aus iReport oder innerhalb iDempiere oder sogar aus dem Classpath eines Java JAR-Files heraus handelt.

Die Connection Expression ist $P{REPORT_CONNECTION} (also die gleiche Datenbank-Verbindung benutzen wie der Hauptreport).

Dann kann man Parameters definieren. Das ist eigentlich das wichtigste am Subreport, weil hier die Verbindung zum Mutter-Report hergestellt wird. Wenn wir hier einen Parameter RECORD_ID einrichten und dort unser Schlüsselfeld eintragen (z.B. C_BPartner_ID), sorgen wir dafür, das sich das für den Subreport genauso anfühlt, als sei er ganz alleine für sich aus ADempiere heraus aufgerufen worden. Der Subreport kann also auch alleine aus ADempiere heraus aufgerufen werden. Wird diese Konvention überall beachtet, erhält man so eine Sammlung von Modulen, die alle austauschbar und ineinander schachtelbar sind.

Über das Kontext-Menü des Subreport-Elements kann man nun den Subreport öffnen und selber definieren. Hier macht es u.U. Sinn, alle möglichen Footer- und Header-Bereiche auszublenden. Anosnsten erzeugt man auch hier zuerst einen Parameter mit dem Namen "RECORD_ID" und verwendet diesen dann in einem Query in der Form $P{RECORD_ID}. Also alles wie beim Mutter-Report.

weitere Probleme beim Auffinden von Subreports

Leider zeigten meine Tests, das das Auffinden von Subreports mit Hilfe des Parameters SUBREPORT_DIR nicht so gut funktioniert, wie man sich das denkt. Ein Blick in den Quelltext ergab, das man in JasperReports/src/org/compiere/report/ReportStarter.java suchen muss, wenn man Antworten haben will. Ich konnte dort nicht finden, wo (und ob) SUBREPORT_DIR gesetzt wurde.

Ich habe den Kern des Problems erst nach einigen Versuchen und dem Quellcode-Studium verstanden und versuche mal, es hier zu beschreiben: Wenn ein Report einen Subreport aufrufet, enthält dieser einen Ausdruck, der den Subreport beschreibt. Das ist im Normalfall ein String mit einem Dateinamen. Nun liegen bei einer ADempiere-Installation, die per Webstart gestartet wird, die *.jrxml- und die *.jasper-Dateien ja nicht einfach in der Gegend herum, sondern sind normalerweise im Classpath (oder vielleicht auf einem Server per HTTP). Außerdem weiss man ja auch nie, ob eine *.jrxml oder eine *.jasper vorliegt, ob also noch etwas compiliert werden muss. Also müssen diese Dateien zuerst heruntergeladen und aufbereitet werden. Dann liegen sie in einem definierten Verzeichnis (und zwar /tmp) und können ausgeführt werden. Dabei gibt es nun zwei Probleme:

1. ADempiere muss wissen, welche Dateien es herunterladen und aufbereiten muss 2. Reporte müssen etwas über die Umgebung, in der sie laufen, wissen (den Pfad der Subreporte)

Der erste Punkt hört sich erstmal trivial an, ist er aber nicht (und ist damit der Kern des Problems). Sowohl im Web als auch im Classpath gibt es keine Möglichkeit, ein Verzeichnis aller Dateien zu bekommen (Man könnte die Information über die Subreports aus dem Hauptreport extrahieren, hat man aber nicht). Also gibt es folgende Konvention (im Quellcode ab Zeile 474, Code von trifon): Zu einem im Paket "de.bayen.freibier.reports" befindlichen Report "MeinBericht.jrxml" sucht ADempiere eigenständig die Datei "MeinBerichtSubreport1.jrxml" und wiederholt das mit den Zahlen 1-9. Diese Datei wird dann ggf. übersetzt und auch in das Zielverzeichnis (also /tmp) kopiert. Dann wird der Dateipfad in den Parameter "de_bayen_freibier_reporte_MeinBerichtSubreport1" geschrieben.

Was heisst das nun?

Wir nennen unseren Subreport zu "MeinBericht" schon in iReport "MeinBerichtSubreport1". Dann erzeugen wir im Hauptreport einen Parameter mit dem Namen "de_bayen_freibier_reporte_MeinBerichtSubreport1" und dem Default-Wert "./MeinBerichtSubreport1.jasper". Das läuft dann sowohl in iReport als auch in ADempiere, wenn man das in das angegebene Paket installiert.

Was es dazu noch zu sagen gibt:

Übrigens habe ich gesehen, das auch eine System-Property "org.compiere.report.path" gesetzt wird. Diese enthält dann "/tmp". Aber wie gesagt - da ist eh nur das drin, was ADempiere vorher bereits identifizieren konnte. Also fällt das z.B. für eingebettete Grafiken etc. aus.

Alles in allem ist die Methode, wie hier Dinge nachgeladen werden, nicht so toll. Eigentlich kann man in JasperReports hierfür Handler definieren. Dann könnte man z.B. auch eingebettete Grafiken etc. aus dem Classpath laden. Hier gibt es noch Raum für Verbesserungen.

Zur Benutzung des /tmp-Verzeichnisses siehe auch ADempiere on a terminalserver.

Reporte komplett modularisieren

Wenn man Reporte komplett modularisieren will, bedeutet das, das der Report, den man offiziell aufruft, eigentlich keinen einzigen Tintenklecks Inhalt enthält, sondern lediglich Subreports aufruft, die dann z.B. den Header, den Footer und verschiedene, kombinierbare Inhalte enthält. Dabei gibt es verschiedene Dinge zu beachten:

  • Wie rufe ich die Unterreporte richtig auf? (siehe oben zu Suchpfaden für externe Dateien)
  • Wie übergebe ich Parameter an den Subreport?
  • Wie definiere ich Queries in Subreporten (genauso wie immer, aber halt im Subreport und nicht im Hauptreport)
  • Wie sorge ich für ein anständiges Seitenlayout? (Siehe unten)

Um ein ordentliches Seitenlayout hinzubekommen, bietet es sich an, z.B. den Header als einen eigenen Subreport zu implementieren. Hier kann man dann einen eigenen Query benutzen, um den Namen der Firma, den Benutzer, ein Logo oder sonstige Informationen zu erhalten. Der Vorteil ist hier vor allem, das man - auch wenn man sehr viele Reporte erstellt - diesen Seitenkopf recht schnell an einer einzigen Stelle anpassen kann.

Wenn man nun aber seine eigentlichen Reporte auch modularisiert, kann man diese später mit mehreren zusammensetzen. Dazu ruft man den Report, der die eigentliche Logik (den Query etc.) enthält, als Subreport auf. Dieser Subreport enthält dann keine Seitenheader und -footer. Er hat eine Breite, die bereits direkt ohne Rand gerechnet ist (der Rand ist im Haupt-Report definiert). Ein Trick hierbei liegt in der Frage, wo dieser Subreport nun hingehört. Man kann ihn nicht in die Bänder "Title" oder "Summary" setzen. Wird der Subreport länger als eine Seite, wird dann nämlich kein Seitenheader und -footer erzeugt. Also muss er zwischen Header und Footer. Als "Columnheader" kann man ihn auch nicht nehmen. Dieser darf (wie der Seitenheader) nicht länger als eine Druckseite sein (logisch, weil die neue Seite ja einen neuen Header auslösen würde). Also muss er in das "Detail"-Band. Dieses wird allerdings nur ausgegeben, wenn es Daten gibt (und zwar für jede Zeile einmal). Also muss ich dafür sorgen, das der Hauptreport genau eine Zeile Daten enthält. Dazu stelle ich den Query dort auf "SELECT 1;". Dann geht's! :-)

Etwas schwieriger ist es wohl, Informationen über die aktuelle Seitenzahl oder gar ein "Seite x von y" an einen Header-Subreport zu übergeben. Ich habe es noch nicht probiert, könnte mir aber vorstellen, das man mit der Übergabe entsprechender Parameter eine Seitenzahl schaffen könnte (glaube aber nicht, das das mit der Gesamtzahl auch geht).

Links mit Beispielen von Subreports und Datasets


Vorbereiten von *.jasper-Dateien mit unterschiedlichen JasperReports-Versionen

Grundsätzlich kann man unkompilierte *.jrxml-Dateien benutzen. Diese werden vom ReportStarter (die Programmklasse, die JasperReports in iDempiere integriert) automatisch kompiliert und dann ggf. gecachet. Eigentlich gibt es also keinen Grund, vorkompilierte Dateien zu benutzen. Wer das dennoch möchte, kann das mit dem "Classic" ReportStarter folgendermassen machen. Das folgende Beispiel ist für ADempiere 361 (Ich habe es nicht angepasst, weil ich inzwischen JasperReportsFreiBier benutze, das dieses Problem etwas anders löst.)

Meine Report-Dateien (*.jrxml) speichere ich in das Eclipse-Projekt meines Plugins. Dort habe ich dann ein Ant-buildskript erstellt. Dieses kompiliert die XML-Datei in eine *.jasper-Datei und benutzt dazu die Version der Bibliothek, die sich im benachbarten Projekt der ADempiere-Basis befindet.

Hier meine build.xml:

<project default="jasperreports">
	
	<path id="classpath">
		<!--
		<fileset dir="../adempiere361/">
			<include name="**/lib/*.jar"/>
		</fileset>
		-->
		<fileset dir="../adempiere361/JasperReportsTools/lib">
			<include name="**/*.jar"/>
		</fileset>
		<fileset dir="../adempiere361/tools/lib">
			<include name="**/*.jar"/>
		</fileset>
	</path>
		
	<taskdef name="jrc" classname="net.sf.jasperreports.ant.JRAntCompileTask">
		<classpath refid="classpath"/>
	</taskdef>
	
	<target name="jasperreports">
		<jrc srcdir="src">
			<classpath refid="classpath"/>
			<include name="**/*.jrxml"/>
		</jrc>
	</target>
</project>


Parameter in Berichten

ADempiere übergibt verschiedene Parameter an JasperReports. Diese muss man nicht benutzen, kann es aber, indem man sie in iReport als Parameter definiert. Dabei muss man einfach einen Parameter des angegebenen Namens (Gross- und Kleinschreibung beachten) anlegen. Um seine Reporte auch in iReport sinnvoll testen zu können, sollte man dann noch einen intelligenten Default-Wert angeben, der einem beim Test halbwegs sinnvolle Daten in die Ausgabe zaubert. Natürlich kann es von Fall zu Fall auch sinnvoll sein, einen Default-Wert im SQL-Code, der diese Parameter benutzt, z.B. mit COALESCE(...) zu erzeugen.

Ich konnte keine Dokumentation zu den Parametern finden und habe dann JasperReports/src/org/compiere/report/ReportStarter.java als authoritative Quelle benutzt.

  • AD_CLIENT_ID - Erlaubt Zugriff auf den Mandanten, der momentan aktiv ist. Leider gibt es kein direktes Feld für die Organisations-ID, beide kann man allerdings auch über AD_PINSTANCE_ID extrahieren.
  • AD_ROLE_ID - Die Rolle, als der der Benutzer gerade eingeloggt ist. Rollen bieten Zugriff auf Rechte und erlauben es daher, in einem Report gewisse Abstufungen zu machen, auf welche Tabellen man zugreift oder nicht.
  • AD_USER_ID - Die Benutzer-ID erlaubt z.B. Zugriff auf den Benutzernamen.
  • AD_PINSTANCE_ID - Erlaubt Zugriff auf eine Tabelle für Prozess-Instanzen. Dort gibt es z.B. Zugriff auf die Organisation.
  • RECORD_ID - Wird nur ausgefüllt, wenn momentan ein Record ausgewählt ist. Das ist z.B. der Fall, wenn der Report aus einem normalen Datensatz-Fenster heraus mit einem Druck auf den "Drucken"-Button aufgerufen wird.
  • CURRENT_LANG - enthält den String der eingestellten Sprache (z.B. "en-US")
  • REPORT_LOCALE - enthält das java.util.Locale-Objekt zur eingestellten Sprache
  • RESOURCE - enthält ein Objekt vom Typ java.util.PropertyResourceBundle, aber nur, wenn eine entsprechende Properties-Datei der Art MeinReportName_de.properties vorhanden ist. Man kann hiermit Mehrsprachigkeit eines Reports verwirklichen, indem man statische Texte in entsprechende Properties-Dateien auslagert und dann über dieses Objekt anspricht. (Habe ich selber noch nicht ausprobiert)
  • Subreports - Für jeden erkannten Subreport wird ein Parameter mit dem entsprechenden Pfad angelegt (siehe oben).
  • Prozess-Parameter - Sind im ADempiere Application Dictionaty Parameter definiert, so werden diese selbstverständlich auch an den Report übergeben. Diese werden für gewöhnlich vom Benutzer eingegeben, nachdem er einen Report aus dem Menü heraus gestartet hat. Handelt es sich um From-To-Parameter, so werden z.B. aus "zeitraum" die Parameter "zeitraum1" und "zeitraum2" erzeugt. (Im Quelltext werden übrigens ProcessParameters und ProcessInfoParameters eingefügt, deren Unterschied mir bisher nicht ganz klar ist...)

Wer den patch von JasperReports with Window Sql-Clauses schon integriert hat, bekommt auch noch:

  • CONTEXT - This parameter keeps the ADempiere Context. This has nothing to do with the issue talked about at this page but you could use that if you want.
  • REPORT_WHERE - This parameter contains the whole WHERE clause that gives you the records that are actual shown in your tab. It does not contain the word "WHERE" so you can use it in a wider term and combine it with AND to filter it even more if you want. If there is not defined any search this parameter contains "TRUE" so you can safely insert it in your WHERE clause at any point.
  • REPORT_ORDERBY - This parameter contains the ORDER clause that is defined in the Application Directory's Tab definition. You can use it in your Query and combine inside a wider term too. If there is defined no order you get the term "0-1". In PostgreSQL (and I believe in Oracle too) you can use this inside a ORDER BY clause and it does nothing. So it does not harm if you use this variable to construct your query.
  • REPORT_SORT - If you changed the sorting inside the Tab by clicking on a column header you get a sort description here. It is similat to the REPORT_ORDERBY value. If you did not sort your table it contains also the value "0+1" so you can safely insert it into your query string.