Python-Code für erweiterte Office-Berichte

Module: Leistung & CRM
Erstellt: 12.04.2018, Änderung:
context.evalocl() genauer beschrieben.
Mehr ansehen

Die Datenlogik eines erweiterten Office-Berichts basiert auf einer Hierarchie von sogenannten Frames. Ein Frame ist vergleichbar mit einer Tabelle, es hat Zeilen mit Datenfeldern drauf. Die Felder eines Frames werden mit Hilfe von Feld-Definitionen festgelegt.

Der Python Code für einen erweiterten Office-Bericht besteht im Wesentlichen aus der Deklaration der Frames, startend mit dem Haupt-Frame (main_frame) des Reports.

Eine minimale Bericht-Definition muss eine Frame-Deklaration sowie die Zuweisung der main_frame Variable enthalten. Hier wird beispielsweise ein Bearbeiter-Frame mit 2 Feldern definiert:

# Beispiel eines minimalen Mitarbeiter-Reports

class Bearbeiter(Frame):
    businessclass = "Projektbearbeiter"
    fields = [
        OclTextField("name"), 
        OclTextField("kuerzel"),
    ]

main_frame = Bearbeiter

Jedes Band im Word verweist auf ein bestimmtes Frame (bndXXXExp). Die innerhalb des Bands verwendeten Variablen verweisen auf Felder, die im Frame deklariert wurden (siehe Abschnitt Felder weiter unten). Felder, die im Frame nicht deklariert wurden, sind im Word nicht verfügbar.

Das main_frame entspricht dem Standard-Band im Word, welches alles umschliesst.

Frames können anderen Frames als Sub-Frames zugewiesen werden. Dadurch können hierarchische Strukturen abgebildet werden:

Felder

Auf den Frames werden Felder erzeugt. Auf diese Felder kann später im Report referenziert 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.

Feld-Typen

Es gibt folgende Arten von Feldern:

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 Einstellung 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
FrameField OclFrameField Enthält als Wert ein weiteres Frame. Frame-Fields erlauben die Darstellung von hierarchischen Datenstrukturen.

OCL-Felder

OCL Felder enthalten als ersten Parameter den Feldnamen. Entspricht dieser dem Member, welches für die Berechnung verwendet werden soll, muss nichts weiter angegeben werden:

OclTextField("code")

Optional kann als zweites Argument eine OCL Expression angegeben werden:

OclTextField("name", "projektleiter.name")

OCL-Felder sind nur auf Frames zulässig, welche einer eindeutigen Businessklasse zugeordnet werden können. Diese wird in der Deklaration des Frames mittels businessclass= angegeben.

Einfache Felder

Einfache Felder (ohne OCL-Präfix) enthalten ebenfalls als ersten Parameter den Feldnamen. Ohne weitere Angaben stellen diese nicht automatisch berechnete Felder dar, welche beim Berechnen des Frames im Code mit Werten gefüllt werden können.

CurrencyField("value1")

Optional kann als zweites Argument eine Python Funktion angegeben werden, die zur Berechnung des Feldes aufgerufen wird. Beispiel:

class Rechnnung(Frame):
    fields = [CurrencyField("summe", "calc_summe")]

    def calc_summe(context):
        rechnung = context.currentobject
        sum = 0.0

        for leist in rechnung.leistungen:
            sum += leist.wertext

        return sum

Frame-Felder (Subframes)

Etwas anders als die anderen Feld-Typen sind die Frame-Felder aufgebaut, also die Felder, mit welchen die Sub-Frames angegeben werden.

Diese Felder haben drei Argumente:

FrameField("leistungen", "Leistungen", "calculate_leistungen")
  1. Das erste Argument enthält wie immer den Namen, mit welchem im Report auf dieses Subset zugegriffen werden kann, beispielsweise als Band-Expression.
  2. Mit dem zweiten Argument wird die entsprechende Frame-Deklaration übergeben:
  3. Mit dem dritten Argument wird angegeben, wie der Inhalt des Sub-Frames berechnet wird.
    Falls die Berechnung per OCL Expression erfolgt (OclFrameField), muss das Ergebnis der Expression eine Liste von Businessobjekten sein, welche der Frame Deklaration entsprechen. Falls der Name, also das erste Argument, der OCL Expression entspricht, kann dieses Argument bei OclFrameFields auch weggelassen werden:

    OclFrameField("offeneLeistungen", "Leistung")

    Falls die Berechnung per Python Funktion erfolgt (FrameField), wie im obigen Beispiel dargestellt, muss die Funktion eine Frame-Instanz des richtigen Typs zurückgeben.

Frames werden wie folgt aufgebaut:

Frames

Ein Frame deklarieren

Syntax:

Class XY(Frame):

Das Hauptframe zuweisen (main_frame)

Es gibt ein Hauptframe. Dieses heisst main_frame und muss ein deklariertes Frame zugewiesen bekommen:

main_frame = XY

Sub-Frames zuweisen (FrameField)

Frames können anderen Frames als Sub-Frames zugewiesen werden. Dies geschieht mit einem Frame-Feld (siehe Abschnitt Felder weiter oben).

fields = [FrameField("leistungen", "Leistungen", "calculate_leistungen")]

Frames aufbauen

Bei Frames, bei welchem die Objekte alle zur gleichen Businessklasse gehören, können die Felder via OCL berechnet werden:

class Leistungen(Frame):
    businessclass="OffeneLeistung"
    fields = [OclDateField("datum"),
        OclTextField("projekt", "projekt.code"),
        OclTextField("text"),
        OclMinuteField("minutenext"),
        OclCurrencyField("ansatzext"),
        OclCurrencyField("wertext"),
    ]

Frames im Code aufbauen

Frames können auch im Code aufgebaut werden. Dies wird dann benötigt, wenn in einer Zeile, also in einem Band, verschiedene Objekte dargestellt werden sollen, welche nicht zur gleichen Businessklasse gehören, und also nicht automatisch berechnet werden können.

Dafür steht auf dem Frame Objekt eine add_row Methode zur Verfügung, welche ein zeilenweises Aufbauen des Frames erlaubt. Die auf dem Frame definierten Felder stehen als Eigenschaften des Row-Objektes zur Verfügung.

class DetailsFrame(Frame):
    fields = [TextField("code"),
        CurrencyField("value1"),
        CurrencyField("value2"),
    ]

class Invoice(Frame):
    fields = [FrameField("details", DetailsFrame, calculate_details)]

Die einzelnen Zeilen eines solchen Frames werden via add_row() hinzugefügt. Bei der Berechnung werden dann die Felder des Frames gefüllt.

def calculate_details(context):
    phasen = context.evalocl("phasenaufrechnung")
    frame = DetailsFrame(context)
    for ph in phasen:
        row = frame.add_row(ph)
        row.value1 = ph.evalocl("sumMinutenInt")
        row.value2 = ph.evalocl("sumWertExt")

calculate_main_frame

Alle Frames ausser dem main_frame werden als Feld in einem anderen Frame angegeben und dabei eine Berechnung mitgegeben.

Beim allerersten Frame, dem main_frame, ist das nicht möglich. Möchte man dieses erste Frame berechnen, kann dies mit der Methode calculate_main_frame geschehen.

Dies braucht man immer dann, wenn man im main_frame etwas berechnen möchte (und nicht beispielsweise alle Projekte bzw. das Objekt, auf welchem man den Report startet) haben möchte.

Die Methode calculate_main_frame gibt es in 2 Varianten:

  • calculate_main_frame ist eine Funktion
    calculate_main_frame ist eine Funktion, welche ein Context-Objekt erhält und eine Frame-Instanz des richtigen Typs zurückgeben soll.Signatur der Funktion ist calculate_main_frame(context), Rückgabewert ist eine Frame Instanz.Beispiel:Der Report ist auf einer Liste von Projekten registriert, das Main-Frame stellt aber eine Liste von Bearbeitern dar. Die Main-Frame Klasse heisst Bearbeiter.

    main_frame=Bearbeiter
    
    def calculate_main_frame(context):
        bearbeiter_list = context.evalocl("bearbeiter")
        frame = Bearbeiter(context)
        for b in bearbeiter_list:
            row = frame.add_row(b)
            row.specialvalue = calculate_some_value()
        return frame
  • calculate_main_frame ist ein String
    calculate_main_frame ist ein String. Dieser wird als OCL Expression ausgewertet und muss eine Liste ergeben. Die Expression wird auf der ursprünglichen Liste des Reports ausgewertet.Beispiel, wie oben, aber nur mit Expression:

    main_frame = Bearbeiter
    calculate_main_frame = "bearbeiter"

Python Funktion

Eine Python Funktion wird wie folgt deklariert:

def Funktionsname(context):
     return XY

Funktionen können innerhalb eines Frames zugewiesen werden und sind dann in diesem Frame verfügbar. Sollen Funktionen überall verfügbar sein, müssen Sie ausserhalb der Frames deklariert werden.

Jeder Berechnung im Rahmen der Report-Generierung wird das Context-Objekt mitgegeben. Dieses wird so durch alle Berechnungsfunktionen durchgeschlauft und hat folgende Eigenschaften:

Das Context-Objekt

Variablen

Die folgenden Variablen sind in jedem Report definiert:

currentobject Das jeweils aktuelle Objekt der Berechnung.

context.currentobject

currentdate Das heutige Datum, ohne Zeitteil.
context.currentdate
optarg Das optionale Adressen-Argument bei Berichten. Um mit dem Objekt weiterzuarbeiten, kann wie folgt darauf zugegriffen werden:
class Projekt(Frame): 
    fields = [OclTextField("code"),
OclTextField("beschrieb"),
OclTextField("creationdatetime"),
TextField('adresstext', 'calcadresse'),
]

    def calcadresse (context):
        return context.optarg.adresstext

main_frame = Projekt
rootlist Die Liste, auf der der Report ausgeführt wurde. Normalerweise ist das die eintraege-Liste des Containers, auf dem der Report ausgeführt wurde, beziehungsweise eine Liste mit dem Objekt drin, auf dem der Report ausgeführt wurde.
container Der Container, auf dem der Report ausgeführt wurde.
var<frame> Für jedes übergeordnete Frame ist eine Variable mit dessen aktuellem Objekt definiert.
var<frame>List Für jedes übergeordnete Frame ist eine Variable mit der Liste der Objekte des Frames definiert.

Weitere Variablen können selbst definiert und dem Context-Objekt zugewiesen werden.

firma = vtcapp.getpropertyvalue('Firma')
if firma:
    context.firma = firma

Diese können später auf dem Report direkt über eine Context-Expression ausgegeben werden.

Methode evalocl(expression)

Ausserdem verfügt das Context-Objekt über die Methode evalocl(<expression>), welche eine OCL-Expression auf dem aktuellen Objekt (currentobject) auswertet.

context.evalocl("offeneLeistungen.wertext->sum")

ist äquivalent zu

context.currentobject.evalocl("offeneLeistungen.wertext->sum")

Wenn das letzte Argument von context.evalocl None ist oder nicht angegeben wird, dann wird das currentobject des contexts verwendet. Man kann aber auch ein Objekt oder eine Liste als Argument übergeben; in diesem Fall wird die Expression auf das übergebene Objekt bzw. Liste angewendet.

context.evalocl("offeneLeistungen.wertext->sum", phasenliste)

Context-Variablen in OCL verwenden

Ab Vertec 6.2.0.7 können die Context-Variablen direkt in OCL verwendet werden. Berechnungen der folgenden Art können dadurch stark vereinfacht werden:

class Bearbeiter(Frame):
    businessclass = "Projektbearbeiter"
    fields = [
        MinuteField("balance", "calc_balance"),
    ]

    def calc_balance(context):
        return context.currentobject.evalocl("self->getFerienSaldo(%s)" % vtcapp.ocldate(context.todate))

Durch die Möglichkeit der Verwendung der Context-Variable in OCL reduziert sich dieser Code auf:

class Bearbeiter(Frame):
    businessclass = "Projektbearbeiter"
    fields = [
        OclMinuteField("balance", "self->getFerienSaldo(todate)"),
    ]

Es gelten folgende Regeln:

  • Namensgebung: Die Namen der automatisch gesetzten Varieblen sind gleich wie auf dem Contextobjekt, nämlich varXY und varXYList. Die manuell auf dem Contextobjekt gesetzten Variablen können mit Keywords in OCL kollidieren (self, date, now). In diesem Fall erscheint eine Fehlermeldung.
  • Die Reporting OCL Variablen werden auf dem privaten OCL Evaluator des ausgeführten Reports angelegt. Das bedeutet, dass die Variablen nur in OCL Expressions von OCL-Feldern oder in der context.evalocl Methode verwendet werden können. Alle anderen OCL Aufrufe, insbesondere die evalocl Methoden von Businessobjekten, verwenden den globalen OCL Evaluator und der kennt die lokalen Report-Variablen nicht. Statt:
    templist = spesenList.evalocl("self->select(getbearbeiter=varBearbeiter)->ordermulti('datum;boldid')")

    verwendet man deshalb:

    templist = context.evalocl("self->select(getbearbeiter=varBearbeiter)->ordermulti('datum;boldid')", spesenList)
  • Es werden nur OCL kompatible Werte als Variablen unterstützt (Vertec-Objekte und -Listen, einfache Werte). Bei inkompatiblen Werten (z.B. Python Dictionaries, Python-Listen, etc) erscheint ein Fehler.

Zugriff auf andere Felder in Feldberechnung

