Python code for Office reports

Python code for Office reports

Operating mode

Cloud Suite

|

ON-PREMISES

Modules

Services & CRM

Budget & Phases

Purchases

Resource Planning

Business Intelligence

Created: 11.04.2022
Machine translated
Updated: 22.01.2024 | New Context Variables project, phase and opportunity from Vertec 6.7.0.6.

This article applies to Vertec versions from 6.5.0.20. A description for older versions can be found here. What has changed compared to previous versions can be found in the article Python code for office reports before – after.

The data content of a Office Reports is calculated by Python code, which is stored as a report definition:

The exact structure of this code is described in detail below.

Overview

  • Logical Structure
  • Tables
    • The main table (main_table)
    • Assign Sub-Tables (TableField)
    • calculate_main_table
    • SingleRowTable (frame for list reports)
    • initialize_row
    • add_row
    • len(table)
  • Fields
    • Field types
    • OCL Fields
    • Simple fields
    • Table fields (subtables)
  • The Context Object
    • context.evalocl
    • Context variables
    • Access to other fields in Field Calculation
  • Before Report Logic

Logical structure

The data logic of a  Office Reports is based on a hierarchy of so-called tables.

  • A table is created for each class to be shown (project, user, services, etc.).
  • A table has data fields and can have sub-tables.

The individual objects are then the “rows” of such a table: for each object (individually or in the list on which the report is executed), a row (e.g. row) was introduced.

Each Band in word or Excel (Disambiguation) refers to a table. The variables used within the band refer to the Fields.

Tables

A table is declared as follows:

class Name(Table):

The name is optional, but must be unique within the report definition.

# Beispiel eines minimalen Projekt-Reports

class Projekt(Table):
    businessclass = "Projekt"

On this table no own have been (yet) Fields defined. For these objects, all normal Vertec members are available, as they are also available via OCL.

In order for the Vertec member call to work in the report, the table must be assigned to a unique business class (project, project user, address entry, etc.). This is specified in the declaration of the table using businessclass=.

Note: If no business class is specified, the values are simply returned as strings. This works for simple ads, but is not expected. We therefore recommend that you always specify the business class.

The main table (main_table)

If several tables are used that do not have a parent table, one of them must be marked as the parent table. This is done with the variable main_table:

class Projekt(Table):     
    businessclass = "Projekt"

main_table = Projekt

Assign Sub-Tables (TableField)

The assignment of a table as a sub-table is done via a field declaration. The exact procedure is described in section Table Fields (Subtables) described.

calculate_main_table

All tables except the main table (main_table) are specified as fields in another table and a calculation is given. The main table, the main_table, is not the case. There, the list (rootlist) on which the report is executed (executed on a single object is a list with one entry).

If you want to pre-calculate the main table (for example, to filter or sort a list), you can do this with the method calculate_main_table. There are 2 variants of this method:

  • calculate_main_table is a feature
    calculate_main_table is a feature that Context Object contains and should return a Table instance of the correct type. The signature of the feature is calculate_main_table(context), the return value is a Table instance.

    Example: The report is registered to a list of projects. It should be sorted by project code.
    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 is a string
    calculate_main_table is a string. This will be evaluated as an OCL expression and must return a list. The expression will be evaluated on the original list of the report. Example, as above, but as an expression:
    calculate_main_table = "self.oclAsType(Projekt)->orderby(code)->asSet"

    self references the given rootlist. The rootlist itself does not know the type of its objects, so it must be specified (.oclAsType(Klasse)).

SingleRowTable (frame for list reports)

If the objects of a list are not to be shown per object, but in a list, a parent report table must be created, which provides the frame for the list, but has no object itself.

For this purpose, the main table can be used as SingeRowTable be declared. In such a table, the system will automatically generate exactly one row created, which the list can then contain as a subtable.

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

The SingleRowTable can also be used via calculate_main_table It has the same properties as the normal table, but does not evaluate OCL. So you cannot create OCL fields on it and also the Context Variables are not used via OCL, since there is no object on which the expressions could be evaluated.

For all objects for which the report is executed, a “row” of the table is created. A row is called row.

On the table there is the method initialize_row to initialize the objects. In it you can do calculations, fill fields and set context variables. The syntax is as follows:

def initialize_row(context, row):

This method is called automatically for each individual object.

# 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 can also be built in code. This is needed when different objects are to be displayed that do not belong to the same business class and therefore cannot be calculated automatically.

For this purpose, a add_row method is available on the table, which allows a row-by-row structure of the table. The fields defined on the table are available as properties of the row object.

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

len(table)

The quantity of objects in a table can be determined by len(table) can be determined.

Fields

The standard Vertec members are used as data fields. These can be easily referenced in the report template without the need for a field declaration in the code (see Office Reports – Fields).

In addition, further fields can be declared and filled with calculations, for example.

Field types

There are two types of fields: OCL fields and simple fields. The OCL fields calculate their value via an OCL expression. The simple fields have to be filled manually or with a Python feature.

Fields OCL Fields description
TextField
OclTextField Represents a string value
CurrencyField
OclCurrencyField Represents a fixed-point number, usually used for monetary amounts. Formatting according to country setting.
IntegerField
OclIntegerField Represents an integer
MinuteField
OclMinuteField Represents an integer value as minutes. Formatting according to Settings in Vertec.
Boolean Field
OclBooleanField Represents a True/False value
DateField
OclDateField Represents a date
DateTimeField
OclDateTimeField Represents a date with a time fraction
ImageField
OclImageField Represents an image
TableField
OclTableField Contains a subtable as a value. Table fields allow the representation of hierarchical data structures.

OCL Fields

OCL fields contain the field name (referenced in the report template) as the first parameter, and an OCL expression for the calculation as the second parameter:

OclTextField("projektleiter", "projektleiter.kuerzel")

The result of the expression must be of the data type for which the field was declared (OclTextField = String, OclIntegerField = Integer, etc.).

