Python-Code für Office-Berichte
Produktlinie
Standard
|Expert
Betriebsart
CLOUD ABO
|ON-PREMISES
Module
Leistung & CRM
Budget & Teilprojekt
Fremdkosten
Ressourcenplanung
Business Intelligence
Dieser Artikel gilt für Vertec Versionen ab 6.5.0.20. Eine Beschreibung für ältere Versionen finden Sie hier . Was sich im Vergleich zu den vorherigen Versionen geändert hat, finden Sie im Artikel Python-Code für Office-Berichte Vorher - Nachher .
Der Dateninhalt eines Office-Berichts wird berechnet durch Python-Code, welcher als Berichts-Definition hinterlegt ist:
Wie dieser Code genau aufgebaut wird, ist hier nachfolgend detailliert beschrieben.
Die Datenlogik eines Office-Berichts basiert auf einer Hierarchie von sogenannten Tables.
Die einzelnen Objekte sind dann die "Zeilen" einer solchen Table: Für jedes Objekt (einzeln oder in der Liste, auf der der Bericht ausgeführt wird) wird zu Laufzeit eine Zeile (row
) angelegt.
Jedes Band in Word oder Excel verweist auf eine Table. Die innerhalb des Bands verwendeten Variablen verweisen auf die Felder .
Eine Tabelle wird wie folgt deklariert:
class Name(Table):
Der Name kann frei gewählt werden, muss aber innerhalb der Berichtsdefinition eindeutig sein.
# Beispiel eines minimalen Projekt-Reports class Projekt(Table): businessclass = "Projekt"
Auf dieser Table wurden (noch) keine eigenen Felder definiert . Für diese Objekte stehen also einfach alle normalen Vertec-Member zur Verfügung, wie sie auch über OCL verfügbar sind.
Damit der Aufruf der Vertec-Member im Bericht funktioniert, muss die Table einer eindeutigen Businessklasse (Projekt, Projektbearbeiter, Adresseintrag etc.) zugeordnet werden. Diese wird in der Deklaration der Table mittels businessclass=
angegeben.
Anmerkung: Wird keine Businessclass angegeben, werden die Werte einfach als Strings ausgegeben. Das funktioniert für einfache Anzeigen, gerechnet werden kann damit aber nicht. Wir empfehlen deshalb, die Businessclass immer anzugeben.
Es gibt immer eine Haupttabelle, von der alles ausgeht. Diese Haupttable wird vom System dadurch identifiziert, dass sie nicht Subtable ist von einer anderen Table.
Werden mehrere Tables verwendet, die keine Übertabelle haben, muss eine davon als Haupttabelle gekennzeichnet werden. Das geschieht mit der Variable main_table
:
class Projekt(Table): businessclass = "Projekt" main_table = Projekt
Die Zuweisung einer Table als Sub-Table erfolgt über eine Feld-Deklaration. Das genaue Vorgehen ist im Abschnitt Table-Felder (Subtables) beschrieben.
Alle Tables ausser der Haupttabelle (main_table) werden als Feld in einer anderen Table angegeben und dabei eine Berechnung mitgegeben. Bei der Haupttabelle, der main_table, ist das nicht so. Dort wird einfach die Liste (rootlist
) übergeben, auf der der Bericht ausgeführt wird (ausgeführt auf einem einzelnen Objekt ist das eine Liste mit einem Eintrag).
Möchte man die Haupttabelle vorberechnen (zum Beispiel, um eine Liste zu filtern oder zu sortieren), kann dies mit der Methode calculate_main_table
geschehen. Diese gibt es in 2 Varianten:
calculate_main_table
ist eine Funktion, welche ein Context-Objekt
enthält und eine Table Instanz des richtigen Typs zurückgeben soll. Signatur der Funktion ist calculate_main_table(context)
, Rückgabewert ist eine Table Instanz.def calculate_main_table(context): projektlist = context.rootlist.evalocl("self->orderby(code)") # Hier können Berechnungen gemacht werden return Projekt(context, projektlist)
calculate_main_table
ist ein String. Dieser wird als OCL Expression ausgewertet und muss eine Liste ergeben. Die Expression wird auf der ursprünglichen Liste des Berichts ausgewertet. Beispiel, wie oben, aber als Expression:
calculate_main_table = "self.oclAsType(Projekt)->orderby(code)->asSet"
self
referenziert dabei die übergebene Rootlist. Diese weiss selbst nichts vom Typ ihrer Objekte, deshalb muss diese angegeben werden (.oclAsType(Klasse)
).
Sollen die Objekte einer Liste nicht pro Objekt, sondern in einer Liste angezeigt werden, muss eine übergeordnete Bericht-Tabelle erzeugt werden, welche den Rahmen für die Liste bietet, selbst aber kein Objekt hat.
Dafür kann die Haupttabelle als SingeRowTable
deklariert werden. In einer solchen Tabelle wird vom System automatisch genau eine row
erzeugt, welche die Liste dann als Subtable enthalten kann.
# Projektliste mit Reporttable class Projekt(Table): fields = [] class Report(SingleRowTable): fields = [ TableField("projekte", Projekt), ] def initialize_row(context, row): row.projekte = Projekt(context, context.rootlist)
Auch die SingleRowTable kann bei Bedarf via calculate_main_table
vorberechnet werden. Sie hat die gleichen Eigenschaften wie die normale Table, wertet aber kein OCL aus. Es können darauf also keine OCL Felder angelegt und auch die Context-Variablen
nicht via OCL verwendet werden, da es kein Objekt gibt, auf dem die Expressions ausgewertet werden könnten.
Für alle Objekte, für die der Bericht ausgeführt wird, wird eine "Zeile" der Table angelegt. Eine Zeile heisst row
.
Auf der Tabelle gibt es die Methode initialize_row, um die Objekte zu initialisieren. Darin können Berechnungen gemacht, Felder gefüllt und Context-Variablen gesetzt werden. Die Syntax lautet wie folgt:
def initialize_row(context, row):
Diese Methode wird automatisch für jedes einzelne Objekt aufgerufen.
# Projektliste mit Reporttable class Projekt(Table): fields = [] class Report(SingleRowTable): fields = [ TableField("projekte", Projekt), ] def initialize_row(context, row): row.projekte = Projekt(context, context.rootlist)
Tables können auch im Code aufgebaut werden. Dies wird dann benötigt, wenn verschiedene Objekte dargestellt werden sollen, welche nicht zur gleichen Businessklasse gehören, und also nicht automatisch berechnet werden können.
Dafür steht auf der Tabelle eine add_row
Methode zur Verfügung, welche ein zeilenweises Aufbauen der Tabelle erlaubt. Die auf der Tabelle definierten Felder stehen als Eigenschaften des row-Objektes zur Verfügung.
class Details(Table): fields = [ TextField("code"), CurrencyField("value1"), CurrencyField("value2"), ] class Invoice(Table): fields = [ TableField("details", Details, "calculate_details") ] def calculate_details(context): phasen = context.evalocl("phasentotale.phase->asSet") for ph in phasen: row = context.table.add_row(ph) row.value1 = ph.evalocl("sumMinutenInt") row.value2 = ph.evalocl("sumWertExt")
Die Anzahl der Objekte einer Tabelle kann über len(table)
eruiert werden.
Als Datenfelder werden die normalen Vertec-Member verwendet. Diese können einfach in der Berichtsvorlage referenziert werden, ohne dass es eine Feld-Deklaration braucht im Code (siehe Office-Berichte - Felder ).
Zusätzlich können weitere Felder deklariert und z.B. mit Berechnungen gefüllt werden.
Es gibt zwei Arten von Feldern: OCL Felder und einfache Felder. Die OCL Felder berechnen ihren Wert über eine OCL Expression. Bei den einfachen Feldern muss der Inhalt manuell bzw. mit einer Python Funktion abgefüllt werden.
Felder | OCL Felder | Beschreibung |
---|---|---|
TextField |
OclTextField | Stellt einen String-Wert dar |
CurrencyField |
OclCurrencyField | Stellt eine Fixkomma Zahl dar, wird üblicherweise für Geld-Beträge verwendet. Formatierung gemäss Ländereinstellung. |
IntegerField |
OclIntegerField | Stellt eine Ganzzahl dar |
MinuteField |
OclMinuteField | Stellt einen Integer-Wert als Minuten dar. Formatierung gemäss Einstellungen in Vertec. |
BooleanField |
OclBooleanField | Stellt einen Wahr/Falsch Wert dar |
DateField |
OclDateField | Stellt ein Datum dar |
DateTimeField |
OclDateTimeField | Stellt ein Datum mit Zeitanteil dar |
ImageField |
OclImageField | Stellt ein Bild dar |
TableField |
OclTableField | Enthält als Wert eine Subtable. Table-Fields erlauben die Darstellung von hierarchischen Datenstrukturen. |
OCL Felder enthalten als ersten Parameter den Feldnamen (der in der Berichtsvorlage referenziert wird), sowie als zweiten Parameter eine OCL Expression für die Berechnung:
OclTextField("projektleiter", "projektleiter.kuerzel")
Das Resultat der Expression muss vom Datentyp sein, für welchen das Feld deklariert wurde (OclTextField = String, OclIntegerField = Ganzzahl etc.).
Der zweite Parameter ist optional. Entspricht er dem ersten Parameter (z.B. OclTextField("code", "code")
, kann er weggelassen werden oder das Feld direkt im Bericht referenziert
werden, ohne dass dafür überhaupt ein Feld deklariert werden muss.
Einfache Felder (ohne Ocl
-Präfix) enthalten ebenfalls als ersten Parameter den Feldnamen:
CurrencyField("offeneLeistungen")
Ohne weitere Angaben stellen diese nicht automatisch berechnete Felder dar, welche beim Berechnen der Table im Code manuell mit Werten gefüllt werden können, zum Beispiel in einem initialize_row :
class Projekt(Table): businessclass = "Projekt" fields = [ CurrencyField("offeneLeistungen"), CurrencyField("offeneSpesen"), ] def initialize_row(context, row): row.offeneLeistungen = context.evalocl("offeneleistungen.wertext->sum") row.offeneSpesen = context.evalocl("offeneSpesen.wertext->sum")
Optional kann als zweites Argument eine Python Funktion angegeben werden, die automatisch zur Berechnung des Feldes aufgerufen wird. Beispiel:
class Projekt(Table): businessclass = "Projekt" fields = [ CurrencyField("offeneLeistungen", "calc_leistungen"), CurrencyField("offeneSpesen", "calc_spesen"), ] def calc_leistungen(context): return context.evalocl("offeneleistungen.wertext->sum") def calc_spesen(context): return context.evalocl("offenespesen.wertext->sum")
Es ist auch möglich, dieselbe Funktion in mehreren Feldern zu verwenden, durch Übergabe des fieldnames
:
class Projekt(Table): businessclass = "Projekt" fields = [ CurrencyField("offeneLeistungen", "calc_summen"), CurrencyField("offeneSpesen", "calc_summen"), ] def calc_summen(context, fieldname): if fieldname == "offeneLeistungen": return context.evalocl("offeneleistungen.wertext->sum") if fieldname == "offeneSpesen": return context.evalocl("offenespesen.wertext->sum")
Etwas anders als die anderen Feld-Typen sind die Table-Felder aufgebaut, also die Felder, mit welchen die Sub-Tables angegeben werden.
Diese Felder haben drei Argumente:
TableField("leistungen", "Leistungen", "calculate_leistungen")
Berechnung. Das dritte Argument ist optional und kann wie folgt verwendet werden:
OclTableField
), muss das Ergebnis der Expression eine Liste von Businessobjekten sein, welche der Table Deklaration entsprechen.
OclTableField("leistungen", "Leistung", "offeneLeistungen->orderby(datum)")Falls die OCL Expression dem Namen der Table entspricht, so kann diese weggelassen werden. Der Bericht Mechanismus verwendet so den Namen als OCL Expression:
OclTableField("offeneLeistungen", "Leistung")
Python Funktion: Falls die Berechnung per Python Funktion erfolgt (Deklaration ohne Präfix Ocl: TableField
), muss die Funktion eine Liste von Businessobjekten zurückgeben, welche der Table Deklaration entsprechen.
In der Berechnungsfunktion eines Table Fields (3. Argument als Python Funktion) steht ein resultierendes Table Objekt als context.table
zur Verfügung. Damit kann auf eine bereits angelegte Table zugegriffen werden, was das Anlegen einer Row via context.table.add_row()
ermöglicht.
# Projektliste mit Leistungen class Leistung(Table): fields = [ IntegerField("einwert"), ] class Projekt(Table): fields = [ TableField("leistungen", Leistung, "calc_leistungen"), ] def calc_leistungen(context): for l in context.evalocl("offeneleistungen"): row = context.table.add_row(l) row.einwert = 1234
TableField("offeneLeistungen", "Leistung")
Jeder Berechnung im Rahmen der Bericht-Generierung wird das Context-Objekt mitgegeben. Dieses wird so durch alle Berechnungsfunktionen durchgeschlauft und hat folgende Eigenschaften:
Das Context-Objekt verfügt über die Methode evalocl(<expression>)
, welche eine OCL Expression auf dem aktuellen Objekt auswertet.
context.evalocl("offeneLeistungen.wertext->sum")
Als optionales zweites Argument neben der Expression kann ein Objekt oder eine Liste übergeben werden. In diesem Fall wird die Expression auf das übergebene Objekt bzw. Liste angewendet.
context.evalocl("offeneLeistungen.wertext->sum", phasenliste)
Mit der Methode context.translate(text)
können Texte gemäss Bericht-Sprache (context.language
) übersetzt werden.
Der im Code zuletzt gesetzte Wert von context.language
wird zur Übersetzung der Texte in der Berichtsvorlage verwendet.
Wird ein unbekannter Sprachcode eingegeben, dann werden die originalen Texte angezeigt. Der Bericht wird also einfach nicht übersetzt.
Dieses Übersetzungssystem funktioniert nicht für MLString-Felder, da diese einen anderen Übersetzungsmechanismus haben. Welches MLString-Felder sind und wie diese übersetzt werden, finden Sie im Artikel über die Mehrsprachigkeit mit Vertec beschrieben.
Möchte man solche Begriffe in Office-Berichten übersetzen, muss man auf die OCL-Methode .asstringbylanguage zurückgreifen:
context.evalocl("typ.text.asStringByLanguage(language)")
Mögliche Sprachen/Parameter sind 'DE', 'DD' (ab 6.5.0.9), 'FR', 'IT', 'EN' oder 'NV'. Eine Unterscheidung nach Jargon ist bei MLString-Feldern nicht möglich.
class Leistung(Table): businessclass = "OffeneLeistung" fields = [ OclTextField("text", "typ.text.asStringByLanguage(language)"), ] def initialize_row(context, row): context.language = "FR"
bzw.
class Leistung(Table): businessclass = "OffeneLeistung" fields = [ TextField("text"), ] def initialize_row(context, row): context.language = context.language[:2] row.text = context.evalocl("typ.text.asStringByLanguage(language)")
Mit context.language[:2]
eruiert man die ersten zwei Zeichen des bestehenden Language-Attributs. Da in Berichten die Sprache standardmässig immer mit dem Jargon-Suffix hinterlegt ist (DE0..), und dies bei MLString-Feldern nicht möglich ist (siehe oben), erreicht man so die Übersetzung in die aktuelle Berichts-Sprache.
Die folgenden Variablen sind in jedem Bericht definiert:
container |
Der Container, auf dem der Bericht ausgeführt wurde. |
comment | Ab Version 6.6.0.9. Enthält den Kommentar aus dem Druck-Dialog . |
currentobject |
Das jeweils aktuelle Objekt der Berechnung.
|
currentdate |
Das heutige Datum, ohne Zeitteil.context.currentdate |
language |
Diese Variable enthält die Bericht-Sprache (Oberflächensprache oder Projektsprache, falls Übersetzung aktiviert). Sie kann auch gesetzt werden, um den Bericht in einer anderen Sprache auszugeben:
Es kennt folgende Werte: – Dies beeinflusst die mit translation markierten Texte in der Berichtsvorlage. Für die Übersetzung von Text im Python Code kann die Methode context.translate() verwendet werden. |
optarg |
Das optionale Adressen-Argument bei Berichten. Um mit dem Objekt weiterzuarbeiten, kann wie folgt darauf zugegriffen werden: class Projekt(Table): fields = [ TextField('adresstext', 'calcadresse'), ] def calcadresse (context): return context.optarg.adresstext |
opportunity |
Ab Version 6.7.0.6. Für die Verwendung in Outlook App: E-Mail-Vorlagen . Enthält die in der Outlook App vorausgewählte Opportunität des E-Mails. Falls keine Opportunität vorausgewählt ist oder bei Aufruf des Berichts ausserhalb der Outlook App ist die Variable |
phase |
Ab Version 6.7.0.6. Für die Verwendung in Outlook App: E-Mail-Vorlagen . Enthält die in der Outlook App vorausgewählte Phase des E-Mails. Falls keine Phase vorausgewählt ist oder bei Aufruf des Berichts ausserhalb der Outlook App ist die Variable |
project |
Ab Version 6.7.0.6. Für die Verwendung in Outlook App: E-Mail-Vorlagen . Enthält das in der Outlook App vorausgewählte Projekt des E-Mails. Falls kein Projekt vorausgewählt ist oder bei Aufruf des Berichts ausserhalb der Outlook App ist die Variable |
reportdef |
Ab Version 6.6.0.3. Enthält das Bericht-Objekt, welches den Bericht ausführt. Das ist vor allem für zentralisierten Code nützlich, wenn von mehreren Berichten auf denselben Code zugegriffen wird. |
rootlist |
Die Liste, auf der der Bericht ausgeführt wurde. Normalerweise ist das die eintraege-Liste des Containers, auf dem der Bericht ausgeführt wurde, beziehungsweise eine Liste mit dem Objekt drin, auf dem der Bericht ausgeführt wurde. |
subject | Ab Version 6.6.0.9. Enthält den Betreff der Berichtsregistrierung bzw. aus dem Druck-Dialog . |
var<Table> |
Für jede übergeordnete Table ist eine Variable mit deren aktuellem Objekt definiert. |
var<Table>List |
Für jede übergeordnete Table ist eine Variable mit der Liste der Objekte der Table definiert. |
Weitere Variablen können selbst definiert und dem Context-Objekt zugewiesen werden.
firma = vtcapp.getpropertyvalue('Firma') if firma: context.firma = firma
Via set_image(name, value)
kann ein Bild in eine Context-Variable eingelesen werden:
logo = vtcapp.getpropertyvalue('CompanyLogo') context.set_image('logo', logo)
Die Context-Variablen können auf dem Bericht über eine Context-Expression ausgegeben werden.
Context-Variablen können auch in OCL verwendet werden. Der Aufruf erfolgt über den Namen der Variablen (ohne context.
), hier im Beispiel todate
:
class Bearbeiter(Table): fields = [ OclMinuteField("balance", "self->getFerienSaldo(todate)"), ]
Die Variablen sind nur in OCL Feldern
sowie in der context.evalocl
Methode verfügbar. Andere OCL Aufrufe wie vtcapp.evalocl()
oder die evalocl
Methoden auf Listen und Objekten verwenden einen globalen OCL Evaluator und der kennt die Context-Variablen nicht.
Soll beispielsweise eine Context-Variable auf einer Liste angewandt werden, muss evalocl trotzdem auf dem context aufgerufen und die entsprechende Liste als Argument übergeben werden. Statt spesenlist.evalocl(..)
schreibt man also:
context.evalocl("self->select(getProjekt=varProjekt)->orderby(code))", spesenlist)
Via OCL wird nur der Aufruf von OCL-kompatiblen Werten unterstützt (Vertec-Objekte und -Listen, Strings, Integers etc.). Bei inkompatiblen Werten (z.B. Python Dictionaries, Python-Listen, etc) erscheint eine Fehlermeldung.
Kollidieren manuell auf dem Context-Objekt gesetzte Variablen mit Keywords in OCL (self, date, now), erscheint ebenfalls eine Fehlermeldung.
In der Berechnungsmethode eines Feldes kann auf andere Felder derselben Table zugegriffen werden. Dafür gibt auf dem Context eine Methode context.get_fieldvalue(fieldname)
.
class Projekt(Table): fields = [ OclMinuteField("bdgaufwand", "planminutenint"), OclMinuteField("effaufwand", "leistsums->select(projekt=varProjekt)->collect(minutenintoffen+minutenintverrechnet)->sum"), MinuteField("restaufwand", "calculate_restaufwand"), ] def calculate_restaufwand(context): return context.get_fieldvalue("bdgaufwand")-context.get_fieldvalue("effaufwand")
Um vor der Ausführung des Berichts Python-Code ausführen zu können (um beispielsweise Dialoge
anzuzeigen und Variablen auf dem Context-Objekt
zu setzen), gibt es die Methode before_report(context)
. Ist diese Methode vorhanden, wird sie automatisch als erstes aufgerufen.
def before_report(context): # Frage den User nach dem Datum initValues = {} initValues["Stichdatum"] = vtcapp.currentdate() dlgDefinition=""" <Dialog Title="{Translate 'Choose date'}" Width="400"> <Group Orientation="Vertical"> <DatePicker Name="Stichdatum" Label="Stichdatum" /> </Group> <Dialog.Buttons> <Button Text="OK" IsAccept="True" Command="{Binding OkCommand}" /> <Button Text="Cancel" IsCancel="True" Command="{Binding CancelCommand}" /> </Dialog.Buttons> </Dialog> """ ok, values = vtcapp.showcustomdialog(dlgDefinition, initValues) if not ok: return False context.stichdatum = values["Stichdatum"]
Gibt die Methode False
zurück, bricht die Ausführung des Berichts ab. Ansonsten läuft sie weiter (True
ist Standard und muss nicht extra angegeben werden).
Eine Python Funktion wird wie folgt deklariert:
def Funktionsname(context): return XY
Funktionen können innerhalb einer Table definiert werden und sind dann für diese Table verfügbar.
Funktionen, die überall verfügbar sein sollen, müssen ausserhalb der Tables deklariert werden.
Referenzen auf Tables und Funktionen können auf zwei Arten angegeben werden: