Support vertec.com

Python-Code für erweiterte Office-Berichte

Python-Code für erweiterte Office-Berichte

Standard

|

Expert

CLOUD ABO

|

ON-PREMISES

Leistung & CRM

Budget & Teilprojekt

Fremdkosten

Ressourcen & Projektplanung

Business Intelligence

Erstellt: 11.04.2022
Aktualisiert: 26.07.2022 | Python-Code für erweiterte Office-Berichte ab Version 6.5.0.20

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 erweiterte Office-Berichte Vorher - Nachher.

Logischer Aufbau

Die Datenlogik eines erweiterten Office-Berichts basiert auf einer Hierarchie von sogenannten Tables.

  • Für jede Klasse, die angezeigt werden soll (Projekt, Bearbeiter, Leistungen etc.), wird eine Table angelegt.
  • Eine Table hat Datenfelder und kann Sub-Tables haben.

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.

Tables

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.

Die Haupttabelle (main_table)

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

Sub-Tables zuweisen (TableField)

Die Zuweisung einer Table als Sub-Table erfolgt über eine Feld-Deklaration. Das genaue Vorgehen ist im Abschnitt Table-Felder (Subtables) beschrieben.

calculate_main_table

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 Report 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
    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.

    Beispiel: Der Report ist auf einer Liste von Projekten registriert. Sie soll nach Projektcode sortiert werden.
    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
    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 Reports 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)).

SingleRowTable (Rahmen für Listenberichte)

Sollen die Objekte einer Liste nicht pro Objekt, sondern in einer Liste angezeigt werden, muss eine übergeordnete Report-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.

initialize_row

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)

add_row

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("phasenaufrechnung")
        for ph in phasen:
            row = context.table.add_row(ph)
            row.value1 = ph.evalocl("sumMinutenInt")
            row.value2 = ph.evalocl("sumWertExt")

len(table)

Die Anzahl der Objekte einer Tabelle kann über len(table) eruiert werden.

Felder

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 Erweiterte Office-Berichte - Felder).

Zusätzlich können weitere Felder deklariert und z.B. mit Berechnungen gefüllt werden.

Feld-Typen

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

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

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

Table-Felder (Subtables)

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")
  1. Name der Table: Das erste Argument enthält wie immer den Namen, mit welchem im Report auf dieses Subset zugegriffen werden kann, beispielsweise als Band-Expression.
  2. Name der Table-Deklaration: Mit dem zweiten Argument wird die entsprechende Table-Deklaration übergeben:
  3. Berechnung. Das dritte Argument ist optional und kann wie folgt verwendet werden:

    • OCL-Expression: Mit dem dritten Argument wird angegeben, wie der Inhalt der Sub-Table berechnet wird.
      Falls die Berechnung per OCL Expression erfolgt (Deklaration mit Präfix Ocl: 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 Report 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.

      context.table

      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
    • Manueller Aufbau: Tables können auch von Hand erstellt und gefüllt werden. Ist dies der Fall, kann das dritte Argument weggelassen werden:
      TableField("offeneLeistungen", "Leistung")

Das Context-Objekt

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

context.evalocl

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)

Context-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(Table): 
    fields = [
        TextField('adresstext', 'calcadresse'),
    ] 

    def calcadresse (context): 
        return context.optarg.adresstext 
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<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.

Context-Variablen setzen

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 Report über eine Context-Expression ausgegeben werden.

Context-Variablen in OCL verwenden

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.

Zugriff auf andere Felder in Feldberechnung

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

Before Report Logik

Um vor der Ausführung des Reports 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 Reports ab. Ansonsten läuft sie weiter (True ist Standard und muss nicht extra angegeben werden).

Python Funktionen

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

Referenzen auf Tables 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.

 

Bitte wählen Sie Ihren Standort