The second parameter is optional. If it corresponds to the first parameter (e.g. OclTextField("code", "code"), it can be omitted or the field directly referenced in the report without having to declare a field at all.

Simple fields

Simple fields (without Ocl-prefix) also contain the field name as the first parameter:

CurrencyField("offeneLeistungen")

Without further information, these are not automatically calculated fields, which can be manually filled with values when calculating the table in the code, for example in a 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")

Optionally, a second argument can be given as a Python feature, which is called automatically to calculate the field. Example:

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

It is also possible to use the same feature in several fields, by passing the 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 fields (subtables)

The structure of the table fields is somewhat different from the other field types, i.e. the fields with which the sub-tables are specified.

These fields have three arguments:

 TableField("leistungen", "Leistungen", "calculate_leistungen")
  1. Table name: As always, the first argument contains the name with which this subset can be accessed in the report, for example as a band expression.
  2. Table declaration name: The second argument passes the corresponding table declaration:
  3. Calculation. The third argument is optional and can be used as follows:

    • OCL expression: The third argument specifies how the contents of the sub-table are calculated.
      If the calculation is done by OCL expression (declaration prefixed with Ocl: OclTableField), the result of the expression must be a list of business objects corresponding to the Table declaration.
      OclTableField("leistungen", "Leistung", "offeneLeistungen->orderby(datum)")
      If the OCL expression matches the name of the table, it can be omitted. The report mechanism uses the name as OCL expression:
      OclTableField("offeneLeistungen", "Leistung")
    • Python feature: If the calculation is performed using a Python feature (declaration without Ocl prefix: TableField), the feature must return a list of business objects corresponding to the Table declaration.

      context.table

      In the calculation function of a Table Field (3rd argument as a Python function), a resulting Table object is context.table This allows access to an already created table, which allows the creation of a row via context.table.add_row().

      # 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
    • Manual construction: Tables can also be created and populated manually. If this is the case, the third argument can be omitted:
      TableField("offeneLeistungen", "Leistung")

The Context Object

Each calculation in the context of report generation is given the context object. The context object is looped through all calculation functions and has the following properties:

context.evalocl

The context object has the method evalocl(<expression>), which evaluates an OCL expression on the up-to-date object.

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

An object or list can be passed as an optional second argument beside the expression. In this case, the expression is applied to the passed object or list.

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

context.translate

With the method context.translate(text) may produce texts in accordance with the language of the report (context.language) to be translated.

The value last set in the code of context.language is used to translate the texts in the report template.

If an unknown language code is entered, the original texts are shown. The report is simply not translated.

Translations of MLString fields

This translation system does not work for MLString fields because they have a different translation mechanism. See the article on multilingualism with Vertec for more information on what MLString fields are and how they are translated.

If you want to translate such terms in Office reports, you have to use the OCL method .asstringbylanguage:

context.evalocl("typ.text.asStringByLanguage(language)")

Possible languages/parameters are 'DE’, 'DD’ (from 6.5.0.9), 'FR’, 'IT’, 'EN’ or 'NV’. Jargon discrimination is not possible for MLString fields.

Examples
class Leistung(Table):
    businessclass = "OffeneLeistung"
    fields = [
        OclTextField("text", "typ.text.asStringByLanguage(language)"),
    ]
    def initialize_row(context, row):
        context.language = "FR"

or

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

With context.language[:2] you get the first two symbols of the existing language attribute. Since the default language in reports is always defined with the jargon suffix (DE0..), and this is not possible for MLString fields (see above), you get the translation to the current report language.

Context variables

The following variables are defined in each report:

container
The container on which the report was run.
comment From version 6.6.0.9. Contains the comment from the Print Dialog.
currentobject
The up-to-date object of the calculation.

context.currentobject

currentdate
Today’s date, without time part.
context.currentdate
Linguistic

This variable contains the report language (interface language or project language if translation is activated).

It can also be set to output the report in a different language:

context.language="IT"

It has the following values:

–        DE: German (Switzerland)
–        DD: German (Germany)
–        EN: English
–        FR: French
–        IT: Italian
If a jargon is to be considered (project or mandate language), the suffix 0 (for project language) or 1 (for mandate language) can be added: DE0, DE1, etc.

This affects the texts marked with translation in the report template. To translate text in Python code, the context.translate() method can be used.

optarg

The optional address argument for reports. To continue working with the object, it can be accessed as follows:

class Projekt(Table): 
    fields = [
        TextField('adresstext', 'calcadresse'),
    ] 

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

From version 6.7.0.6. For use in Outlook App: Email Templates. Contains the opportunity of the email pre-selected in the Outlook app.

If no opportunity is pre-selected or if the report is called outside the Outlook app, the variable is None. This means that the code must be checked to see if an opportunity exists, otherwise an error will be thrown when accessing the report template.

phase

From version 6.7.0.6. For use in Outlook App: Email Templates. Contains the pre-selected phase of the email in the Outlook app.

If no phase is preselected or if the report is called outside the Outlook app, the variable is None. This means that the code must be checked for a phase, otherwise an error will be thrown when accessing the report template.

project

From version 6.7.0.6. For use in Outlook App: Email Templates. Contains the project of the email pre-selected in the Outlook app.

If no project is pre-selected or if the report is called outside the Outlook app, the variable is None. This means that the code must be checked to see if a project exists, otherwise an error will be thrown when accessing the report template.

reportdef
As of version 6.6.0.3. Contains the report object that executes the report. This is mainly for Centralized code useful when several reports are accessing the same code.
rootlist
The list on which the report was run. Usually this is the list of entries of the container on which the report was run, or a list of the object on which the report was run.
subject From version 6.6.0.9. Contains the subject of the Report Registration or from the Print Dialog.
var<Table>
For each parent table a variable with its up-to-date object is defined.
var<Table>List
For each parent table, a variable with the list of objects of the table is defined.

Setting Context Variables

Further variables can be defined and assigned to the context object.

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

Via set_image(name, value) an image can be read into a context variable:

logo = vtcapp.getpropertyvalue('CompanyLogo')
context.set_image('logo', logo)

The context variables can be output on the report via a context expression.

Using context variables in OCL

Context variables can also be used in OCL. The call is made via the name of the variable (without context.), here in the example todate:

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

The variables are only available in Ocl fields as well as in the context.evalocl method available. Other OCL calls such as vtcapp.evalocl() or the evalocl Methods on lists and objects use a global OCL evaluator and it does not know the context variables.

For example, if a context variable is to be applied to a list, evalocl must still be called on the context and the corresponding list must be passed as an argument. spesenlist.evalocl(..) So you write:

context.evalocl("self->select(getProjekt=varProjekt)->orderby(code))", spesenlist)

OCL only supports calling OCL-compatible values (Vertec objects and lists, strings, integers, etc.). Incompatible values (e.g. Python Dictionaries, Python lists, etc.) an error message appears.

If manually set variables on the context object collide with keywords in OCL (self, date, now), an error message also appears.

Access to other fields in field calculation

In the calculation method of a field, other fields of the same table can be accessed. For this purpose, a method context.get_fieldvalue(fieldname) is given on the context.

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 Logic

To be able to run Python code before running the report (for example, Dialogues to show and variables on the Context Object to set), there is the method before_report(context). If this method exists, it is automatically called first.

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

Returns the method False the implementation of the report is interrupted. Otherwise, it will continue (True is standard and does not need to be specified separately).

Python Features

A Python feature is declared as follows:

def Funktionsname(context):
    return XY

Features can be defined within a table and are then available to that table.

Features that should be available everywhere must be declared outside the tables.

References

References to tables and features can be specified in two ways:

  • Reference as name: If the reference name is written in quotation marks, it is a reference as name. In this case, the referenced declaration may be somewhere in the code, the order does not play a role.
  • Direct reference: If the reference name is written without quotation marks, it is a direct reference. In this case, the referenced declaration must be above the code, i.e. before it is referenced.