Ab Vertec 6.2.0.8 kann in in der Berechnungsmethode eines Fields auf andere Fields des selben Frames zugegriffen werden. Dadurch müssen Berechnungen nur noch einmal durchgeführt und danach kann direkt mit dem Wert weitergearbeitet werden. Das ist nicht nur übersichtlicher, sondern auch ein Performance-Vorteil.

Dafür gibt auf dem Context eine Methode context.get_fieldvalue(fieldname).

Beispiel

class Projekt(Frame):
    businessclass = "Projekt"
    fields =  [
        OclMinuteField("bdgaufwand", "if (phasen->size>0) then phasen.bdgvalue('planminutenint', duedate, -1)->sum else planminutenint endif"),
        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")

Before Report Logik

Um vor der Ausführung des Reports Python Code ausführen zu können (um beispielsweise Dialoge anzuzeigen und Parameter auf dem Context-Objekt zu setzen), gibt es die Methode before_report(context):

def before_report(context):

  """Frage den User nach dem Datum ab."""
   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"]

Referenzen

Referenzen auf Frames und Funktionen können auf zwei Arten angegeben werden:

  • Referenz als Name: Wird der Referenzname in Anführungszeichen geschrieben, handelt es sich um eine Referenz als Name. In diesem Fall kann die referenzierte Deklaration irgendwo im Code stehen, die Reihenfolge spielt keine Rolle.
  • Direkte Referenz: Wird der Referenzname ohne Anführungszeichen geschrieben, handelt es sich um eine direkte Referenz. In diesem Fall muss die referenzierte Deklaration oberhalb im Code stehen, also bevor darauf referenziert wird.

Die Referenz als Name wurde eingeführt, weil es zu leserlicherem Code führt. So sieht man zuerst, wo und warum etwas referenziert wird, und dann, was es ist.

Zusammenspiel Frames – Bands (Word)

Jedes Band im Word verweist auf ein bestimmtes Frame. Die innerhalb des Bands verwendeten Variablen verweisen auf Felder, die im Frame deklariert wurden. Felder, die im Frame nicht deklariert wurden, sind im Word nicht als Variablen verfügbar.

Einzelreports

Bei Einzelreports wird für das aktuelle Objekt ein Report erzeugt. Hier ist das main_frame das einzelne Objekt.

Listenreports

Bei Listenreports gibt es zwei Varianten:

  1. Für jedes Objekt in der Liste, z.B. für jeden Bearbeiter, soll eine neue Seite im Report erscheinen. In diesem Fall ist der Report gleich aufgebaut wie der Einzelreport.Hier ist das main_frame einfach die Liste:
    class Bearbeiter(Frame):
        businessclass = "Projektbearbeiter"
        fields = [OclTextField("name"),
            OclTextField("kuerzel"),
            FrameField("leistungen", "Leistungen", "calculate_leistungen"),
        ]
    
        def calculate_leistungen(context):
            leistungen = context.evalocl("offeneleistungen->orderby(datum)")
            return Leistungen(context, leistungen)
    
    main_frame = Bearbeiter

    Beispielreport: Rückzuerstattende Spesen.

  2. Die Liste soll als Liste im Report erscheinen, rundherum gibt es aber noch einen Rahmen wie z.B. einen Übertitel oder Texte, welche übersetzt werden sollen, einen Stichtag etc. In diesem Fall braucht es ein Rahmenframe, welches genau einmal aufgerufen wird (und nicht einmal pro Objekt in der Liste). Dies kann wie folgt geschehen:Zuerst wird ein Frame erzeugt, welches als Sub-Frame die eigentliche Liste darstellt (welche im ersten Beispiel das main_frame wäre):
    class Report(Frame):
        fields = [FrameField("projekte", "Projekt", "calculate_projekte")]
    
        def calculate_projekte(context):
            return Projekt(context, context.projekte)

    Dann wird dieses extra erstellte Frame als main_frame zugewiesen. Da dieses Frame kein Objekt hat, welches dargestellt werden kann, muss via add_row eine einzelne Zeile erzeugt werden – sonst würde der Report einfach nichts anzeigen:

    def calculate_main_frame(context):
        frame = Report(context)
        row = frame.add_row()
        return frame

    Beispielreport: Angefangene Arbeiten

ZUGFeRD Metadaten generieren

Für die Generierung von ZUGFeRD Metadaten in Rechnungsreports gibt es ab Vertec Version 6.3.0.12 die Methode

metadata_zugferd(context)

Wird diese Methode im Report-Code definiert, werden die damit generierten XML-Metadaten in das PDF des Reports integriert. Das genaue Vorgehen ist im separaten Artikel Rechnungen nach ZUGFeRD 2.0 Standard (X-Rechnung) beschrieben.