diff --git a/.liquibase/Data_alias/basic/2020.1.2/AlterDatatypeOfKeyColumnsToChar/alter_DocumentTemplatePlaceOfUseDatatype.xml b/.liquibase/Data_alias/basic/2020.1.2/AlterDatatypeOfKeyColumnsToChar/alter_DocumentTemplatePlaceOfUseDatatype.xml
index 372497186413416a3a27831bd933c08792bbd0d2..a6e5c99db425623c89a8aaa4446df2b04a77fdb0 100644
--- a/.liquibase/Data_alias/basic/2020.1.2/AlterDatatypeOfKeyColumnsToChar/alter_DocumentTemplatePlaceOfUseDatatype.xml
+++ b/.liquibase/Data_alias/basic/2020.1.2/AlterDatatypeOfKeyColumnsToChar/alter_DocumentTemplatePlaceOfUseDatatype.xml
@@ -15,7 +15,7 @@
     
     <changeSet dbms="!derby" author="b.ulrich" id="85ba6a61-9318-4118-ac4c-e33730f6581d">
         <dropPrimaryKey tableName="DOCUMENTTEMPLATE" constraintName="PK_DOCUMENTTEMPLATE_DOCUMENTTEMPLATEID" dropIndex="true"/>
-        <dropNotNullConstraint tableName="DOCUMENTTEMPLATE" columnName="DOCUMENTTEMPLATEID"/>
+        <dropNotNullConstraint tableName="DOCUMENTTEMPLATE" columnName="DOCUMENTTEMPLATEID" columnDataType="VARCHAR(36)"/>
         <modifyDataType tableName="DOCUMENTTEMPLATE" columnName="DOCUMENTTEMPLATEID" newDataType="CHAR(36)"/>
         <addNotNullConstraint columnName="DOCUMENTTEMPLATEID" tableName="DOCUMENTTEMPLATE" columnDataType="CHAR(36)" validate="true"/>
         <addPrimaryKey tableName="DOCUMENTTEMPLATE" constraintName="PK_DOCUMENTTEMPLATE_DOCUMENTTEMPLATEID" columnNames="DOCUMENTTEMPLATEID"/>
diff --git a/entity/Appointment_entity/Appointment_entity.aod b/entity/Appointment_entity/Appointment_entity.aod
index 6f51538041ffc0b890e44450ea85fd73235b5185..2b98d6bd2c33292ef54ca82d1d5c8f624b353cbf 100644
--- a/entity/Appointment_entity/Appointment_entity.aod
+++ b/entity/Appointment_entity/Appointment_entity.aod
@@ -171,11 +171,6 @@
     </entityParameter>
     <entityConsumer>
       <name>AppointmentLinks</name>
-      <dependency>
-        <name>dependency</name>
-        <entityName>AppointmentLink_entity</entityName>
-        <fieldName>Links</fieldName>
-      </dependency>
       <children>
         <entityParameter>
           <name>AppointmentId_param</name>
@@ -187,6 +182,11 @@
           <expose v="false" />
         </entityParameter>
       </children>
+      <dependency>
+        <name>dependency</name>
+        <entityName>AppointmentLink_entity</entityName>
+        <fieldName>Links</fieldName>
+      </dependency>
     </entityConsumer>
     <entityActionField>
       <name>deleteSeries</name>
@@ -254,6 +254,11 @@
       <name>#PROVIDER_AGGREGATES</name>
       <useAggregates v="true" />
     </entityProvider>
+    <entityParameter>
+      <name>ErrorOnPermissionDenied</name>
+      <valueProcess>%aditoprj%/entity/Appointment_entity/entityfields/erroronpermissiondenied/valueProcess.js</valueProcess>
+      <expose v="true" />
+    </entityParameter>
   </entityFields>
   <recordContainers>
     <jDitoRecordContainer>
diff --git a/entity/Appointment_entity/entityfields/erroronpermissiondenied/valueProcess.js b/entity/Appointment_entity/entityfields/erroronpermissiondenied/valueProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..e5bfa3bbe7f58d2ffaf401248014a6d1a560d2de
--- /dev/null
+++ b/entity/Appointment_entity/entityfields/erroronpermissiondenied/valueProcess.js
@@ -0,0 +1,3 @@
+import("system.result");
+
+result.string("true");
\ No newline at end of file
diff --git a/entity/Appointment_entity/grantDeleteProcess.js b/entity/Appointment_entity/grantDeleteProcess.js
index 4caa2d99cfdda2637190ab5c76a15565666b79b5..aef302f111201f63dc427328d2e4db900ac4c0d3 100644
--- a/entity/Appointment_entity/grantDeleteProcess.js
+++ b/entity/Appointment_entity/grantDeleteProcess.js
@@ -2,6 +2,10 @@ import("system.vars");
 import("system.result");
 import("system.calendars");
 import("system.text");
+import("system.tools");
 
-var owner = text.decodeMS(JSON.parse(vars.get("$param.Entry_param"))["h"])[1].split(":")[1];
-result.string(calendars.hasPermission([owner], calendars.VEVENT, "WRITE"));
\ No newline at end of file
+
+var user = tools.getCurrentUser();
+var calUser = calendars.getCalendarUser(user["title"]);
+var calUserCn = text.decodeMS(calUser)[1].split(":")[1];
+result.string(calendars.hasPermission(calUserCn, calendars.VEVENT, "WRITE"));
\ No newline at end of file
diff --git a/entity/Appointment_entity/grantUpdateProcess.js b/entity/Appointment_entity/grantUpdateProcess.js
index 3562ea4a6c4ba8fda784eb7ea85abd591e022f87..e6ac4c8426c460fd26d1f16844a7844c09300153 100644
--- a/entity/Appointment_entity/grantUpdateProcess.js
+++ b/entity/Appointment_entity/grantUpdateProcess.js
@@ -2,7 +2,13 @@ import("system.vars");
 import("system.result");
 import("system.calendars");
 import("system.text");
+import("system.tools");
+import("system.logging");
 
-var owner = text.decodeMS(JSON.parse(vars.get("$param.Entry_param"))["h"])[1].split(":")[1];
-result.string(calendars.hasPermission([owner], calendars.VEVENT, "WRITE"));
 
+var user = tools.getCurrentUser();
+var calUser = calendars.getCalendarUser(user["title"]);
+var calUserCn = text.decodeMS(calUser)[1].split(":")[1];
+var permitted = calendars.hasPermission(calUserCn, calendars.VEVENT, "WRITE");
+
+result.string(permitted);
diff --git a/entity/Appointment_entity/recordcontainers/jdito/contentProcess.js b/entity/Appointment_entity/recordcontainers/jdito/contentProcess.js
index e56415a0faf52007f91bcc4e92f9ae20aebfa819..709bfac8d00e3b805a383dc0c78c0cb8b91a07d1 100644
--- a/entity/Appointment_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/Appointment_entity/recordcontainers/jdito/contentProcess.js
@@ -1,3 +1,4 @@
+import("Calendar_lib");
 import("Employee_lib");
 import("system.tools");
 import("system.db");
@@ -9,6 +10,7 @@ import("system.datetime");
 import("system.eMath");
 import("system.util");
 import("system.neon");
+import("system.text");
 
 var appointmentSelect = newSelect("APPOINTMENT_ID").from("AB_APPOINTMENTLINK");
 var appointmentUids;
@@ -27,14 +29,14 @@ if(vars.exists("$param.Entry_param") && vars.get("$param.Entry_param"))
 
     //@TODO Icon 
     result.object([
-        buildEntry(entry, masterEntry)
+        CalendarUtil.buildEntry(entry, masterEntry)
     ]);
 }
 
 else if(vars.get("$sys.recordstate") != neon.OPERATINGSTATE_NEW && vars.get("$local.idvalues") != null && vars.get("$local.idvalues") != "")
 {
     var selectedids = vars.get("$local.idvalues");
-    result.object([buildEntry(calendars.getEntry(selectedids, null, null), null)]);
+    result.object([CalendarUtil.buildEntry(calendars.getEntry(selectedids, null, null), null)]);
 }
 
 else if(vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
@@ -42,7 +44,7 @@ else if(vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
     var contactid = EmployeeUtils.getCurrentContactId();
     
     appointmentSelect.whereIfSet("AB_APPOINTMENTLINK.OBJECT_ROWID", contactid)
-    result.object(buildEntriesFromUids(appointmentSelect.table()));
+    result.object(CalendarUtil.buildEntriesFromUids(appointmentSelect.table()));
 }
 
 /**
@@ -51,78 +53,5 @@ else if(vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
 else if(vars.getString("$param.LinkedObjectId_param") != undefined)
 {
     appointmentSelect.whereIfSet("AB_APPOINTMENTLINK.OBJECT_ROWID", "$param.LinkedObjectId_param")
-    result.object(buildEntriesFromUids(appointmentSelect.table()));
-}
-
-function buildEntriesFromUids(appointmentUids)
-{
-    var entryArray = new Array(appointmentUids.length);
-    
-    for(var i = 0; i < appointmentUids.length; i++)
-        entryArray[i] = buildEntry(calendars.getEntry(appointmentUids[i], null, null), null);
-    
-    return entryArray;
-}
-
-
-function buildEntry(pEntry, pMasterentry)
-{
-    var uid = pEntry[calendars.ID];    
-    var summary = pEntry[calendars.SUMMARY];
-    var attendees = pEntry[calendars.AFFECTEDUSERS];
-    var startdate = pEntry[calendars.DTSTART];
-    var enddate = pEntry[calendars.DTEND];
-    var links = pEntry[calendars.LINKS];
-    var description = pEntry[calendars.DESCRIPTION];
-    if(pEntry[calendars.ORGANIZER2] != undefined)
-        var organizer = pEntry[calendars.ORGANIZER2]["paramvalue"];
-    if(pEntry[calendars.USER2] != undefined)
-        var owner = pEntry[calendars.USER2]["paramvalue"];
-    var status = pEntry[calendars.STATUS];
-    var location = pEntry[calendars.LOCATION];
-    var reminder = pEntry[calendars.REMINDER_DURATION];
-    var remindercheck = pEntry[calendars.HASREMINDER]
-    var classification = pEntry[calendars.CLASSIFICATION];
-    var transparency = pEntry[calendars.TRANSPARENCY];
-    var categories = pEntry[calendars.CATEGORIES];
-    var isAllDay = pEntry["X-ADITO-ISALLDAYEVENT"] != null ? pEntry["X-ADITO-ISALLDAYEVENT"] : "FALSE";
-    
-    var masterBegin = pMasterentry != null ? pMasterentry[calendars.DTSTART] : null
-    var masterEnd = pMasterentry != null ? pMasterentry[calendars.DTEND] : null
-    
-    // Recurrence
-    var recurrenceID = pEntry[calendars.RECURRENCEID];
-    var rrule = null;
-    if (pMasterentry != null) { // Entry is a recurrence exception, therefore get rrule from master
-        rrule = pMasterentry[calendars.RRULE] != null ? pMasterentry[calendars.RRULE][0] : null;
-    } else {
-        rrule = pEntry[calendars.RRULE] != null ? pEntry[calendars.RRULE][0] : null;
-    }
-    
-    return [
-            uid, 
-            attendees.length, 
-            startdate, 
-            enddate, 
-            summary, 
-            organizer,
-            owner,
-            attendees, 
-            status,  
-            description, 
-            location, 
-            '', 
-            isAllDay,
-            classification,
-            transparency, 
-            categories, 
-            reminder, 
-            remindercheck, 
-            rrule, 
-            recurrenceID, 
-            null, 
-            masterBegin, 
-            masterEnd,
-            null
-        ];
+    result.object(CalendarUtil.buildEntriesFromUids(appointmentSelect.table()));
 }
diff --git a/entity/Appointment_entity/recordcontainers/jdito/onDelete.js b/entity/Appointment_entity/recordcontainers/jdito/onDelete.js
index 4e0e5c7a5c1257293965989052765e861a71fff7..6f6249aee33bd1be9f2247d09814ffba59a1d904 100644
--- a/entity/Appointment_entity/recordcontainers/jdito/onDelete.js
+++ b/entity/Appointment_entity/recordcontainers/jdito/onDelete.js
@@ -1,20 +1,30 @@
+import("system.logging");
 import("Sql_lib");
 import("system.neon");
 import("system.calendars");
 import("system.vars");
 
-if (vars.exists("$param.Entry_param"))
+var uid;
+
+if (vars.get("$param.Entry_param") != null)
 {
     var entry = JSON.parse(vars.getString("$param.Entry_param"));
     var reccurenceid = entry[calendars.RECURRENCEID];
+    
     if (reccurenceid == undefined) 
         reccurenceid = null;
     
-    calendars.removeEntryByUID(calendars.VEVENT, entry[calendars.USER2]["cn"], entry[calendars.ID], reccurenceid)
+    uid = entry[calendars.ID];
+    calendars.removeEntryByUID(calendars.VEVENT, entry[calendars.USER2]["cn"], uid, reccurenceid);
+}
+else if(vars.get("$field.OWNER"))
+{
+    uid = vars.get("$field.UID");
+    calendars.removeEntryByUID(calendars.VEVENT, JSON.parse(vars.get("$field.OWNER"))["cn"], uid, vars.get("$field.RECURRENCEID"));
+}
     
     /**
      * Deletes ApointmentLinks referring to the deleted Appointment.
      */
-    newWhereIfSet("AB_APPOINTMENTLINK.APPOINTMENT_ID", entry[calendars.ID])
-            .deleteData();
-}
\ No newline at end of file
+    newWhereIfSet("AB_APPOINTMENTLINK.APPOINTMENT_ID", uid)
+            .deleteData();
\ No newline at end of file
diff --git a/entity/Appointment_entity/recordcontainers/jdito/rowCountProcess.js b/entity/Appointment_entity/recordcontainers/jdito/rowCountProcess.js
index afff252f743faff586199e13333a707e5d52c51e..d6495461f9409c2d28cd62951d09cc6797a4eb69 100644
--- a/entity/Appointment_entity/recordcontainers/jdito/rowCountProcess.js
+++ b/entity/Appointment_entity/recordcontainers/jdito/rowCountProcess.js
@@ -1,3 +1,4 @@
+import("Calendar_lib");
 import("system.db");
 import("Employee_lib");
 import("Sql_lib");
@@ -5,7 +6,7 @@ import("system.vars");
 import("system.result");
 
 var rowCount = "0";
-var cond = newSelect("count(APPOINTMENT_ID)")
+var cond = newSelect("APPOINTMENT_ID")
                .from("AB_APPOINTMENTLINK");
 
 if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
@@ -13,7 +14,7 @@ if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
 else if (vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
 {
     cond.whereIfSet("AB_APPOINTMENTLINK.OBJECT_ROWID", EmployeeUtils.getCurrentContactId());
-    rowCount = cond.cell();
+    rowCount = CalendarUtil.countEntriesFromUids(cond.table());
 }
 
 /**
@@ -22,7 +23,7 @@ else if (vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
 else if (vars.getString("$param.LinkedObjectId_param") != undefined)
 {
     cond.whereIfSet("AB_APPOINTMENTLINK.OBJECT_ROWID", "$param.LinkedObjectId_param");
-    rowCount = cond.cell();
+    rowCount = CalendarUtil.countEntriesFromUids(cond.table());
 }
 /**
  * Will be used, if the user is operating the calendar.
diff --git a/entity/BulkMailRecipient_entity/BulkMailRecipient_entity.aod b/entity/BulkMailRecipient_entity/BulkMailRecipient_entity.aod
index bae83161b6a2b2580cbc7734093346ce3596a028..60d85496e220808f40709d4fd7fc80bbba176c0a 100644
--- a/entity/BulkMailRecipient_entity/BulkMailRecipient_entity.aod
+++ b/entity/BulkMailRecipient_entity/BulkMailRecipient_entity.aod
@@ -121,6 +121,13 @@
           <iconId>VAADIN:BAN</iconId>
           <tooltipProcess>%aditoprj%/entity/BulkMailRecipient_entity/entityfields/recipientactions/children/removewithcommrestriction/tooltipProcess.js</tooltipProcess>
         </entityActionField>
+        <entityActionField>
+          <name>startMarketingWorkflows</name>
+          <title>Start marketing mailing</title>
+          <onActionProcess>%aditoprj%/entity/BulkMailRecipient_entity/entityfields/recipientactions/children/startmarketingworkflows/onActionProcess.js</onActionProcess>
+          <isObjectAction v="false" />
+          <iconId>VAADIN:ENVELOPES</iconId>
+        </entityActionField>
       </children>
     </entityActionGroup>
     <entityField>
diff --git a/entity/BulkMailRecipient_entity/entityfields/recipientactions/children/startmarketingworkflows/onActionProcess.js b/entity/BulkMailRecipient_entity/entityfields/recipientactions/children/startmarketingworkflows/onActionProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbe91449e5c258643491b9020a7bc6d1b534e7ec
--- /dev/null
+++ b/entity/BulkMailRecipient_entity/entityfields/recipientactions/children/startmarketingworkflows/onActionProcess.js
@@ -0,0 +1,34 @@
+import("Util_lib");
+import("system.entities");
+import("Context_lib");
+import("system.vars");
+import("system.neon");
+
+var rows = vars.get("$sys.selectionRows");
+var filter = vars.get("$sys.filter").filter;
+var targets = [];
+
+if (Utils.isNullOrEmpty(rows))
+{
+    let loadConfig = entities.createConfigForLoadingRows()
+        .entity("BulkMailRecipient_entity")
+        .provider("BulkMailRecipients")
+        .fields(["CONTACT_ID", "TARGETCONTEXT"])
+        .addParameter("BulkMailId_param", vars.get("$param.BulkMailId_param"));
+
+    if (filter)
+        loadConfig.filter(JSON.stringify(filter));
+    
+    rows = entities.getRows(loadConfig);
+}
+
+rows = rows.map(function (row)
+{
+    return [row["CONTACT_ID"], row["TARGETCONTEXT"]];
+});
+
+
+neon.openContext("MarketingWorkflowLauncher", "MarketingWorkflowLauncherEdit_view", null, neon.OPERATINGSTATE_VIEW, {
+    "ObjectIds_param": JSON.stringify(rows),
+    "ObjectType_param": ContextUtils.getCurrentContextId()
+});
\ No newline at end of file
diff --git a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/hascommrestriction.value/expression.js b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/hascommrestriction.value/expression.js
index 3020cb618cdf0f0aac9fbf0f9201753a29563a37..04cd6d4dc91c5a7e36cd82b78301acf456fc05ff 100644
--- a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/hascommrestriction.value/expression.js
+++ b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/hascommrestriction.value/expression.js
@@ -5,5 +5,5 @@ import("system.db");
 import("system.result");
 
 var commRestrictionCond = ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail());
-var sql = "case when (" + commRestrictionCond.toString() + ") then 'true' else 'false' end";
-result.string(sql);
+var sql = SqlBuilder.caseWhen(commRestrictionCond).thenString("true").elseString("false");
+result.string(sql.toString());
diff --git a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/status.displayvalue/expression.js b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/status.displayvalue/expression.js
index 115c35f85e46db99d439362dca426fd984657802..49b6fefd59233f6aa50934a21ce43f06a3a61172 100644
--- a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/status.displayvalue/expression.js
+++ b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/status.displayvalue/expression.js
@@ -1,17 +1,12 @@
 import("Sql_lib");
 import("Contact_lib");
 import("system.translate");
-import("system.db");
-import("Bulkmail_lib");
 import("system.result");
 import("Keyword_lib");
 import("KeywordRegistry_basic");
 
-var commRestrictionCond = ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail()).build();
+var commRestrictionCond = ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail());
 var keywordSql = KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.bulkMailRecipientStatus(), "BULKMAILRECIPIENT.STATUS");
 
-// TODO: is prepared possible?
-var sql = "case when (" + SqlUtils.translateStatementWithQuotes(commRestrictionCond)
-    + ") then '" + translate.text("Advertising ban")
-    + "' else (" + keywordSql + ") end";
-result.string(sql);
+var sql = SqlBuilder.caseWhen(commRestrictionCond).thenString(translate.text("Advertising ban")).elseValue(keywordSql);
+result.string(sql.toString());
diff --git a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
index 9bac722a3fa50b14fe0fd7fcf9cb92ebd8e49fba..5247fde75b3f57fdb34fb70500efb8bca84d6906 100644
--- a/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
+++ b/entity/BulkMailRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
@@ -1,7 +1,7 @@
+import("Sql_lib");
 import("Context_lib");
 import("system.result");
 
-// TODO: is prepared possible?
-result.string("case when PERSON_ID is null then '" + ContextUtils.getContextName("Organisation")
-    + "' when CONTACT.ORGANISATION_ID is not null and CONTACT.PERSON_ID is not null then '" + ContextUtils.getContextName("Person") 
-    + "' else '' end");
\ No newline at end of file
+var sql = SqlBuilder.caseWhen("PERSON_ID is null").thenString(ContextUtils.getContextName("Organisation"))
+    .when(newWhere("CONTACT.ORGANISATION_ID is not null").and("CONTACT.PERSON_ID is not null")).thenString(ContextUtils.getContextName("Person"))
+result.string(sql.toString());
\ No newline at end of file
diff --git a/entity/CampaignParticipant_entity/CampaignParticipant_entity.aod b/entity/CampaignParticipant_entity/CampaignParticipant_entity.aod
index 3c8e5a5655f3c8320c4188939e9588988e7b9b44..4d3b86ba02c4c01afcd977df57bbd15e493c3c08 100644
--- a/entity/CampaignParticipant_entity/CampaignParticipant_entity.aod
+++ b/entity/CampaignParticipant_entity/CampaignParticipant_entity.aod
@@ -177,6 +177,13 @@
           <tooltip>Update campaign step</tooltip>
           <tooltipProcess>%aditoprj%/entity/CampaignParticipant_entity/entityfields/filterviewactiongroup/children/setsteptoparticipantselection/tooltipProcess.js</tooltipProcess>
         </entityActionField>
+        <entityActionField>
+          <name>startMarketingWorkflows</name>
+          <title>Start marketing mailing</title>
+          <onActionProcess>%aditoprj%/entity/CampaignParticipant_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js</onActionProcess>
+          <isObjectAction v="false" />
+          <iconId>VAADIN:ENVELOPES</iconId>
+        </entityActionField>
       </children>
     </entityActionGroup>
     <entityField>
diff --git a/entity/CampaignParticipant_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js b/entity/CampaignParticipant_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..40e60ff20d2ca20af3b447981c2a988959d2de63
--- /dev/null
+++ b/entity/CampaignParticipant_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
@@ -0,0 +1,36 @@
+import("Util_lib");
+import("system.entities");
+import("Context_lib");
+import("system.vars");
+import("system.neon");
+
+var rows = vars.get("$sys.selectionRows");
+var filter = vars.get("$sys.filter").filter;
+var targets = [];
+
+if (Utils.isNullOrEmpty(rows))
+{
+    let loadConfig = entities.createConfigForLoadingRows()
+        .entity("CampaignParticipant_entity")
+        .provider("CampaignParticipantsProvider")
+        .fields(["CONTACT_ID", "CONTACTCONTEXT"])
+        .addParameter("CampaignId_param", vars.get("$param.CampaignId_param"))
+        .addParameter("CampaignStepId_param", vars.get("$param.CampaignStepId_param"))
+        .addParameter("ContactId_param", vars.get("$param.ContactId_param"));
+
+    if (filter)
+        loadConfig.filter(JSON.stringify(filter));
+    
+    rows = entities.getRows(loadConfig);
+}
+
+rows = rows.map(function (row)
+{
+    return [row["CONTACT_ID"], row["CONTACTCONTEXT"]];
+});
+
+
+neon.openContext("MarketingWorkflowLauncher", "MarketingWorkflowLauncherEdit_view", null, neon.OPERATINGSTATE_VIEW, {
+    "ObjectIds_param": JSON.stringify(rows),
+    "ObjectType_param": ContextUtils.getCurrentContextId()
+});
\ No newline at end of file
diff --git a/entity/Campaign_entity/recordcontainers/db/conditionProcess.js b/entity/Campaign_entity/recordcontainers/db/conditionProcess.js
index b56a17e4e112b773aa2b1f6f5339c986b5d205bf..ea77837ea6f8defb3e4e66200740f5b8614adae5 100644
--- a/entity/Campaign_entity/recordcontainers/db/conditionProcess.js
+++ b/entity/Campaign_entity/recordcontainers/db/conditionProcess.js
@@ -7,19 +7,11 @@ import("Sql_lib");
 
 
 var recordState = vars.get("$sys.recordstate");
+var condition = newWhere();
 
 if(vars.get("$param.ShowOnlyCurrentUsersCampaigns_param") == 'true')
 {
-    //TODO: use a preparedCondition (.build instead of .translate) when available #1030812 #1034026
-    result.string(newWhere("CAMPAIGN.EMPLOYEE_CONTACT_ID", EmployeeUtils.getCurrentContactId()).toString());
-} else if (recordState != neon.OPERATINGSTATE_NEW && recordState != neon.OPERATINGSTATE_EDIT) {
-    var condition = new SqlBuilder()
-        .whereIfSet("STEPDATESTART_TABLEALIAS.CAMPAIGN_ID = CAMPAIGN.CAMPAIGNID")
-        .andIfSet("STEPDATEEND_TABLEALIAS.CAMPAIGN_ID = CAMPAIGN.CAMPAIGNID")
-        ;
+    condition.and("CAMPAIGN.EMPLOYEE_CONTACT_ID", EmployeeUtils.getCurrentContactId());
+}
 
-    result.string(condition.toString());
-} else {
-    
-    result.string(newWhere().toString());
-}
\ No newline at end of file
+result.string(condition.toString());
\ No newline at end of file
diff --git a/entity/Campaign_entity/recordcontainers/db/fromClauseProcess.js b/entity/Campaign_entity/recordcontainers/db/fromClauseProcess.js
index 5eec4c678e0303f56f160d22534f7c2fbe2bd302..f1704c63ce08d64a2b42eaa2192c87e929b2c290 100644
--- a/entity/Campaign_entity/recordcontainers/db/fromClauseProcess.js
+++ b/entity/Campaign_entity/recordcontainers/db/fromClauseProcess.js
@@ -7,10 +7,11 @@ var recordState = vars.get("$sys.recordstate");
 var res = "CAMPAIGN";
 
 if (recordState != neon.OPERATINGSTATE_NEW && recordState != neon.OPERATINGSTATE_EDIT) {
-    var subSelectDateStart = "(select min(DATE_START) as STEPDATESTART_ALIAS, CAMPAIGN_ID from CAMPAIGNSTEP group by CAMPAIGN_ID) as STEPDATESTART_TABLEALIAS";
-    var subSelectDateEnd = "(select max(DATE_END) as STEPDATEEND_ALIAS, CAMPAIGN_ID from CAMPAIGNSTEP group by CAMPAIGN_ID) as STEPDATEEND_TABLEALIAS";
-         
-    res += ", " + subSelectDateStart + ", " + subSelectDateEnd;
+    res +=  " join (select min(DATE_START) as STEPDATESTART_ALIAS, CAMPAIGN_ID from CAMPAIGNSTEP group by CAMPAIGN_ID) STEPDATESTART_TABLEALIAS"
+        + " on STEPDATESTART_TABLEALIAS.CAMPAIGN_ID = CAMPAIGN.CAMPAIGNID "
+        + " join (select max(DATE_END) as STEPDATEEND_ALIAS, CAMPAIGN_ID from CAMPAIGNSTEP group by CAMPAIGN_ID) STEPDATEEND_TABLEALIAS"
+        + " on STEPDATEEND_TABLEALIAS.CAMPAIGN_ID = CAMPAIGN.CAMPAIGNID ";
+
 }
 
 result.string(res);
diff --git a/entity/Campaign_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js b/entity/Campaign_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
index fa780bd3a980c26cd0931459951df89c31b7d4a3..dd188aaf77933759617acc567c9f1ba0cb1313dc 100644
--- a/entity/Campaign_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
+++ b/entity/Campaign_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
@@ -1,6 +1,11 @@
+import("Context_lib");
+import("Sql_lib");
 import("system.vars");
 import("system.result");
+//!LibFunction
+var cond = newWhere(null, newSelect("ACTIVITYLINK.OBJECT_ROWID").from("ACTIVITYLINK")
+    .join("ACTIVITY", newWhere("ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID")
+        .and("ACTIVITYLINK.OBJECT_TYPE", ContextUtils.getCurrentContextId()))
+    .where(vars.get("$local.condition")), SqlBuilder.EXISTS());
 
-var from = "ACTIVITYLINK join ACTIVITY on ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID and ACTIVITYLINK.OBJECT_TYPE = 'Campaign'";
-
-result.string("CAMPAIGNID in (select ACTIVITYLINK.OBJECT_ROWID from " + from + " where " + vars.get("$local.condition")+ ")");
\ No newline at end of file
+result.string(cond.toString());
\ No newline at end of file
diff --git a/entity/ClassificationAdmin_entity/recordcontainers/jdito/onUpdate.js b/entity/ClassificationAdmin_entity/recordcontainers/jdito/onUpdate.js
index 43a6304543f7b01dab728e1774e8d6f9e06980e0..ea803f184c537724212607c29cfa59f9342de57c 100644
--- a/entity/ClassificationAdmin_entity/recordcontainers/jdito/onUpdate.js
+++ b/entity/ClassificationAdmin_entity/recordcontainers/jdito/onUpdate.js
@@ -178,7 +178,7 @@ function _update()
             }
         }
 
-        cond = newWhereIfSet("CLASSIFICATIONTYPE.CLASSIFICATIONTYPEID = '" + text.decodeMS(uid)[1] + "'");
+        cond = newWhereIfSet("CLASSIFICATIONTYPE.CLASSIFICATIONTYPEID", text.decodeMS(uid)[1]);
         columns.push("SCORETYPE");
         values.push(vars.get("$field.CLASSIFICATIONTYPEIDDISPLAYVALUE"));
         cond.updateData(true, table, columns, null, values);
@@ -195,13 +195,13 @@ function _update()
                                                 .where("CLASSIFICATIONGROUP.CLASSIFICATIONGROUPID", id)
                                                 .cell();
 
-        cond = newWhereIfSet("CLASSIFICATIONTYPE.CLASSIFICATIONGROUP_ID = '" + classificationGroupId + "'");
+        cond = newWhereIfSet("CLASSIFICATIONTYPE.CLASSIFICATIONGROUP_ID", classificationGroupId);
 
         var groupTable = "CLASSIFICATIONGROUP";
         var groupColumns = ["SORTING", "TITLE"];
         var groupName = rowdata["CLASSIFICATIONGROUP.value"] == classificationGroupId ? rowdata["CLASSIFICATIONGROUP.displayValue"] : rowdata["CLASSIFICATIONGROUP.value"];
         var groupValues = [vars.get("$field.SORTING"), groupName];
-        var groupCond = newWhereIfSet("CLASSIFICATIONGROUP.CLASSIFICATIONGROUPID = '" + classificationGroupId + "'");
+        var groupCond = newWhereIfSet("CLASSIFICATIONGROUP.CLASSIFICATIONGROUPID", classificationGroupId);
         groupCond.updateData(true, groupTable, groupColumns, null, groupValues);
     }
 }
\ No newline at end of file
diff --git a/entity/Competition_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js b/entity/Competition_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
index 33554680cccd2c3d363443686457c41e0c1c5729..fb69d484cfa8fc6aa1c45987d94b793c35efd20a 100644
--- a/entity/Competition_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
+++ b/entity/Competition_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
@@ -1,4 +1,4 @@
 import("system.result");
 import("Context_lib");
 
-result.string(ContextUtils.getNameSubselectSql("OBJECT_TYPE", "OBJECT_ROWID"))
\ No newline at end of file
+result.string(ContextUtils.getNameSubselectSql("COMPETITION.OBJECT_TYPE", "COMPETITION.OBJECT_ROWID"))
\ No newline at end of file
diff --git a/entity/Contract_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js b/entity/Contract_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
index c2197272407c3509180dc90692e2edc59e2d6d7a..dd188aaf77933759617acc567c9f1ba0cb1313dc 100644
--- a/entity/Contract_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
+++ b/entity/Contract_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
@@ -1,6 +1,11 @@
+import("Context_lib");
+import("Sql_lib");
 import("system.vars");
 import("system.result");
+//!LibFunction
+var cond = newWhere(null, newSelect("ACTIVITYLINK.OBJECT_ROWID").from("ACTIVITYLINK")
+    .join("ACTIVITY", newWhere("ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID")
+        .and("ACTIVITYLINK.OBJECT_TYPE", ContextUtils.getCurrentContextId()))
+    .where(vars.get("$local.condition")), SqlBuilder.EXISTS());
 
-var from = "ACTIVITYLINK join ACTIVITY on ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID and ACTIVITYLINK.OBJECT_TYPE = 'Contract'";
-
-result.string("CONTRACTID in (select ACTIVITYLINK.OBJECT_ROWID from " + from + " where " + vars.get("$local.condition")+ ")");
\ No newline at end of file
+result.string(cond.toString());
\ No newline at end of file
diff --git a/entity/DocumentTemplate_entity/DocumentTemplate_entity.aod b/entity/DocumentTemplate_entity/DocumentTemplate_entity.aod
index 9cf1d9cca301bb4240acea90684327824bbd201d..dbb9e49a3b635be85e5615422520d9f08d387aac 100644
--- a/entity/DocumentTemplate_entity/DocumentTemplate_entity.aod
+++ b/entity/DocumentTemplate_entity/DocumentTemplate_entity.aod
@@ -321,6 +321,7 @@
     <entityConsumer>
       <name>DocumentTemplatePlaceOfUse</name>
       <stateProcess>%aditoprj%/entity/DocumentTemplate_entity/entityfields/documenttemplateplaceofuse/stateProcess.js</stateProcess>
+      <onValidation>%aditoprj%/entity/DocumentTemplate_entity/entityfields/documenttemplateplaceofuse/onValidation.js</onValidation>
       <dependency>
         <name>dependency</name>
         <entityName>DocumentTemplatePlaceOfUse_entity</entityName>
diff --git a/entity/DocumentTemplate_entity/entityfields/documenttemplateplaceofuse/onValidation.js b/entity/DocumentTemplate_entity/entityfields/documenttemplateplaceofuse/onValidation.js
new file mode 100644
index 0000000000000000000000000000000000000000..a40be7eb7d8e21b088a3cf4683b41849239e35ee
--- /dev/null
+++ b/entity/DocumentTemplate_entity/entityfields/documenttemplateplaceofuse/onValidation.js
@@ -0,0 +1,14 @@
+import("system.translate");
+import("system.result");
+import("system.vars");
+import("Entity_lib");
+
+var usages = EntityConsumerRowsHelper.getCurrentConsumerRows("DocumentTemplatePlaceOfUse", ["PLACEOFUSE"]);
+var hasMarketingWorkflowUsage = usages.some(function (usage)
+{
+    return usage["PLACEOFUSE"] == "MarketingWorkflowLauncher";
+});
+if (hasMarketingWorkflowUsage && !vars.get("$field.Content").includes("{@workflowActionLink@}"))
+{
+    result.string(translate.text("The template must contain the placeholder for the worklow-link to use it with the marketing workflow"));
+}
diff --git a/entity/Document_entity/Document_entity.aod b/entity/Document_entity/Document_entity.aod
index 420157a37135f2b6b3fd1105810584e32ac6935e..cba9173b17cbedf17fc79b500cd69af232cb8c10 100644
--- a/entity/Document_entity/Document_entity.aod
+++ b/entity/Document_entity/Document_entity.aod
@@ -144,6 +144,26 @@
     <entityProvider>
       <name>Documents</name>
       <recordContainer>jdito</recordContainer>
+      <children>
+        <entityParameter>
+          <name>AssignmentName_param</name>
+          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/documents/children/assignmentname_param/valueProcess.js</valueProcess>
+          <expose v="true" />
+          <documentation>%aditoprj%/entity/Document_entity/entityfields/documents/children/assignmentname_param/documentation.adoc</documentation>
+        </entityParameter>
+        <entityParameter>
+          <name>AssignmentRowId_param</name>
+          <expose v="true" />
+        </entityParameter>
+        <entityParameter>
+          <name>AssignmentTable_param</name>
+          <expose v="true" />
+        </entityParameter>
+        <entityParameter>
+          <name>Keyword_param</name>
+          <expose v="true" />
+        </entityParameter>
+      </children>
       <dependencies>
         <entityDependency>
           <name>1eae1907-53ea-4d6f-bcf1-772052365020</name>
@@ -225,7 +245,7 @@
         </entityDependency>
         <entityDependency>
           <name>2e6fcf27-ee98-4f7d-a99d-7ce02774076b</name>
-          <entityName>UserhelpResources</entityName>
+          <entityName>UserhelpResources_entity</entityName>
           <fieldName>Documents</fieldName>
           <isConsumer v="false" />
         </entityDependency>
@@ -242,12 +262,23 @@
           <isConsumer v="false" />
         </entityDependency>
       </dependencies>
+    </entityProvider>
+    <entityProvider>
+      <name>MainDocuments</name>
+      <recordContainer>jdito</recordContainer>
       <children>
+        <entityParameter>
+          <name>Keyword_param</name>
+          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/keyword_param/valueProcess.js</valueProcess>
+          <expose v="true" />
+          <mandatory v="true" />
+          <description>TODO: expose auf false. aktuell wird der Code nicht ausgeführt, wenn Expose false ist.</description>
+        </entityParameter>
         <entityParameter>
           <name>AssignmentName_param</name>
-          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/documents/children/assignmentname_param/valueProcess.js</valueProcess>
+          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/assignmentname_param/valueProcess.js</valueProcess>
           <expose v="true" />
-          <documentation>%aditoprj%/entity/Document_entity/entityfields/documents/children/assignmentname_param/documentation.adoc</documentation>
+          <documentation>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/assignmentname_param/documentation.adoc</documentation>
         </entityParameter>
         <entityParameter>
           <name>AssignmentRowId_param</name>
@@ -258,14 +289,10 @@
           <expose v="true" />
         </entityParameter>
         <entityParameter>
-          <name>Keyword_param</name>
-          <expose v="true" />
+          <name>DisallowCreate_param</name>
+          <expose v="false" />
         </entityParameter>
       </children>
-    </entityProvider>
-    <entityProvider>
-      <name>MainDocuments</name>
-      <recordContainer>jdito</recordContainer>
       <dependencies>
         <entityDependency>
           <name>87d738a5-5d5e-425e-b013-007371475a38</name>
@@ -310,33 +337,6 @@
           <isConsumer v="false" />
         </entityDependency>
       </dependencies>
-      <children>
-        <entityParameter>
-          <name>Keyword_param</name>
-          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/keyword_param/valueProcess.js</valueProcess>
-          <expose v="true" />
-          <mandatory v="true" />
-          <description>TODO: expose auf false. aktuell wird der Code nicht ausgeführt, wenn Expose false ist.</description>
-        </entityParameter>
-        <entityParameter>
-          <name>AssignmentName_param</name>
-          <valueProcess>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/assignmentname_param/valueProcess.js</valueProcess>
-          <expose v="true" />
-          <documentation>%aditoprj%/entity/Document_entity/entityfields/maindocuments/children/assignmentname_param/documentation.adoc</documentation>
-        </entityParameter>
-        <entityParameter>
-          <name>AssignmentRowId_param</name>
-          <expose v="true" />
-        </entityParameter>
-        <entityParameter>
-          <name>AssignmentTable_param</name>
-          <expose v="true" />
-        </entityParameter>
-        <entityParameter>
-          <name>DisallowCreate_param</name>
-          <expose v="false" />
-        </entityParameter>
-      </children>
     </entityProvider>
     <entityParameter>
       <name>DisallowCreate_param</name>
@@ -346,14 +346,6 @@
     <entityProvider>
       <name>SingleDocument</name>
       <titlePlural>Document</titlePlural>
-      <dependencies>
-        <entityDependency>
-          <name>91f87622-d0e8-43c6-99a0-5f9cebf79aaf</name>
-          <entityName>SerialLetter_entity</entityName>
-          <fieldName>Documents</fieldName>
-          <isConsumer v="false" />
-        </entityDependency>
-      </dependencies>
       <children>
         <entityParameter>
           <name>AssignmentName_param</name>
@@ -365,6 +357,14 @@
           <expose v="true" />
         </entityParameter>
       </children>
+      <dependencies>
+        <entityDependency>
+          <name>91f87622-d0e8-43c6-99a0-5f9cebf79aaf</name>
+          <entityName>SerialLetter_entity</entityName>
+          <fieldName>Documents</fieldName>
+          <isConsumer v="false" />
+        </entityDependency>
+      </dependencies>
     </entityProvider>
     <entityProvider>
       <name>#PROVIDER_AGGREGATES</name>
diff --git a/entity/Forecast_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js b/entity/Forecast_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
index 33554680cccd2c3d363443686457c41e0c1c5729..856c1bb3cfff78abcc06bc817a49a40757e3529f 100644
--- a/entity/Forecast_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
+++ b/entity/Forecast_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
@@ -1,4 +1,4 @@
 import("system.result");
 import("Context_lib");
 
-result.string(ContextUtils.getNameSubselectSql("OBJECT_TYPE", "OBJECT_ROWID"))
\ No newline at end of file
+result.string(ContextUtils.getNameSubselectSql("FORECAST.OBJECT_TYPE", "FORECAST.OBJECT_ROWID"))
\ No newline at end of file
diff --git a/entity/LetterRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js b/entity/LetterRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
index 9bac722a3fa50b14fe0fd7fcf9cb92ebd8e49fba..b9a588dfe867d2a870ad80b9eb25ef5803ddfa4b 100644
--- a/entity/LetterRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
+++ b/entity/LetterRecipient_entity/recordcontainers/db/recordfieldmappings/targetcontext.value/expression.js
@@ -1,7 +1,7 @@
+import("Sql_lib");
 import("Context_lib");
 import("system.result");
 
-// TODO: is prepared possible?
-result.string("case when PERSON_ID is null then '" + ContextUtils.getContextName("Organisation")
-    + "' when CONTACT.ORGANISATION_ID is not null and CONTACT.PERSON_ID is not null then '" + ContextUtils.getContextName("Person") 
-    + "' else '' end");
\ No newline at end of file
+var sql = SqlBuilder.caseWhen("PERSON_ID is null").thenString(ContextUtils.getContextName("Organisation"))
+    .when(newWhere("CONTACT.ORGANISATION_ID is not null").and("CONTACT.PERSON_ID is not null")).thenString(ContextUtils.getContextName("Person"));
+result.string(sql.toString());
\ No newline at end of file
diff --git a/entity/MarketingWorkflowLauncher_entity/MarketingWorkflowLauncher_entity.aod b/entity/MarketingWorkflowLauncher_entity/MarketingWorkflowLauncher_entity.aod
index 40b9b39c8993d2b421f368b11fca27d80abb24f5..791b08d72b2e85a807d11a6c47284056ae16a977 100644
--- a/entity/MarketingWorkflowLauncher_entity/MarketingWorkflowLauncher_entity.aod
+++ b/entity/MarketingWorkflowLauncher_entity/MarketingWorkflowLauncher_entity.aod
@@ -14,7 +14,7 @@
     </entityProvider>
     <entityField>
       <name>DOCUMENTTEMPLATE_ID</name>
-      <title>Document template</title>
+      <title>Document Template</title>
       <consumer>EmailTemplates</consumer>
       <mandatory v="true" />
       <state>EDITABLE</state>
@@ -80,6 +80,7 @@
   <recordContainers>
     <datalessRecordContainer>
       <name>dataLess</name>
+      <alias>Data_alias</alias>
     </datalessRecordContainer>
   </recordContainers>
 </entity>
diff --git a/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/processvariables_param/valueProcess.js b/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/processvariables_param/valueProcess.js
index b81df3d4fb93fc9608febd7783247052a61d4acd..cea637a710a0e3e0903248241654aec944c88137 100644
--- a/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/processvariables_param/valueProcess.js
+++ b/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/processvariables_param/valueProcess.js
@@ -2,6 +2,7 @@ import("system.vars");
 import("system.result");
 
 var variables = {
-    documentTemplate: vars.get("$field.DOCUMENTTEMPLATE_ID")
+    documentTemplateId: vars.get("$field.DOCUMENTTEMPLATE_ID"),
+    originUrl: vars.get("$sys.origin")
 };
 result.string(JSON.stringify(variables));
\ No newline at end of file
diff --git a/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/targets_param/valueProcess.js b/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/targets_param/valueProcess.js
index 6a28aafa528aa4130e913b642f80e04cfadc8b19..cca5f0e8d7ce5b6b8bb2ad60f3ffb068c4c6f1a1 100644
--- a/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/targets_param/valueProcess.js
+++ b/entity/MarketingWorkflowLauncher_entity/entityfields/workflowlauncherintegration/children/targets_param/valueProcess.js
@@ -1,13 +1,17 @@
 import("Util_lib");
 import("system.vars");
 import("system.result");
+import("FilterViewAction_lib");
 
 var context = vars.get("$param.ObjectType_param");
-var targets = Utils.parseJSON(vars.get("$param.ObjectIds_param")) || [];
-targets = targets.map(function (targetId)
+var targets = Utils.parseJSON(vars.get("$param.ObjectIds_param"));
+var filter = Utils.parseJSON(vars.get("$param.ObjectFilter_param"));
+
+targets = FilterViewActionUtils.getUidsBySelectionOrFilter(context, targets, filter).map(function (targetId)
 {
     if (Utils.isString(targetId))
         return [targetId, context]; //todo: context dynamic (eg for participants)
     return targetId;
 });
+
 result.string(JSON.stringify(targets));
\ No newline at end of file
diff --git a/entity/Member_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js b/entity/Member_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
index 000d055cfacc970e2ca0dc70689b4c793e0bc0cd..a5d62617bee8630b78204e4bc85c851dba3b10df 100644
--- a/entity/Member_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
+++ b/entity/Member_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
@@ -5,6 +5,6 @@ import("Context_lib");
 //TODO: refactor:
 //whenever we want to shrink data for a single object it's not needed to resolve the objects name where we're from
 if (vars.get("$param.ObjectType_param") == null)
-    result.string(ContextUtils.getNameSubselectSql("OBJECT_TYPE", "OBJECT_ROWID"));
+    result.string(ContextUtils.getNameSubselectSql("OBJECTMEMBER.OBJECT_TYPE", "OBJECTMEMBER.OBJECT_ROWID"));
 else
     result.string("'OBJECT_ROWID.displayValue not loaded'");
\ No newline at end of file
diff --git a/entity/Notification_entity/Notification_entity.aod b/entity/Notification_entity/Notification_entity.aod
index 8dead6bc118e4d353c6c708976a037b3ace5c45e..cf3203213ac627ef13ec8064010448a7db4a5970 100644
--- a/entity/Notification_entity/Notification_entity.aod
+++ b/entity/Notification_entity/Notification_entity.aod
@@ -175,8 +175,7 @@
     <dbRecordContainer>
       <name>db</name>
       <alias>_____SYSTEMALIAS</alias>
-      <maximumDbRows v="200" />
-      <isPageable v="false" />
+      <isPageable v="true" />
       <fromClauseProcess>%aditoprj%/entity/Notification_entity/recordcontainers/db/fromClauseProcess.js</fromClauseProcess>
       <conditionProcess>%aditoprj%/entity/Notification_entity/recordcontainers/db/conditionProcess.js</conditionProcess>
       <orderClauseProcess>%aditoprj%/entity/Notification_entity/recordcontainers/db/orderClauseProcess.js</orderClauseProcess>
diff --git a/entity/Offer_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js b/entity/Offer_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
index 9a3d04480545203f0da83c3f6be762e684c8883a..dd188aaf77933759617acc567c9f1ba0cb1313dc 100644
--- a/entity/Offer_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
+++ b/entity/Offer_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
@@ -1,6 +1,11 @@
+import("Context_lib");
+import("Sql_lib");
 import("system.vars");
 import("system.result");
+//!LibFunction
+var cond = newWhere(null, newSelect("ACTIVITYLINK.OBJECT_ROWID").from("ACTIVITYLINK")
+    .join("ACTIVITY", newWhere("ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID")
+        .and("ACTIVITYLINK.OBJECT_TYPE", ContextUtils.getCurrentContextId()))
+    .where(vars.get("$local.condition")), SqlBuilder.EXISTS());
 
-var from = "ACTIVITYLINK join ACTIVITY on ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID and ACTIVITYLINK.OBJECT_TYPE = 'Offer'";
-
-result.string("OFFERID in (select ACTIVITYLINK.OBJECT_ROWID from " + from + " where " + vars.get("$local.condition")+ ")");
\ No newline at end of file
+result.string(cond.toString());
\ No newline at end of file
diff --git a/entity/Offer_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js b/entity/Offer_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
index 33554680cccd2c3d363443686457c41e0c1c5729..c0332af5cb599a99d062e3828f354ebfd005273f 100644
--- a/entity/Offer_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
+++ b/entity/Offer_entity/recordcontainers/db/recordfieldmappings/object_rowid.displayvalue/expression.js
@@ -1,4 +1,4 @@
 import("system.result");
 import("Context_lib");
 
-result.string(ContextUtils.getNameSubselectSql("OBJECT_TYPE", "OBJECT_ROWID"))
\ No newline at end of file
+result.string(ContextUtils.getNameSubselectSql("OFFER.OBJECT_TYPE", "OFFER.OBJECT_ROWID"))
\ No newline at end of file
diff --git a/entity/Offeritem_entity/entityfields/product_id/onValueChange.js b/entity/Offeritem_entity/entityfields/product_id/onValueChange.js
index 3a5de6feaba1a19a9d36f5ed067305250b098734..8ff2c784424efb6750081f6cbbb1aaaeaae7faab 100644
--- a/entity/Offeritem_entity/entityfields/product_id/onValueChange.js
+++ b/entity/Offeritem_entity/entityfields/product_id/onValueChange.js
@@ -21,7 +21,7 @@ if(pid != "")
     var productInfoSubSql = newSelect("DESCRIPTION")
         .from("DESCRIPTIONTRANSLATION")
         .whereIfSet("DESCRIPTIONTRANSLATION.OBJECT_ROWID", "$local.value")
-        .and("DESCRIPTIONTRANSLATION.OBJECT_TYPE = 'Product'")
+        .and("DESCRIPTIONTRANSLATION.OBJECT_TYPE", "Product")
         .andIfSet("DESCRIPTIONTRANSLATION.LANG", "$param.Language_param")
         .toString();
     var ProductDetails = ProductUtils.getProductDetails(pid, PriceListFilter, 
diff --git a/entity/Order_entity/recordcontainers/db/recordfieldmappings/offer_id.displayvalue/expression.js b/entity/Order_entity/recordcontainers/db/recordfieldmappings/offer_id.displayvalue/expression.js
index a86ac585f271d61227c89ec425d97d7c43e2a73d..2aec63a8d1bbd3bd15e9522ccb53fa52f74f7623 100644
--- a/entity/Order_entity/recordcontainers/db/recordfieldmappings/offer_id.displayvalue/expression.js
+++ b/entity/Order_entity/recordcontainers/db/recordfieldmappings/offer_id.displayvalue/expression.js
@@ -1,4 +1,5 @@
 import("system.result");
 import("Context_lib");
+import("system.db");
 
-result.string(ContextUtils.getNameSubselectSql("'Offer'", "SALESORDER.OFFER_ID"));
\ No newline at end of file
+result.string(db.translateStatement(ContextUtils.getNameSql("Offer", "SALESORDER.OFFER_ID")));
\ No newline at end of file
diff --git a/entity/Organisation_entity/Organisation_entity.aod b/entity/Organisation_entity/Organisation_entity.aod
index 4d5db6ed1a8789383791d9793b7c5791ae8f30a6..50afcd2e32295cc01c67e21a154d33520e99a7ce 100644
--- a/entity/Organisation_entity/Organisation_entity.aod
+++ b/entity/Organisation_entity/Organisation_entity.aod
@@ -812,6 +812,10 @@
           <name>LinkedObjectId_param</name>
           <valueProcess>%aditoprj%/entity/Organisation_entity/entityfields/linkedappointments/children/linkedobjectid_param/valueProcess.js</valueProcess>
         </entityParameter>
+        <entityParameter>
+          <name>ErrorOnPermissionDenied</name>
+          <valueProcess>%aditoprj%/entity/Organisation_entity/entityfields/linkedappointments/children/erroronpermissiondenied/valueProcess.js</valueProcess>
+        </entityParameter>
       </children>
     </entityConsumer>
     <entityConsumer>
@@ -955,6 +959,13 @@
           <tooltip>Export fields of this table</tooltip>
           <tooltipProcess>%aditoprj%/entity/Organisation_entity/entityfields/filterviewactiongroup/children/export/tooltipProcess.js</tooltipProcess>
         </entityActionField>
+        <entityActionField>
+          <name>startMarketingWorkflows</name>
+          <title>Start marketing mailing</title>
+          <onActionProcess>%aditoprj%/entity/Organisation_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js</onActionProcess>
+          <isObjectAction v="false" />
+          <iconId>VAADIN:ENVELOPES</iconId>
+        </entityActionField>
       </children>
     </entityActionGroup>
     <entityActionField>
diff --git a/entity/Organisation_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js b/entity/Organisation_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b04867cc90fd72a300747605800b32664d4b5b4
--- /dev/null
+++ b/entity/Organisation_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
@@ -0,0 +1,9 @@
+import("Context_lib");
+import("system.vars");
+import("system.neon");
+
+neon.openContext("MarketingWorkflowLauncher", "MarketingWorkflowLauncherEdit_view", null, neon.OPERATINGSTATE_VIEW, {
+    "ObjectIds_param": JSON.stringify(vars.get("$sys.selection")),
+    "ObjectFilter_param": JSON.stringify(vars.get("$sys.filter")),
+    "ObjectType_param": ContextUtils.getCurrentContextId()
+});
\ No newline at end of file
diff --git a/entity/Organisation_entity/entityfields/linkedappointments/children/erroronpermissiondenied/valueProcess.js b/entity/Organisation_entity/entityfields/linkedappointments/children/erroronpermissiondenied/valueProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..15b1f8d0322593145f40d707c61dfdfd9babf0fe
--- /dev/null
+++ b/entity/Organisation_entity/entityfields/linkedappointments/children/erroronpermissiondenied/valueProcess.js
@@ -0,0 +1,3 @@
+import("system.result");
+
+result.string("false");
\ No newline at end of file
diff --git a/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationgroup_filter/groupQueryProcess.js b/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationgroup_filter/groupQueryProcess.js
index 768e6ca7501cdd94caba44242bd5d9f1b3c76859..aad0c1782f7ca0b380aa12d11d4855978b9d3648 100644
--- a/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationgroup_filter/groupQueryProcess.js
+++ b/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationgroup_filter/groupQueryProcess.js
@@ -13,7 +13,8 @@ var name = vars.get("$local.name");
 var stmt = newSelect(isCount ? groupedColumns : columns)
     .from("ORGANISATION")
     .join("CONTACT", "ORGANISATIONID = ORGANISATION_ID  and PERSON_ID is null")
-    .leftJoin("CLASSIFICATIONSTORAGE", "CLASSIFICATIONSTORAGE.OBJECT_ROWID = CONTACT.CONTACTID and OBJECT_TYPE = 'Organisation'")
+    .leftJoin("CLASSIFICATIONSTORAGE", newWhere("CLASSIFICATIONSTORAGE.OBJECT_ROWID = CONTACT.CONTACTID")
+        .and("CLASSIFICATIONSTORAGE.OBJECT_TYPE", "Organisation"))
 
 if (condition)
 {
diff --git a/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js b/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
index ccf8f498ce2171f22150fbf2c26e89f495ba169b..a4e9edc5b2c7f90da86619beb5d4d968bb0a392b 100644
--- a/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
+++ b/entity/Organisation_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
@@ -11,15 +11,18 @@ var groupedColumns = vars.get("$local.groupedlist") // The coloumns, used for gr
 var order = vars.get("$local.order");               // The order of the result
 var classificationId = vars.get("$local.name");
 classificationId = classificationId.slice(classificationId.lastIndexOf(".") + 1, classificationId.length);
-var valuefield = "''"
-var stmt = "";
 
-stmt = newSelect(isCount ? "1" : columns)
+var sql = newSelect(isCount ? "1" : columns)
                     .from("ORGANISATION")
                     .join("CONTACT", "ORGANISATIONID = ORGANISATION_ID  and PERSON_ID is null")
-                    .leftJoin("CLASSIFICATION", "CLASSIFICATION.OBJECT_ROWID = CONTACT.CONTACTID and OBJECT_TYPE = 'Organisation' and CLASSIFICATION.CLASSIFICATIONTYPE_ID = '" + classificationId + "' ")
-                    .leftJoin("CLASSIFICATIONSCORE", "CLASSIFICATIONSCOREID = CLASSIFICATION.CLASSIFICATIONSCORE_ID " +  (condition != "  " ? " WHERE " + condition : ""))
-                    .groupBy(groupedColumns + (order != null && !isCount ? " ORDER BY " + order : ""))
-                    .toString();
+                    .leftJoin("CLASSIFICATION", newWhere("CLASSIFICATION.OBJECT_ROWID = CONTACT.CONTACTID")
+                        .and("CLASSIFICATION.OBJECT_TYPE", "Organisation")
+                        .and("CLASSIFICATION.CLASSIFICATIONTYPE_ID", classificationId))
+                    .leftJoin("CLASSIFICATIONSCORE", "CLASSIFICATIONSCOREID = CLASSIFICATION.CLASSIFICATIONSCORE_ID")
+                    .whereIfSet(condition.trim())
+                    .groupBy(groupedColumns);
 
-result.string(stmt);
\ No newline at end of file
+if (order != null && !isCount)
+    sql.orderBy(order);
+
+result.string(sql.toString());
\ No newline at end of file
diff --git a/entity/PermissionOverview_entity/recordcontainers/jdito/contentProcess.js b/entity/PermissionOverview_entity/recordcontainers/jdito/contentProcess.js
index 0a21aef128bcefe95904d2fbed4bfd44cf0bccc8..293144a2513456337e2e290106ec4020e21269a3 100644
--- a/entity/PermissionOverview_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/PermissionOverview_entity/recordcontainers/jdito/contentProcess.js
@@ -74,7 +74,7 @@ for each (var entry in rolesOrEntities) { // entry contains either a role or an
                                 .select("ASYS_PERMISSIONSETID")
                                 .from("ASYS_PERMISSIONSET")
                                 .where("ASYS_PERMISSIONSET.ASYS_PERMISSIONSET_ID", entityPermSetId)
-                                .and("ACCESSTYPE = 'R'")
+                                .and("ASYS_PERMISSIONSET.ACCESSTYPE", "R")
                                 .arrayColumn();
 
         currOverview = [entityPermSetId, entry, "VAADIN:CLOSE", "VAADIN:CLOSE", "VAADIN:CLOSE", "VAADIN:CLOSE", "VAADIN:CLOSE"];
diff --git a/entity/Person_entity/Person_entity.aod b/entity/Person_entity/Person_entity.aod
index c66ce949329c5b9e71486cb5802b41b0b2244ddf..42f436e7093dbe2303fabd9df2a098fc792ca049 100644
--- a/entity/Person_entity/Person_entity.aod
+++ b/entity/Person_entity/Person_entity.aod
@@ -884,6 +884,10 @@
           <name>LinkedObjectId_param</name>
           <valueProcess>%aditoprj%/entity/Person_entity/entityfields/appointments/children/linkedobjectid_param/valueProcess.js</valueProcess>
         </entityParameter>
+        <entityParameter>
+          <name>ErrorOnPermissionDenied</name>
+          <valueProcess>%aditoprj%/entity/Person_entity/entityfields/appointments/children/erroronpermissiondenied/valueProcess.js</valueProcess>
+        </entityParameter>
       </children>
     </entityConsumer>
     <entityField>
@@ -1099,9 +1103,10 @@
         </entityActionField>
         <entityActionField>
           <name>startMarketingWorkflows</name>
-          <title>Marketing Workflow</title>
+          <title>Start marketing mailing</title>
           <onActionProcess>%aditoprj%/entity/Person_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js</onActionProcess>
           <isObjectAction v="false" />
+          <iconId>VAADIN:ENVELOPES</iconId>
         </entityActionField>
       </children>
     </entityActionGroup>
diff --git a/entity/Person_entity/entityfields/appointments/children/erroronpermissiondenied/valueProcess.js b/entity/Person_entity/entityfields/appointments/children/erroronpermissiondenied/valueProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..15b1f8d0322593145f40d707c61dfdfd9babf0fe
--- /dev/null
+++ b/entity/Person_entity/entityfields/appointments/children/erroronpermissiondenied/valueProcess.js
@@ -0,0 +1,3 @@
+import("system.result");
+
+result.string("false");
\ No newline at end of file
diff --git a/entity/Person_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js b/entity/Person_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
index 3e462ab4498eb4215bac2daa8457aee59ae8cd2a..2b04867cc90fd72a300747605800b32664d4b5b4 100644
--- a/entity/Person_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
+++ b/entity/Person_entity/entityfields/filterviewactiongroup/children/startmarketingworkflows/onActionProcess.js
@@ -4,5 +4,6 @@ import("system.neon");
 
 neon.openContext("MarketingWorkflowLauncher", "MarketingWorkflowLauncherEdit_view", null, neon.OPERATINGSTATE_VIEW, {
     "ObjectIds_param": JSON.stringify(vars.get("$sys.selection")),
+    "ObjectFilter_param": JSON.stringify(vars.get("$sys.filter")),
     "ObjectType_param": ContextUtils.getCurrentContextId()
 });
\ No newline at end of file
diff --git a/entity/Product_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js b/entity/Product_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
index b8141bc3b760b0439c6a4e6e07c99b38dda33d54..dd188aaf77933759617acc567c9f1ba0cb1313dc 100644
--- a/entity/Product_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
+++ b/entity/Product_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
@@ -1,6 +1,11 @@
+import("Context_lib");
+import("Sql_lib");
 import("system.vars");
 import("system.result");
+//!LibFunction
+var cond = newWhere(null, newSelect("ACTIVITYLINK.OBJECT_ROWID").from("ACTIVITYLINK")
+    .join("ACTIVITY", newWhere("ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID")
+        .and("ACTIVITYLINK.OBJECT_TYPE", ContextUtils.getCurrentContextId()))
+    .where(vars.get("$local.condition")), SqlBuilder.EXISTS());
 
-var from = "ACTIVITYLINK join ACTIVITY on ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID and ACTIVITYLINK.OBJECT_TYPE = 'Product'";
-
-result.string("PRODUCTID in (select ACTIVITYLINK.OBJECT_ROWID from " + from + " where " + vars.get("$local.condition")+ ")");
\ No newline at end of file
+result.string(cond.toString());
\ No newline at end of file
diff --git a/entity/SalesprojectConversionRate_entity/recordcontainers/jdito/contentProcess.js b/entity/SalesprojectConversionRate_entity/recordcontainers/jdito/contentProcess.js
index 95781d32513a9442d468d93568947555bfcdef3d..2f3dad2864f82b5ebd688e2ffea700eec78eaa78 100644
--- a/entity/SalesprojectConversionRate_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/SalesprojectConversionRate_entity/recordcontainers/jdito/contentProcess.js
@@ -37,7 +37,13 @@ if (filter != null)
             })).length > 0
         })[0];
         phasenFilter.operator = "OR";
-        let pre = newSelect("PRE.KEYID").from("AB_KEYWORD_ENTRY").join("AB_KEYWORD_ENTRY", "AB_KEYWORD_ENTRY.SORTING -1 = PRE.SORTING", "PRE").where("AB_KEYWORD_ENTRY.CONTAINER", $KeywordRegistry.salesprojectPhase()).and("PRE.CONTAINER = '" + $KeywordRegistry.salesprojectPhase() + "'").and("AB_KEYWORD_ENTRY.KEYID", phasenFilter.childs[0].value).cell()
+        let pre = newSelect("PRE.KEYID")
+            .from("AB_KEYWORD_ENTRY")
+            .join("AB_KEYWORD_ENTRY", "AB_KEYWORD_ENTRY.SORTING -1 = PRE.SORTING", "PRE")
+            .where("AB_KEYWORD_ENTRY.CONTAINER", $KeywordRegistry.salesprojectPhase())
+            .and(["AB_KEYWORD_ENTRY", "CONTAINER", "PRE"], $KeywordRegistry.salesprojectPhase())
+            .and("AB_KEYWORD_ENTRY.KEYID", phasenFilter.childs[0].value)
+            .cell()
         if (pre != "") {
             let temp = JSON.parse(JSON.stringify(phasenFilter.childs[0]));
             temp.value = temp.key  = pre;
diff --git a/entity/Salesproject_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js b/entity/Salesproject_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
index 524e35692ad0f51d4bebd9dce80d82ebfc6efc80..cf3462e69ddf08a90ec5e2a230b1c0d8bcdd3856 100644
--- a/entity/Salesproject_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
+++ b/entity/Salesproject_entity/recordcontainers/db/filterextensions/classificationtype_filter/groupQueryProcess.js
@@ -10,13 +10,17 @@ var groupedColumns = vars.get("$local.groupedlist") // The coloumns, used for gr
 var order = vars.get("$local.order");               // The order of the result
 var classificationId = vars.get("$local.name");
 classificationId = classificationId.slice(classificationId.lastIndexOf(".") + 1, classificationId.length);
-var valuefield = "''"
-var stmt = "";
 
-stmt = newSelect(isCount ? "1" : columns)
-                    .from("SALESPROJECT")
-                    .leftJoin("CLASSIFICATION", "CLASSIFICATION.OBJECT_ROWID = SALESPROJECT.SALESPROJECTID and CLASSIFICATION.OBJECT_TYPE = 'Salesproject' and CLASSIFICATION.CLASSIFICATIONTYPE_ID = '" + classificationId + "' ")
-                    .leftJoin("CLASSIFICATIONSCORE", "CLASSIFICATIONSCORE.CLASSIFICATIONSCOREID = CLASSIFICATION.CLASSIFICATIONSCORE_ID" +  (condition != "  " ? " WHERE " + condition : ""))
-                    .groupBy(groupedColumns + (order != null && !isCount ? " ORDER BY " + order : ""))
-                    .toString();
-result.string(stmt);
\ No newline at end of file
+var sql = newSelect(isCount ? "1" : columns)
+    .from("SALESPROJECT")
+    .leftJoin("CLASSIFICATION", newWhere("CLASSIFICATION.OBJECT_ROWID = SALESPROJECT.SALESPROJECTID")
+        .and("CLASSIFICATION.OBJECT_TYPE", "Salesproject")
+        .and("CLASSIFICATION.CLASSIFICATIONTYPE_ID", classificationId))
+    .leftJoin("CLASSIFICATIONSCORE", "CLASSIFICATIONSCORE.CLASSIFICATIONSCOREID = CLASSIFICATION.CLASSIFICATIONSCORE_ID")
+    .whereIfSet(condition.trim())
+    .groupBy(groupedColumns);
+
+if (order != null && !isCount)
+    sql.orderBy(order);
+                    
+result.string(sql.toString());
\ No newline at end of file
diff --git a/entity/Salesproject_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js b/entity/Salesproject_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
index 3a8b1fd9f8491be342a16fd7967c756ef82b0e21..dd188aaf77933759617acc567c9f1ba0cb1313dc 100644
--- a/entity/Salesproject_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
+++ b/entity/Salesproject_entity/recordcontainers/db/recordfieldmappings/activities/filterConditionProcess.js
@@ -1,6 +1,11 @@
+import("Context_lib");
+import("Sql_lib");
 import("system.vars");
 import("system.result");
+//!LibFunction
+var cond = newWhere(null, newSelect("ACTIVITYLINK.OBJECT_ROWID").from("ACTIVITYLINK")
+    .join("ACTIVITY", newWhere("ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID")
+        .and("ACTIVITYLINK.OBJECT_TYPE", ContextUtils.getCurrentContextId()))
+    .where(vars.get("$local.condition")), SqlBuilder.EXISTS());
 
-var from = "ACTIVITYLINK join ACTIVITY on ACTIVITY.ACTIVITYID = ACTIVITYLINK.ACTIVITY_ID and ACTIVITYLINK.OBJECT_TYPE = 'Salesproject'";
-
-result.string("SALESPROJECTID in (select ACTIVITYLINK.OBJECT_ROWID from " + from + " where " + vars.get("$local.condition")+ ")");
\ No newline at end of file
+result.string(cond.toString());
\ No newline at end of file
diff --git a/entity/Turnover_entity/recordcontainers/jdito/contentProcess.js b/entity/Turnover_entity/recordcontainers/jdito/contentProcess.js
index bd20eceda69bc4ed288c1090ba77ee4918831b57..fd0e3bd7a6f2c7518d54d02f9351a842f3d25d9b 100644
--- a/entity/Turnover_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/Turnover_entity/recordcontainers/jdito/contentProcess.js
@@ -27,8 +27,8 @@ import("system.translate");
  */
 
 
-var turnoverCategory = translate.text('Turnover');
-var forecastCategory = translate.text('Forecast');
+var turnoverCategory = translate.text("Turnover");
+var forecastCategory = translate.text("Forecast");
 
 var maxYear = parseInt(vars.get("$param.MaxYear_param"));
 var yearCountToShow = parseInt(vars.get("$param.YearCountToShow_param"));
diff --git a/entity/UserhelpResources/UserhelpResources.aod b/entity/UserhelpResources_entity/UserhelpResources_entity.aod
similarity index 70%
rename from entity/UserhelpResources/UserhelpResources.aod
rename to entity/UserhelpResources_entity/UserhelpResources_entity.aod
index 7775d3fd5a4ba8d43ec112062c7772b437bd68c9..32f253d41162e3e5c8b4e33f1bcba45bdc803965 100644
--- a/entity/UserhelpResources/UserhelpResources.aod
+++ b/entity/UserhelpResources_entity/UserhelpResources_entity.aod
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <entity xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.3.17" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/entity/1.3.17">
-  <name>UserhelpResources</name>
+  <name>UserhelpResources_entity</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
-  <documentation>%aditoprj%/entity/UserhelpResources/documentation.adoc</documentation>
+  <documentation>%aditoprj%/entity/UserhelpResources_entity/documentation.adoc</documentation>
   <entityFields>
     <entityProvider>
       <name>#PROVIDER</name>
@@ -12,21 +12,21 @@
     </entityField>
     <entityConsumer>
       <name>Documents</name>
-      <dependency>
-        <name>dependency</name>
-        <entityName>Document_entity</entityName>
-        <fieldName>Documents</fieldName>
-      </dependency>
       <children>
         <entityParameter>
           <name>AssignmentTable_param</name>
-          <valueProcess>%aditoprj%/entity/UserhelpResources/entityfields/documents/children/assignmenttable_param/valueProcess.js</valueProcess>
+          <valueProcess>%aditoprj%/entity/UserhelpResources_entity/entityfields/documents/children/assignmenttable_param/valueProcess.js</valueProcess>
         </entityParameter>
         <entityParameter>
           <name>AssignmentRowId_param</name>
-          <valueProcess>%aditoprj%/entity/UserhelpResources/entityfields/documents/children/assignmentrowid_param/valueProcess.js</valueProcess>
+          <valueProcess>%aditoprj%/entity/UserhelpResources_entity/entityfields/documents/children/assignmentrowid_param/valueProcess.js</valueProcess>
         </entityParameter>
       </children>
+      <dependency>
+        <name>dependency</name>
+        <entityName>Document_entity</entityName>
+        <fieldName>Documents</fieldName>
+      </dependency>
     </entityConsumer>
     <entityProvider>
       <name>#PROVIDER_AGGREGATES</name>
diff --git a/entity/UserhelpResources/documentation.adoc b/entity/UserhelpResources_entity/documentation.adoc
similarity index 100%
rename from entity/UserhelpResources/documentation.adoc
rename to entity/UserhelpResources_entity/documentation.adoc
diff --git a/entity/UserhelpResources/entityfields/documents/children/assignmentrowid_param/valueProcess.js b/entity/UserhelpResources_entity/entityfields/documents/children/assignmentrowid_param/valueProcess.js
similarity index 100%
rename from entity/UserhelpResources/entityfields/documents/children/assignmentrowid_param/valueProcess.js
rename to entity/UserhelpResources_entity/entityfields/documents/children/assignmentrowid_param/valueProcess.js
diff --git a/entity/UserhelpResources/entityfields/documents/children/assignmenttable_param/valueProcess.js b/entity/UserhelpResources_entity/entityfields/documents/children/assignmenttable_param/valueProcess.js
similarity index 100%
rename from entity/UserhelpResources/entityfields/documents/children/assignmenttable_param/valueProcess.js
rename to entity/UserhelpResources_entity/entityfields/documents/children/assignmenttable_param/valueProcess.js
diff --git a/entity/VisitPlanEntry_entity/recordcontainers/jdito/contentProcess.js b/entity/VisitPlanEntry_entity/recordcontainers/jdito/contentProcess.js
index ce8bb62314fa0a0ae0023f81b31d95a3871bd812..ae8d3918edde1c772c1f09bb3623506cce40cdc1 100644
--- a/entity/VisitPlanEntry_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/VisitPlanEntry_entity/recordcontainers/jdito/contentProcess.js
@@ -46,19 +46,19 @@ if(entryData.length > 0)
 {
     for(var i = 0; i < entryData.length; i++)
     {
-        var entryDateRaw, visitPlanEntryId, beginn_time, end_time, organisationContact_id, contact_id, status, visitplanemployeeweek_id, appointmentid;
-        [entryDateRaw, visitPlanEntryId, beginn_time, end_time, organisationContact_id, contact_id, status, visitplanemployeeweek_id, appointmentid] = entryData[i]
+        var entryDateRaw, visitPlanEntryId, beginTime, endTime, organisationContactId, contactId, status, visitplanEmployeeWeekId, appointmentid;
+        [entryDateRaw, visitPlanEntryId, beginTime, endTime, organisationContactId, contactId, status, visitplanEmployeeWeekId, appointmentid] = entryData[i]
         
-        var contactname = db.cell(PersUtils.getResolvingDisplaySubSql("'" + contact_id + "'"));
-        var orgname = OrganisationUtils.getNameByContactId(organisationContact_id);
+        var contactname = ContactUtils.getTitleByContactId(contactId);
+        var orgname = OrganisationUtils.getNameByContactId(organisationContactId);
         var parentName = translate.text(datetime.toDate(entryDateRaw, "EEEE"));
         entryDate = datetime.toDate(entryDateRaw, "dd.MM.yyyy");
 
         var statusDisplay = KeywordUtils.getViewValue($KeywordRegistry.visitPlanEntryStatus(), status)
         var alias = SqlUtils.getSystemAlias();
 
-        items.push([visitPlanEntryId, false, parentName + "#" + entryDate, "",  beginn_time
-            , end_time, organisationContact_id, orgname, contact_id, contactname, entryDateRaw, status, visitplanemployeeweek_id, appointmentid, statusDisplay]);
+        items.push([visitPlanEntryId, false, parentName + "#" + entryDate, "",  beginTime
+            , endTime, organisationContactId, orgname, contactId, contactname, entryDateRaw, status, visitplanEmployeeWeekId, appointmentid, statusDisplay]);
 
         if(!vars.get("$local.idvalues"))
         {
diff --git a/entity/VisitRecommendation_entity/recordcontainers/jdito/contentProcess.js b/entity/VisitRecommendation_entity/recordcontainers/jdito/contentProcess.js
index 7effd511c0a20b96a072e8df4ba4e7650ede81c9..42fecc4e3d198603e333edf0c71beed523ff615a 100644
--- a/entity/VisitRecommendation_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/VisitRecommendation_entity/recordcontainers/jdito/contentProcess.js
@@ -271,7 +271,7 @@ function _getPrioByDueDateSubSql (pDueDateField, pPriorityField)
 {
     var currentDate = datetime.date();
     var sqlMasker = new SqlMaskingUtils();
-    
+    //!SqlBuilder
     var subSql = "case when " + pDueDateField + " < ? then '" + $KeywordRegistry.visitRecommendationPriority$critical()
         + "' when " + pDueDateField + " < ? then '" + $KeywordRegistry.visitRecommendationPriority$veryHigh()
         + "' when " + pDueDateField + " < ? then '" + $KeywordRegistry.visitRecommendationPriority$high()
diff --git a/entity/WorkflowLauncher_entity/WorkflowLauncher_entity.aod b/entity/WorkflowLauncher_entity/WorkflowLauncher_entity.aod
index f1640dc1aacfcbb6ebd9de2ce5afb3d6ca8b92c9..e5749fe5101007e83c45d26c9f24bd15561458b1 100644
--- a/entity/WorkflowLauncher_entity/WorkflowLauncher_entity.aod
+++ b/entity/WorkflowLauncher_entity/WorkflowLauncher_entity.aod
@@ -88,6 +88,7 @@
   <recordContainers>
     <datalessRecordContainer>
       <name>dataLess</name>
+      <alias>Data_alias</alias>
     </datalessRecordContainer>
   </recordContainers>
 </entity>
diff --git a/entity/WorkflowTask_entity/afterSave.js b/entity/WorkflowTask_entity/afterSave.js
index 68fae8b0d2432769e1c21ae7d594d02fd20cdb87..61b54c53434b45f42ffd7184e88c153966cfac5c 100644
--- a/entity/WorkflowTask_entity/afterSave.js
+++ b/entity/WorkflowTask_entity/afterSave.js
@@ -30,9 +30,9 @@ if (entityData["FORMRESULT"])
     else
     {
         var params = {
-            "TaskTitle_param" : rowData["NAME.value"]
+            "TaskTitle_param": entityData["NAME"]
         };
         //if you try to open the task now, it will display "Task done"
-        neon.openContext("WorkflowTask", "WorkflowTaskPreview_view", [rowData["UID.value"]], neon.OPERATINGSTATE_VIEW, params);
+        neon.openContext("WorkflowTask", "WorkflowTaskPreview_view", [entityData["UID"]], neon.OPERATINGSTATE_VIEW, params);
     }
 }
\ No newline at end of file
diff --git a/language/_____LANGUAGE_EXTRA/_____LANGUAGE_EXTRA.aod b/language/_____LANGUAGE_EXTRA/_____LANGUAGE_EXTRA.aod
index bee7fd6513ec22226a7dec19b5ee03fd0a3c2cdf..3439544cfe83b426af1216d5545709508f9cdf3e 100644
--- a/language/_____LANGUAGE_EXTRA/_____LANGUAGE_EXTRA.aod
+++ b/language/_____LANGUAGE_EXTRA/_____LANGUAGE_EXTRA.aod
@@ -7293,6 +7293,9 @@
     <entry>
       <key>Standard Zip</key>
     </entry>
+    <entry>
+      <key>Outstanding Amount</key>
+    </entry>
   </keyValueMap>
   <font name="Dialog" style="0" size="11" />
   <sqlModels>
diff --git a/language/_____LANGUAGE_de/_____LANGUAGE_de.aod b/language/_____LANGUAGE_de/_____LANGUAGE_de.aod
index b3d003710b7d14ec99d63e66bbfcee469231a149..5ae81ef6a8a2856d375b73be7156726e401553e4 100644
--- a/language/_____LANGUAGE_de/_____LANGUAGE_de.aod
+++ b/language/_____LANGUAGE_de/_____LANGUAGE_de.aod
@@ -2795,6 +2795,10 @@
       <key>new</key>
       <value>neu</value>
     </entry>
+    <entry>
+      <key>Start marketing mailing</key>
+      <value>Marketing Mailing starten</value>
+    </entry>
     <entry>
       <key>Adviser</key>
       <value>Berater</value>
@@ -6548,7 +6552,7 @@
     </entry>
     <entry>
       <key>Order number</key>
-      <value>Belegsnummer</value>
+      <value>Belegnummer</value>
     </entry>
     <entry>
       <key>Permission Action</key>
@@ -6568,7 +6572,7 @@
     </entry>
     <entry>
       <key>Print reminder</key>
-      <value>Mahnung drucken</value>
+      <value>Mahnung anzeigen</value>
     </entry>
     <entry>
       <key>Rech.-Betrag</key>
@@ -6576,7 +6580,7 @@
     </entry>
     <entry>
       <key>Order date</key>
-      <value>Belegsdatum</value>
+      <value>Belegdatum</value>
     </entry>
     <entry>
       <key>Due date</key>
@@ -6628,7 +6632,7 @@
     </entry>
     <entry>
       <key>This error should never appear - contact administrator (PermissionDetail_entity.PermissionAction.onValidation).</key>
-      <value>Dieser Fehler sollte nie erscheinen - kontaktieren sie einen Administrator (PermissionDetail_entity.PermissionAction.onValidation).</value>
+      <value>Dieser Fehler sollte nie erscheinen - kontaktieren Sie einen Administrator (PermissionDetail_entity.PermissionAction.onValidation).</value>
     </entry>
     <entry>
       <key>Empty actions are invalid!</key>
@@ -9270,7 +9274,7 @@ Bitte Datumseingabe prüfen</value>
     </entry>
     <entry>
       <key>Child Attributes</key>
-      <value>Untergeordnete Eigenschaften</value>
+      <value>Diagrameigenschaften</value>
     </entry>
     <entry>
       <key>Recalculate all Classifications</key>
@@ -9430,16 +9434,16 @@ Bitte Datumseingabe prüfen</value>
       </value>
     </entry>
     <entry>
-      <key>LinkedIn (Person)</key>
-      <value>LinkedIn (Person)</value>
+      <key>Linked in (Person)</key>
+      <value>Linked In (Person)</value>
     </entry>
     <entry>
       <key>Mobile number (Organisation)</key>
       <value>Handynummer (Organisation)</value>
     </entry>
     <entry>
-      <key>LinkedIn (Organisation)</key>
-      <value>LinkedIn (Organisation)</value>
+      <key>Linked in (Organisation)</key>
+      <value>Linked In (Organisation)</value>
     </entry>
     <entry>
       <key>Xing (Organisation)</key>
@@ -9674,6 +9678,10 @@ Bitte Datumseingabe prüfen</value>
       <key>Standard Zip</key>
       <value>Standart Plz</value>
     </entry>
+    <entry>
+      <key>Outstanding Amount</key>
+      <value>Offener Betrag</value>
+    </entry>
     <entry>
       <key>#rememberme</key>
       <value>Angemeldet bleiben</value>
diff --git a/language/_____LANGUAGE_en/_____LANGUAGE_en.aod b/language/_____LANGUAGE_en/_____LANGUAGE_en.aod
index 5cdb447d6f2e8ede8872d869542c99486b40818c..193aaea85b4963c84362c964b8e24578e77c0212 100644
--- a/language/_____LANGUAGE_en/_____LANGUAGE_en.aod
+++ b/language/_____LANGUAGE_en/_____LANGUAGE_en.aod
@@ -7374,6 +7374,9 @@
     <entry>
       <key>Standard Zip</key>
     </entry>
+    <entry>
+      <key>Outstanding Amount</key>
+    </entry>
     <entry>
       <key>#rememberme</key>
       <value>Stay logged in</value>
diff --git a/neonContext/MarketingWorkflowLauncher/MarketingWorkflowLauncher.aod b/neonContext/MarketingWorkflowLauncher/MarketingWorkflowLauncher.aod
index bad0c03850983d94a74b3d3af4896308fb1e2086..e200a8a70f878ecb8c691752d3585cf3880e5bef 100644
--- a/neonContext/MarketingWorkflowLauncher/MarketingWorkflowLauncher.aod
+++ b/neonContext/MarketingWorkflowLauncher/MarketingWorkflowLauncher.aod
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <neonContext xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonContext/1.1.1">
   <name>MarketingWorkflowLauncher</name>
-  <title>f</title>
+  <title>Marketing workflow</title>
   <majorModelMode>DISTRIBUTED</majorModelMode>
   <entity>MarketingWorkflowLauncher_entity</entity>
   <references>
diff --git a/neonContext/Userhelp/Userhelp.aod b/neonContext/Userhelp/Userhelp.aod
index 435de68c2c1e230ca426a074593bb1004a7ebf6d..057b38dda494f9366cb2e2f617314104f8fd2dd3 100644
--- a/neonContext/Userhelp/Userhelp.aod
+++ b/neonContext/Userhelp/Userhelp.aod
@@ -3,7 +3,7 @@
   <name>Userhelp</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
   <filterView>Userhelp_FilterView</filterView>
-  <entity>UserhelpResources</entity>
+  <entity>UserhelpResources_entity</entity>
   <references>
     <neonViewReference>
       <name>2a1dd62b-0f30-442b-aa1d-969b46312d2a</name>
diff --git a/neonView/SalesprojectAnalyses_view/SalesprojectAnalyses_view.aod b/neonView/SalesprojectAnalyses_view/SalesprojectAnalyses_view.aod
index 1228d3cc49bdac80030aec42937bcdb4fc9c643a..67c3c7369040ce1004d8d223db8fa989e450cef7 100644
--- a/neonView/SalesprojectAnalyses_view/SalesprojectAnalyses_view.aod
+++ b/neonView/SalesprojectAnalyses_view/SalesprojectAnalyses_view.aod
@@ -8,7 +8,7 @@
       <name>SalesprojectPhases</name>
       <title>Salesproject phases</title>
       <description>Shows how many sales projects are in the various sales phases</description>
-      <fragment>Salesproject/filter?search=eyJ0eXBlIjoiZ3JvdXAiLCJvcGVyYXRvciI6IkFORCIsImNoaWxkcyI6W3sidHlwZSI6InJvdyIsIm5hbWUiOiJQSEFTRSIsIm9wZXJhdG9yIjoiTk9UX0VRVUFMIiwidmFsdWUiOiJOZWdvdGlhdGlvbiIsImtleSI6IlNBTFBST0pQSEFTRU5FR08iLCJjb250ZW50dHlwZSI6IlRFWFQifV19&amp;axes=COUNT&amp;grouping=%23EXTENSION.Phase_filterExtention.Phase_filterExtention%23TEXT</fragment>
+      <fragment>Salesproject/filter?search=eyJ0eXBlIjoiZ3JvdXAiLCJvcGVyYXRvciI6IkFORCIsImNoaWxkcyI6W3sidHlwZSI6InJvdyIsIm5hbWUiOiJQSEFTRSIsIm9wZXJhdG9yIjoiTk9UX0VRVUFMIiwidmFsdWUiOiJOZWdvdGlhdGlvbiIsImtleSI6IlNBTFBST0pQSEFTRU5FR08iLCJjb250ZW50dHlwZSI6IlRFWFQifSx7InR5cGUiOiJyb3ciLCJuYW1lIjoiU1RBVFVTIiwib3BlcmF0b3IiOiJFUVVBTCIsInZhbHVlIjoiT2ZmZW4iLCJrZXkiOiJTQUxQUk9KU1RBVE9QRU4iLCJjb250ZW50dHlwZSI6IlRFWFQifV19&amp;axes=COUNT&amp;grouping=%23EXTENSION.Phase_filterExtention.Phase_filterExtention%23TEXT</fragment>
       <singleton v="true" />
       <storeRoles>
         <element>PROJECT_FieldStaff</element>
diff --git a/process/Calendar_lib/process.js b/process/Calendar_lib/process.js
index 0a9a4b99e7c0c1abbe9d41af4f6ca2b2a734cab1..e9f96a4a9089f1ff65bb69dfb21ff41063f90ab8 100644
--- a/process/Calendar_lib/process.js
+++ b/process/Calendar_lib/process.js
@@ -538,4 +538,113 @@ CalendarUtil.getCalendarSystemType = function(pScope)
 
     // Everything is none
     return calendars.BACKEND_NONE;
+}
+
+CalendarUtil.buildEntriesFromUids = function(appointmentUids) 
+{
+    var entryArray = new Array(appointmentUids.length);
+    
+    for(var i = 0; i < appointmentUids.length; i++)
+    {
+        var hasPermission = true;
+        
+        if(vars.get("$param.ErrorOnPermissionDenied") == "false" || vars.getString("$param.LinkedAppointmentsFromDashlet_param"))
+            hasPermission = hasUserPermissionForReadingEntry(getEntryOwnerCn(appointmentUids[i]));
+       
+        if(hasPermission)
+            entryArray[i] = CalendarUtil.buildEntry(calendars.getEntry(appointmentUids[i], null, null), null);
+    }
+    
+    //filter out all null
+    var filteredEntryArray = entryArray.filter(function (el) {
+        return el != null;
+    });
+    
+    return filteredEntryArray;
+}
+
+
+CalendarUtil.countEntriesFromUids = function(appointmentUids) 
+{
+    return CalendarUtil.buildEntriesFromUids(appointmentUids).length;
+}
+
+CalendarUtil.buildEntry = function (pEntry, pMasterentry)
+{
+    var uid = pEntry[calendars.ID];    
+    var summary = pEntry[calendars.SUMMARY];
+    var attendees = pEntry[calendars.AFFECTEDUSERS];
+    var startdate = pEntry[calendars.DTSTART];
+    var enddate = pEntry[calendars.DTEND];
+    var links = pEntry[calendars.LINKS];
+    var description = pEntry[calendars.DESCRIPTION];
+    if(pEntry[calendars.ORGANIZER2] != undefined)
+        var organizer = pEntry[calendars.ORGANIZER2]["paramvalue"];
+    if(pEntry[calendars.USER2] != undefined)
+        var owner = JSON.stringify(pEntry[calendars.USER2]);
+    var status = pEntry[calendars.STATUS];
+    var location = pEntry[calendars.LOCATION];
+    var reminder = pEntry[calendars.REMINDER_DURATION];
+    var remindercheck = pEntry[calendars.HASREMINDER]
+    var classification = pEntry[calendars.CLASSIFICATION];
+    var transparency = pEntry[calendars.TRANSPARENCY];
+    var categories = pEntry[calendars.CATEGORIES];
+    var isAllDay = pEntry["X-ADITO-ISALLDAYEVENT"] != null ? pEntry["X-ADITO-ISALLDAYEVENT"] : "FALSE";
+    
+    var masterBegin = pMasterentry != null ? pMasterentry[calendars.DTSTART] : null
+    var masterEnd = pMasterentry != null ? pMasterentry[calendars.DTEND] : null
+    
+    // Recurrence
+    var recurrenceID = pEntry[calendars.RECURRENCEID];
+    var rrule = null;
+    if (pMasterentry != null) { // Entry is a recurrence exception, therefore get rrule from master
+        rrule = pMasterentry[calendars.RRULE] != null ? pMasterentry[calendars.RRULE][0] : null;
+    } else {
+        rrule = pEntry[calendars.RRULE] != null ? pEntry[calendars.RRULE][0] : null;
+    }
+    
+    return [
+            uid, 
+            attendees.length, 
+            startdate, 
+            enddate, 
+            summary, 
+            organizer,
+            owner,
+            attendees, 
+            status,  
+            description, 
+            location, 
+            '', 
+            isAllDay,
+            classification,
+            transparency, 
+            categories, 
+            reminder, 
+            remindercheck, 
+            rrule, 
+            recurrenceID, 
+            null, 
+            masterBegin, 
+            masterEnd,
+            null
+        ];
+}
+
+
+function hasUserPermissionForReadingEntry(calUserCn)
+{
+    return calendars.hasPermission(calUserCn, calendars.VEVENT, "READ");
+}
+
+function getEntryOwnerCn(appointmentUid)
+{
+    
+    var owner = newSelect("ASYS_CALENDARBACKEND.OWNER", "_____SYSTEMALIAS")
+        .from("ASYS_CALENDARBACKEND")
+        .whereIfSet("ASYS_CALENDARBACKEND.ELEMENTUID", appointmentUid)
+        .cell(true);
+        
+    var ownerArr = text.decodeMS(owner);
+    return ownerArr[1].split(":")[1];
 }
\ No newline at end of file
diff --git a/process/Contact_lib/process.js b/process/Contact_lib/process.js
index 627ec11b149a9b0ef93163901dda95b23f7f98b9..4d53dde63a7f80b5e864330f7aec39f0d3f405ee 100644
--- a/process/Contact_lib/process.js
+++ b/process/Contact_lib/process.js
@@ -528,7 +528,7 @@ ContactUtils.getActiveCommRestrictionsSubselect = function()
                             .and(newWhere()
                                     .or("COMMRESTRICTION.CONTACT_ID = CONTACT.CONTACTID")
                                     .or("COMMRESTRICTION.CONTACT_ID", orgContactSubselect));
-
+        //!SqlBuilder
         parts.push("case when exists(" + subquery.toString() + ") then '" + pMedium[1] + "' else '' end");
     })
 
@@ -661,7 +661,7 @@ function ContactTitleRenderer(pContact, pOptions)
         var maskingUtil = new SqlMaskingUtils();
         var res = maskingUtil.concat([this.contact.salutation, this.contact.title, this.contact.firstname, this.contact.middlename, this.contact.lastname].filter(function (e){
             return e != "";
-        }), " ");
+        }), " ", false);
         //binary AND check for possibility to check serveral options
         if (this._options & ContactTitleRenderer.OPTIONS.IncludeOrganisation && this.contact.organisationName)
             res = maskingUtil.concat([res, this.contact.organisationName], " | ");
diff --git a/process/Context_lib/process.js b/process/Context_lib/process.js
index 31d608284325922fc657f5e9388ee3782d68e8eb..a9b9eac3121960b68e3d6a4cf4d740fa3aa21457 100644
--- a/process/Context_lib/process.js
+++ b/process/Context_lib/process.js
@@ -494,10 +494,14 @@ ContextSelector.prototype.setGroupBy = function(pValue)
 ContextUtils.getSelectMap  = function()
 {
     var maskingUtils = new SqlMaskingUtils();
+    var isOracle = maskingUtils.dbType == db.DBTYPE_ORACLE10_CLUSTER
+        || maskingUtils.dbType == db.DBTYPE_ORACLE10_OCI
+        || maskingUtils.dbType == db.DBTYPE_ORACLE10_THIN;
+    
     return {
             "Organisation": ContextSelector.create("ORGANISATION", "CONTACT.CONTACTID", "ORGANISATION.NAME")
                                        .setJoinExpression("join CONTACT on ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and CONTACT.PERSON_ID is null")
-                                       .setCondition(newWhere("ORGANISATION.ORGANISATIONID != '0'"))
+                                       .setCondition(newWhere("ORGANISATION.ORGANISATIONID", "0", SqlBuilder.NOT_EQUAL()))
                                        .setSubContexts({
                                            "Person": [newSelect("CONTACTID").from("CONTACT").where("PERSON_ID is not null"), "CONTACT.ORGANISATION_ID", ["Offer", "Order", "Contract", "SupportTicket"]]
                                        })
@@ -536,31 +540,31 @@ ContextUtils.getSelectMap  = function()
                                             .setStateField("STATUS")
                                             .setActiveStates([$KeywordRegistry.salesprojectState$open(), $KeywordRegistry.salesprojectState$postponed()])
             ,"Contract": ContextSelector.create("CONTRACT", "CONTRACTID")
-                                        .setTitleExpression(maskingUtils.concat([
+                                        .setTitleExpression(maskingUtils.cast(maskingUtils.concat([
                                                                 KeywordUtils.getResolvedTitleSqlPart("ContractType", "CONTRACTTYPE"),
                                                                 maskingUtils.cast("CONTRACTCODE", SQLTYPES.VARCHAR, 10)
-                                                                ], " "))
+                                                                ], " "), isOracle ? SQLTYPES.NVARCHAR : SQLTYPES.VARCHAR, 50))
                                         .setContactIdField("CONTACT_ID")
                                         .setCreationDateField("CONTRACTSTART")
                                         .setStateField("CONTRACTSTATUS")
                                         .setActiveStates([$KeywordRegistry.contractState$validLimited(), $KeywordRegistry.contractState$validUnlimited(), $KeywordRegistry.contractState$notSigned()])
             ,"Offer": ContextSelector.create("OFFER", "OFFERID")
-                                     .setTitleExpression(maskingUtils.concat([
+                                     .setTitleExpression(maskingUtils.cast(maskingUtils.concat([
                                                 "'" + translate.text("Offer") + "'",
                                                 "' '",
                                                 maskingUtils.cast("OFFERCODE", SQLTYPES.VARCHAR, 10),
                                                 "'-'",
                                                 maskingUtils.cast("VERSNR", SQLTYPES.VARCHAR, 10)
-                                                ], "", false))
+                                                ], "", false), isOracle ? SQLTYPES.NVARCHAR : SQLTYPES.VARCHAR, 50))
                                      .setContactIdField("CONTACT_ID")
                                      .setCreationDateField("OFFERDATE")
                                      .setStateField("STATUS")
                                      .setActiveStates([$KeywordRegistry.offerStatus$open(), $KeywordRegistry.offerStatus$checked(), $KeywordRegistry.offerStatus$sent()])
             ,"Order": ContextSelector.create("SALESORDER", "SALESORDERID")
-                                     .setTitleExpression(maskingUtils.concat([
+                                     .setTitleExpression(maskingUtils.cast(maskingUtils.concat([
                                                         KeywordUtils.getResolvedTitleSqlPart("OrderType", "ORDERTYPE"),
                                                         maskingUtils.cast("SALESORDERCODE", SQLTYPES.VARCHAR, 10)
-                                                        ], " "))
+                                                        ], " "), isOracle ? SQLTYPES.NVARCHAR : SQLTYPES.VARCHAR, 50))
                                      .setContactIdField("CONTACT_ID")
                                      .setCreationDateField("SALESORDERDATE")
                                      .setStateField("ORDERSTATUS")
@@ -607,20 +611,19 @@ ContextUtils.getSelectMap  = function()
  */
 ContextUtils.getNameSubselectSql = function(pContextIdDbField, pRowIdDbField)
 {
-    // TODO: prepared?
-    
-    var select = "(case " + pContextIdDbField + " ";
+    var select = SqlBuilder.caseStatement()
 
-    var selectMap = ContextUtils.getSelectMap ()
+    var selectMap = ContextUtils.getSelectMap();
     for (let contextId in selectMap)
     {
-        select += "when '" + contextId + "' then (select " + selectMap[contextId].titleExpression + " from " + selectMap[contextId].getFullFromClause() + (pRowIdDbField ? " where " + selectMap[contextId].getFullIdField() + " = " + pRowIdDbField : " ") + ") ";
+        let titleSelect = newSelect(selectMap[contextId].titleExpression)
+            .from(selectMap[contextId].getFullFromClause())
+            .where(selectMap[contextId].getFullIdField() + " = " + pRowIdDbField);
+            
+        select.when(pContextIdDbField, contextId).then(titleSelect);
     }
 
-    select += "else 'Not defined in ContextUtils.getNameSql()!'";
-    select += "end)";
-
-    return select;
+    return select.toString();
 }
 
 /**
diff --git a/process/FilterViewAction_lib/process.js b/process/FilterViewAction_lib/process.js
index b0fa04291d03107498b6eaf03adbe5aa17326186..afd89d4d6d45e7afbd01146356a20eadd053896f 100644
--- a/process/FilterViewAction_lib/process.js
+++ b/process/FilterViewAction_lib/process.js
@@ -40,7 +40,7 @@ FilterViewActionUtils.getUidsByEntityFilter = function (pContext, pFilter)
     {
         return new SqlBuilder()
             .selectDistinct("CONTACT.CONTACTID")
-            .from("PERSON")
+            .from("ORGANISATION")
             .join("CONTACT", newWhere("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID").and("CONTACT.PERSON_ID is null"))
             .leftJoin("ADDRESS", "ADDRESS.ADDRESSID = CONTACT.ADDRESS_ID")
             .leftJoin("CLASSIFICATIONSTORAGE", "CLASSIFICATIONSTORAGE.OBJECT_ROWID = CONTACT.CONTACTID")
@@ -52,7 +52,10 @@ FilterViewActionUtils.getUidsByEntityFilter = function (pContext, pFilter)
     var loadRowsConfig = entities.createConfigForLoadingRows()
         .entity(ContextUtils.getEntity(pContext))
         .fields(["#UID"])
-        .filter(JSON.stringify(pFilter.filter || pFilter));
+    if (pFilter.filter)
+        loadRowsConfig.filter(JSON.stringify(pFilter.filter));
+    else if (pFilter)
+        loadRowsConfig.filter(JSON.stringify(pFilter));
         
     return entities.getRows(loadRowsConfig).map(function (row)
     {
diff --git a/process/KeywordAttribute_test/KeywordAttribute_test.aod b/process/KeywordAttribute_test/KeywordAttribute_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..5956980e85aba58177a5e931c819b50b4894f796
--- /dev/null
+++ b/process/KeywordAttribute_test/KeywordAttribute_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>KeywordAttribute_test</name>
+  <title>[TEST] KeywordAttribute_lib</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/KeywordAttribute_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/KeywordAttribute_test/process.js b/process/KeywordAttribute_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..888e3a79bc304ec2fca6ccce31ec19b6cf546955
--- /dev/null
+++ b/process/KeywordAttribute_test/process.js
@@ -0,0 +1,144 @@
+import("system.result");
+import("system.translate");
+import("system.vars");
+import("Keyword_lib");
+import("UnitTest_lib");
+import("Sql_lib");
+
+
+var constructor = new TestSuite("KeywordAttribute.constructor", [
+    new Test("should throw error if no data is found and no default value is defined",
+        function(pTester) {
+            var exception = new Error(translate.withArguments("no keyword attribute \"%0\" found in keyword container \"%1\"", ["contacts", "AddressType"]));
+            pTester.expectThat(function() {
+                new KeywordAttribute("AddressType", "contacts");
+            }).throwsException(exception).assert();
+        }
+    ),
+
+    new Test("should not throw error if no data is found but default value is defined",
+        function(pTester) {
+            pTester.expectThat(function() {
+                new KeywordAttribute("AddressType", "contact", "testDefault");
+            }).not().throwsException().assert();
+        }
+    ),
+]);
+
+
+var getValue = new TestSuite("KeywordAttribute.getValue", [
+    new Test("should throw error if attribute does not exist in container and no default value is defined",
+        function(pTester) {
+            var exception = new Error(translate.withArguments("no keyword attribute \"%0\" found in keyword container \"%1\"", ["asdf", "AddressType"]));
+
+            pTester.expectThat(function() {
+                var ka = new KeywordAttribute("AddressType", "asdf");
+                ka.getValue("abc");
+            }).throwsException(exception).assert();
+        }
+    ),
+        
+    new Test("should throw error if attribute is not assigned to keyword and no default value is defined",
+        function(pTester) {
+            var exception = new Error(translate.withArguments("no keyword attribute \"%0\" found in keyword \"%1\" from container \"%2\"", ["contact", "abc", "AddressType"]));
+
+            pTester.expectThat(function() {
+                var ka = new KeywordAttribute("AddressType", "contact");
+                ka.getValue("abc");
+            }).throwsException(exception).assert();
+        }
+    ),
+
+    new Test("should return default value if defined but attribute does not exist in container",
+        function(pTester) {
+            var ka = new KeywordAttribute("AddressType", "asdf", "testDefault");
+            var actualValue = ka.getValue("HOMEADDR");
+
+            pTester.expectThat(actualValue).equals("testDefault").assert();
+        }
+    ),
+
+    new Test("should return default value if defined and attribute exists in container but is not assigned to keyword",
+        function(pTester) {
+            var ka = new KeywordAttribute("AddressType", "contact", "testDefault");
+            var actualValue = ka.getValue("abc");
+
+            pTester.expectThat(actualValue).equals("testDefault").assert();
+        }
+    ),
+
+    new Test("should return keywords boolean value (actually a number)",
+        function(pTester) {
+            var ka = new KeywordAttribute("AddressType", "organisation");
+            var actualValue = ka.getValue("HOMEADDR");
+
+            pTester.expectThat(actualValue).isNumeric().assert();
+            pTester.expectThat(actualValue).equals("0").assert();
+        }
+    ),
+
+    new Test("should return keywords number value (actually a string)",
+        function(pTester) {
+            var ka = new KeywordAttribute("PaymentTerm", "dayNumber");
+            var actualValue = ka.getValue("PAYTERM30");
+
+            pTester.expectThat(actualValue).isNumeric().assert();
+            pTester.expectThat(actualValue).isString().assert();
+            pTester.expectThat(actualValue).equals("30.00").assert();
+        }
+    ),
+        
+    new Test("should return keywords char value (string)",
+        function(pTester) {
+            var ka = new KeywordAttribute("TaskStatus", "icon");
+            var actualValue = ka.getValue("ASSIGNED");
+
+            pTester.expectThat(actualValue).isString().assert();
+            pTester.expectThat(actualValue).equals("NEON:STATUS_ASSIGNED").assert();
+        }
+    ),
+        
+    new Test("should return keywords long char value (string)",
+        function(pTester) {
+            var ka = new KeywordAttribute("TicketType", "attributes");
+            var actualValue = ka.getValue("SUPPORTTICKET");
+
+            pTester.expectThat(actualValue).isString().assert();
+            pTester.expectThat(actualValue).equals('["ff8b1caf-cf30-4edb-b5ca-a9a219ba8399"]').assert();
+        }
+    ),
+]);
+
+
+var getSqlBuilderSelect = new TestSuite("KeywordAttribute.getSqlBuilderSelect", [
+    new Test("should return a SqlBuilder instance",
+        function(pTester) {
+            var ka = new KeywordAttribute("MemberRole", "Intern");
+            /** @type {SqlBuilder} */
+            var actualValue = ka.getSqlBuilderSelect();
+            
+            var expectQueryResult = [
+                "039fd6ae-b4ad-431e-86bf-59ed2f4df0a9",
+                "8cb1b843-713a-4193-aa50-9f5ca06820f8",
+                "9c421b0b-8529-4e07-9463-28d59fd027b6",
+                "b72294cd-3a46-4f71-ab93-72824f63f7f4",
+                "f78f229f-f809-4bd2-aca8-24e2f82fa220",
+            ];
+            var actualQueryResult = actualValue.orderBy("AB_KEYWORD_ATTRIBUTERELATION.AB_KEYWORD_ENTRY_ID").arrayColumn();
+
+            pTester.expectThat(actualValue).isInstanceOf("SqlBuilder").assert();
+            pTester.expectThat(actualQueryResult).equals(expectQueryResult).assert();
+        }
+    ),
+]);
+
+
+var tester = new Tester("Test KeywordAttribute_lib");
+tester.initCoverage(KeywordAttribute);
+tester.test(constructor);
+tester.test(getValue);
+tester.test(getSqlBuilderSelect);
+
+tester.summary();
+    
+result.object(tester.getResults());
diff --git a/process/Keyword_lib/process.js b/process/Keyword_lib/process.js
index ea1736e5c13412a67527ebff0a4ab5d4bdae0293..994fd55c07d07fda603f24db728838663dd844f2 100644
--- a/process/Keyword_lib/process.js
+++ b/process/Keyword_lib/process.js
@@ -107,6 +107,7 @@ KeywordUtils.getContainerNames = function()
     //do not cache this list since
     // a) the list can easly change when a new container is created
     // b) where this is called it's not relevant in terms of performance
+    //!SqlBuilder
     var list = db.array(db.COLUMN, "select distinct AB_KEYWORD_ENTRY.CONTAINER from AB_KEYWORD_ENTRY order by AB_KEYWORD_ENTRY.CONTAINER asc");
     return list;
 };
diff --git a/process/Keyword_test/Keyword_test.aod b/process/Keyword_test/Keyword_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..e654f56663b71ec5ed3e6241aa1be9bd76dc1d87
--- /dev/null
+++ b/process/Keyword_test/Keyword_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>Keyword_test</name>
+  <title>[TEST] Keyword_lib</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/Keyword_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/Keyword_test/process.js b/process/Keyword_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1b1d910e9dfd985b8c089830f410b21f0151c25
--- /dev/null
+++ b/process/Keyword_test/process.js
@@ -0,0 +1,143 @@
+import("system.result");
+import("system.translate");
+import("system.vars");
+import("Keyword_lib");
+import("UnitTest_lib");
+
+//this test will not work currently 
+//TODO: renable and fix the tests
+
+//var getContainerNames = new TestSuite("KeywordUtils.getContainerNames", [
+//    new Test("should return an alphabetically ascending ordered list of all keyword containers",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getContainerNames();
+//
+//            pTester.expectThat(actualValue).isArray().assert();
+//            pTester.expectThat(actualValue).hasMinLength(1).assert();
+//            pTester.expectThat(actualValue).elementAt(0).equals("ActivityCategory").assert();
+//            pTester.expectThat(actualValue).elementAt(-1).equals("YesNo").assert();
+//        }
+//    )
+//]);
+//
+//
+//var getCategoryNameById = new TestSuite("KeywordUtils.getCategoryNameById", [
+//    new Test("should return existing keyword category name for correct uuid",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getCategoryNameById("a55654b1-6a19-4d0c-b08d-cfbc12b5f7b0");
+//
+//            pTester.expectThat(actualValue).equals("MemberRole").assert();
+//        }
+//    ),
+//
+//    new Test("should return non-existing keyword category name for wrong uuid",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getCategoryNameById("a55654b1-6a19-4d0c-b08d-cfbc12b5f7b1");
+//
+//            pTester.expectThat(actualValue).equals("<unknown>").assert();
+//        }
+//    ),
+//
+//    new Test("should return non-existing keyword category name for missing uuid",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getCategoryNameById();
+//
+//            pTester.expectThat(actualValue).equals("<unknown>").assert();
+//        }
+//    ),
+//]);
+//
+//
+//var getCategoryIdByName = new TestSuite("KeywordUtils.getCategoryIdByName", [
+//    new Test("should return existing keyword uuid for correct category name",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getCategoryIdByName("MemberRole");
+//
+//            pTester.expectThat(actualValue).equals("a55654b1-6a19-4d0c-b08d-cfbc12b5f7b0").assert();
+//        }
+//    ),
+//
+//    new Test("should throw exception for wrong keyword category name",
+//        function(pTester) {
+//            var exception = new Error(translate.withArguments("no keyword category \"%0\" found", ["AddressTypes"]));
+//            pTester.expectThat(function() {
+//                KeywordUtils.getCategoryIdByName("AddressTypes");
+//            }).throwsException(exception).assert();
+//        }
+//    ),
+//
+//    new Test("should throw exception for missing keyword category name",
+//        function(pTester) {
+//            var exception = new Error(translate.withArguments("no keyword category \"%0\" found", [""]));
+//            pTester.expectThat(function() {
+//                KeywordUtils.getCategoryIdByName();
+//            }).throwsException(exception).assert();
+//        }
+//    ),
+//]);
+//
+//
+//var getEntryNamesAndIdsByContainer = new TestSuite("KeywordUtils.getEntryNamesAndIdsByContainer", [
+//    new Test("should return an array of all keywords and their ID's for given container",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getEntryNamesAndIdsByContainer("ActivityDirection");
+//
+//            pTester.expectThat(actualValue).isArray().assert();
+//            pTester.expectThat(actualValue).hasMinLength(1, {name: "array"}).assert();
+//            pTester.expectThat(actualValue).elementAt(0).isArray().assert();
+//            pTester.expectThat(actualValue).elementAt(0).hasLength(2).assert();
+//        }
+//    ),
+//
+//    new Test("should return an empty array for non-existent container",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.getEntryNamesAndIdsByContainer("Asdf");
+//
+//            pTester.expectThat(actualValue).isArray().assert();
+//            pTester.expectThat(actualValue).hasLength(0, {name: "array"}).assert();
+//        }
+//    ),
+//]);
+//
+//
+//var exists = new TestSuite("KeywordUtils.exists", [
+//    new Test("should return a boolean indicating that a known keyword in a known container exists",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.exists("VISIT", "ActivityCategory");
+//
+//            pTester.expectThat(actualValue).isBoolean().assert();
+//            pTester.expectThat(actualValue).equals(true, "known keyword exists in known container").assert();
+//        }
+//    ),
+//
+//    new Test("should return a boolean indicating that an unknown keyword in a known container does not exist",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.exists("ASDF", "ActivityCategory");
+//
+//            pTester.expectThat(actualValue).isBoolean().assert();
+//            pTester.expectThat(actualValue).equals(false, "unknown keyword does not exist in known container").assert();
+//        }
+//    ),
+//
+//    new Test("should return a boolean indicating that a known keyword in an unknown container does not exist",
+//        function(pTester) {
+//            var actualValue = KeywordUtils.exists("VISIT", "Asdf");
+//
+//            pTester.expectThat(actualValue).isBoolean().assert();
+//            pTester.expectThat(actualValue).equals(false, "known keyword does not exist in unknown container").assert();
+//        }
+//    ),
+//]);
+//
+//
+//var tester = new Tester("Test Keyword_lib");
+//tester.initCoverage(KeywordUtils);
+//tester.test(getContainerNames);
+//tester.test(getCategoryNameById);
+//tester.test(getCategoryIdByName);
+//tester.test(getEntryNamesAndIdsByContainer);
+//tester.test(exists);
+//
+//tester.summary();
+//    
+//result.object(tester.getResults());
\ No newline at end of file
diff --git a/process/Leadimport_lib/process.js b/process/Leadimport_lib/process.js
index ea0f5c14c0b084b0ecca76fceee7841a1b247a04..de9d13b2c7bb525960c5503b3e20eb28d9f2190d 100644
--- a/process/Leadimport_lib/process.js
+++ b/process/Leadimport_lib/process.js
@@ -794,7 +794,7 @@ LeadImportUtils.getLeadAttr = function(pImportDefID)
                     .from("AB_ATTRIBUTERELATION")
                     .join("AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID")
                     .where("AB_ATTRIBUTERELATION.OBJECT_ROWID", pImportDefID)
-                    .and("AB_ATTRIBUTERELATION.OBJECT_TYPE = 'Leadimport'")
+                    .and("AB_ATTRIBUTERELATION.OBJECT_TYPE", "Leadimport")
                     .table()
     };
 }
diff --git a/process/ObjectRelation_lib/process.js b/process/ObjectRelation_lib/process.js
index 93063469bb275bf252eac8485933805cea091570..c99e2322413f0dda7eb287c8dfd033f048066895 100644
--- a/process/ObjectRelation_lib/process.js
+++ b/process/ObjectRelation_lib/process.js
@@ -37,6 +37,7 @@ ObjectRelationUtils.getPossibleRelationTypes = function(pObjectTypes, pFullInfo,
     // only id and title:
     if (pFullInfo)
     {
+        //!SqlBuilder
         fields = fields.concat([
             "main.RELATION_TYPE",
             "case when type2.AB_OBJECTRELATIONTYPEID is null then 'same' \n\
diff --git a/process/Offer_lib/process.js b/process/Offer_lib/process.js
index 7c9504fa26da75fa05ae7753f287070f922dd250..f5a8cd0138d503694a5e0e0a52b21bb277eb156c 100644
--- a/process/Offer_lib/process.js
+++ b/process/Offer_lib/process.js
@@ -502,7 +502,7 @@ OfferItemUtils.prototype.insertPartsList = function(pProductId, pAssignedTo, pCu
                             "( " + newSelect("DESCRIPTION")
                                 .from("DESCRIPTIONTRANSLATION")
                                 .where("DESCRIPTIONTRANSLATION.OBJECT_ROWID = PRODUCT.PRODUCTID")
-                                .and("DESCRIPTIONTRANSLATION.OBJECT_TYPE = 'Product'")
+                                .and("DESCRIPTIONTRANSLATION.OBJECT_TYPE", "Product")
                                 .and("DESCRIPTIONTRANSLATION.LANG", pLanguage)
                                 .toString() + ")"]]]]);
                                   
diff --git a/process/Order_lib/process.js b/process/Order_lib/process.js
index d6938458c59687ef679f1d437661ec456e01f2e8..bcb1b8428693ab99e51713ecb61a1b44fc6a4857 100644
--- a/process/Order_lib/process.js
+++ b/process/Order_lib/process.js
@@ -545,7 +545,8 @@ OrderUtils.buildReminderReport = function (pOrderID)
         "Ordernumber": translate.text("Order number",language),
         "Orderdate": translate.text("Order date",language),
         "Orderamount": translate.text("Order amount",language),
-        "Dunninglevel": translate.text("Dunning level",language)  
+        "Dunninglevel": translate.text("Dunning level",language),
+        "OutstandingAmount": translate.text("Outstanding Amount",language)
     };
     
     
diff --git a/process/SendEmail_workflowService/process.js b/process/SendEmail_workflowService/process.js
index 6ccbfe3e76006ca51544b503453320d416d35951..14247ad4e4b3dda3ab4be65989fa896e1434a894 100644
--- a/process/SendEmail_workflowService/process.js
+++ b/process/SendEmail_workflowService/process.js
@@ -10,19 +10,24 @@ import("Workflow_lib");
 
 var processInstanceId = vars.get("$local.uid");
 var variables = JSON.parse(vars.get("$local.value"));
-var recipientContactId = variables.recipientContactId;
+var recipientContactId = variables.recipientContactId || variables.targetId;
 var documentTemplateId = variables.documentTemplateId;
 var senderName = variables.senderName;
 var mailSubject = variables.mailSubject;
+var aditoUrl = variables.originUrl;
 
 var actionParams = Utils.clone(variables);
 actionParams.processInstanceId = processInstanceId;
-var linkPlaceholder = new Placeholder("workflowActionLink", Placeholder.types.CALLBACKFUNCTION, function ()
+var additionalPlaceholders = [];
+if (aditoUrl)
 {
-    return WorkflowLinkActions.getActionLink("https://localhost:8443", actionParams.linkActionType, actionParams.redirectLink, actionParams);
-});
+    additionalPlaceholders.push(linkPlaceholder = new Placeholder("workflowActionLink", Placeholder.types.CALLBACKFUNCTION, function ()
+    {
+        return WorkflowLinkActions.getActionLink(aditoUrl, actionParams.linkActionType, actionParams.redirectLink, actionParams);
+    }));
+}
 
-var email = Email.fromTemplate(documentTemplateId, recipientContactId, null, [linkPlaceholder]);
+var email = Email.fromTemplate(documentTemplateId, recipientContactId, null, additionalPlaceholders);
 email.subject = mailSubject;
 email.toRecipients = [CommUtil.getStandardMail(recipientContactId)];
 
diff --git a/process/SqlBuilder_test/SqlBuilder_test.aod b/process/SqlBuilder_test/SqlBuilder_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..3992282911646839e2344fc4154d6a0a30666daf
--- /dev/null
+++ b/process/SqlBuilder_test/SqlBuilder_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>SqlBuilder_test</name>
+  <title>[TEST] Sql_lib - SqlBuilder</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/SqlBuilder_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/SqlBuilder_test/process.js b/process/SqlBuilder_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ec9ec86ddb15cafc04a2bed81a81e28afc3cf79
--- /dev/null
+++ b/process/SqlBuilder_test/process.js
@@ -0,0 +1,1266 @@
+import("system.db");
+import("system.result");
+import("system.vars");
+import("system.translate");
+import("system.logging");
+import("system.SQLTYPES");
+import("Sql_lib");
+import("UnitTest_lib");
+
+//SqlBuilder-tests:
+var newSelectTests = new TestSuite("SqlLib.newSelect", [
+    new Test("newSelect with just a string should just use it as select",
+        function(pTester)
+        {
+            var actualValue = newSelect("MySuper, Field, String")
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+
+    new Test("newSelect with just an array of strings should just use them concatenated as select",
+        function(pTester)
+        {
+            var actualValue = newSelect(["MySuper", "Field", "String"])
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+
+    new Test("newSelect with just an SqlBuilder should use it as subselect",
+        function(pTester)
+        {
+            var actualValue = newSelect(new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz"))
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("newSelect an array of Strings and SqlBuilders should add them all",
+        function(pTester)
+        {
+            var actualValue = newSelect(["MySuper", "Field", "String", new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz")])
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select MySuper, Field, String, (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+]);
+
+
+var validAndUsageTests = new TestSuite("SqlLib.validAndUsage", [
+    new Test("and should just add simple strings as condition just as it is",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?"
+                                .and("PERSON.LASTNAME = 'Admin'")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' and PERSON.LASTNAME = 'Admin'").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+
+    new Test("and should add a condition if field and value are passed",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "Tim")
+                                .and("PERSON.LASTNAME", "Admin")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and should add a condition if value is an empty string",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "")
+                                .and("PERSON.LASTNAME", "")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and should add a condition if field and value as jdito-var are passed",
+        function(pTester)
+        {
+            vars.set("$global.TestUnitValueName", "Tim");
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "$global.TestUnitValueName")
+
+            pTester.expectThat(actualValue.toString()).equals("( PERSON.FIRSTNAME = 'Tim' ) ").assert();
+        }
+    ),
+
+    new Test("$ should be escaped by a second $ and the string should therefore just be used as string and not as jdito variable",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "$$mySuperString")
+
+            pTester.expectThat(actualValue.toString()).equals("( PERSON.FIRSTNAME = '$mySuperString' ) ").assert();
+        }
+    ),
+
+    new Test("and should add a condition if value is a jdito-var containing an empty string",
+        function(pTester)
+        {
+            vars.set("$global.TestingVarEmptyString", "");
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "$global.TestingVarEmptyString")
+                                .and("PERSON.LASTNAME", "$global.TestingVarEmptyString")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and should use the given condition pattern",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "Tim", "# <> ?")
+                                .and("PERSON.LASTNAME", "Admin")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME <> ? and PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and should use the given SQLTYPE if provided",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", 6, null, SQLTYPES.INTEGER)
+                                .and("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER)
+                                .and("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER)
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and PERSON.LASTNAME <> ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(0).elementAt(1).equals(SQLTYPES.INTEGER).assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(1).elementAt(1).equals(SQLTYPES.INTEGER).assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(2).elementAt(1).equals(SQLTYPES.INTEGER).assert();
+        }
+    ),
+
+    new Test("and only with a prepared statement-array should just use it as it is",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where([
+                                    "PERSON.FIRSTNAME = ?", [["Peter", 12]]
+                                ])
+                                .and([
+                                    "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", []
+                                ])
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? )  and  ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("and only with a SqlBulder object should just use the condition from it",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(new SqlBuilder()
+                                    .select("TEST")
+                                    .from("PERSON")
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .and("PERSON.LASTNAME", "Admin"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and with a builder as value and condition (field is null|undefined) should add the whole builder as subquery",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(null, new SqlBuilder()
+                                    .select("FIRSTNAME")
+                                    .from("PERSON")
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .and("PERSON.LASTNAME", "Admin"),
+                                    "exists ?")  // Note: you can use SqlBuilder.EXISTS() instead of  "exists ?"
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and with a builder as value and field should add the whole builder as subquery with field = (subquery)",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", new SqlBuilder()
+                                    .select("FIRSTNAME")
+                                    .from("PERSON")
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .and("PERSON.LASTNAME", "Admin"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and with a prepared statement-array as value and field is null|undefined should add the whole statement as subquery",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?")
+                                .and(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists  ( select FIRSTNAME from PERSON.FIRSTNAME = ? )  and  ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("and with a prepared statement-array as value and field should add the whole statement as subquery with field = (subquery)",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]])
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+]);
+
+
+var validOrUsageTests = new TestSuite("SqlLib.validOrUsage", [
+    new Test("or should just add simple strings as condition just as it is",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?"
+                                .or("PERSON.LASTNAME = 'Admin'")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = 'Tim' or PERSON.LASTNAME = 'Admin'").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+
+    new Test("or should add a condition if field and value are passed",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "Tim")
+                                .or("PERSON.LASTNAME", "Admin")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("or should use the given condition pattern",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", "Tim", "# <> ?")
+                                .or("PERSON.LASTNAME", "Admin")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME <> ? or PERSON.LASTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("or should use the given SQLTYPE if provided",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", 6, null, SQLTYPES.INTEGER)
+                                .or("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER)
+                                .or("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER)
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? or PERSON.LASTNAME <> ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert();
+        }
+    ),
+
+    new Test("or only with a prepared statement-array should just use it as it is",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where([
+                                    "PERSON.FIRSTNAME = ?", [["Peter", 12]]
+                                ])
+                                .or([
+                                    "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", []
+                                ])
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? )  or  ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("or only with a SqlBulder object should just use the condition from it",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .or("PERSON.LASTNAME", "Admin"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("or with a builder as value and condition (field is null|undefined) should add the whole builder as subquery",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(null, new SqlBuilder()
+                                    .select("FIRSTNAME")
+                                    .from("PERSON")
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .or("PERSON.LASTNAME", "Admin"),
+                                    "exists ?")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("or with a builder as value and field should add the whole builder as subquery with field = (subquery)",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.FIRSTNAME", new SqlBuilder()
+                                    .select("FIRSTNAME")
+                                    .from("PERSON")
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .or("PERSON.LASTNAME", "Admin"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("or with a prepared statement-array as value and field is null|undefined should add the whole statement as subquery",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?")
+                                .or(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("exists  ( select FIRSTNAME from PERSON.FIRSTNAME = ? )  or  ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+]);
+
+var combinedAndOrTests = new TestSuite("SqlLib.combinedAndOr", [
+    new Test("or combining two and",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .where("PERSON.FIRSTNAME", "Tim")
+                            .and("PERSON.LASTNAME", "Admin")
+                            .or(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Peter")
+                                    .and("PERSON.LASTNAME", "Müller"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("(PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?) or  ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert();
+        }
+    ),
+
+    new Test("and combining two or",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .where(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Tim")
+                                    .or("PERSON.LASTNAME", "Admin"))
+                            .and(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Peter")
+                                    .or("PERSON.LASTNAME", "Müller"))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert();
+        }
+    ),
+
+    new Test("some and/or combinations in one select",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .where("PERSON.FIRSTNAME", "Tim")
+                            .or("PERSON.FIRSTNAME", "Franz")
+                            .and("PERSON.LASTNAME", "Admin")
+                            .and(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Peter")
+                                    .or("PERSON.LASTNAME", "Müller"))
+                            .or("PERSON.FIRSTNAME", "Franz")
+                            .and("PERSON.FIRSTNAME", "Franz")
+                            .or(new SqlBuilder()
+                                    .where("PERSON.FIRSTNAME", "Peter")
+                                    .and("PERSON.LASTNAME", "Müller")
+                                    .and(new SqlBuilder()
+                                            .where("PERSON.FIRSTNAME", "Peter")
+                                            .or("PERSON.LASTNAME", "Müller")))
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME = ? or PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  or (PERSON.FIRSTNAME = ?) and PERSON.FIRSTNAME = ? or  ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(11).assert();
+        }
+    ),
+]);
+
+
+var ifSetTests = new TestSuite("SqlLib.ifSet", [
+    new Test("simple and if set with all types of empty values.",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .whereIfSet("PERSON.LASTNAME", null)
+                            .andIfSet("PERSON.LASTNAME", undefined)
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert();
+        }
+    ),
+
+    new Test("jdito variable with null",
+        function(pTester)
+        {
+            vars.set("$global.TestingVarNull", null);
+
+            var actualValue = new SqlBuilder()
+                            .whereIfSet("PERSON.FIRSTNAME", "$global.TestingVarNull")
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert();
+        }
+    ),
+
+    new Test("empty simple conditions",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .whereIfSet("")
+                            .andIfSet(["", []])
+                            .andIfSet(new SqlBuilder())
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert();
+        }
+    ),
+
+    new Test("empty subqueries",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .whereIfSet("PERSON.FIRSTNAME", ["", []])
+                            .andIfSet("PERSON.LASTNAME", new SqlBuilder())
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("", "no sql should be added").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0, "no params should be added").assert();
+        }
+    ),
+]);
+
+
+function cleanWrapperTests()
+{
+    try {
+        db.runStatement("drop table SQL_LIB_TEST_TABLE");
+    } catch(ex) {}
+
+    try {
+        db.deleteData("PERSON", "PERSONID in ('TEST-5', 'TEST-6')")
+    } catch(ex) {}
+
+}
+
+
+var dbWrapperTests = new TestSuite("SqlLib.dbWrapper", [
+    new Test("cell should load only one value",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME")
+                                .from("PERSON")
+                                .where("PERSON.PERSONID", "TEST-5")
+            var actualValue = builder.cell()
+
+            pTester.expectThat(actualValue).equals("Franz").assert();
+        }
+    ),
+
+    new Test("cell should just return '' if no condition set but pExecuteOnlyIfConditionExists is true",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME")
+                                .from("PERSON")
+            var actualValue = builder.cell(true)
+
+            pTester.expectThat(actualValue).equals("").assert();
+        }
+    ),
+
+    new Test("cell should return a value if no condition set and pExecuteOnlyIfConditionExists is false",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME")
+                                .from("PERSON");
+
+            var actualValue = builder.cell(false)
+
+            pTester.expectThat(actualValue).not().equals("").assert();
+            pTester.expectThat(actualValue).not().isNull().assert();
+            pTester.expectThat(actualValue).not().isUndefined().assert();
+        }
+    ),
+
+    new Test("array should load an array of values",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON")
+                                .where("PERSON.PERSONID", "TEST-5");
+
+            var actualValue = builder.array(db.ROW);
+            pTester.expectThat(actualValue).elementAt(0).equals("Franz", {name: "firstname"}).assert();
+            pTester.expectThat(actualValue).elementAt(1).equals("Müller", {name: "lastname"}).assert();
+        }
+    ),
+
+    new Test("array should return an empty array if no condition set but pExecuteOnlyIfConditionExists is true",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON")
+
+            var actualValue = builder.array(db.ROW, true);
+            pTester.expectThat(actualValue).hasLength(0).assert();
+        }
+    ),
+
+    new Test("array should return a non-empty array if no condition set and pExecuteOnlyIfConditionExists is false",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON")
+
+            var actualValue = builder.array(db.ROW, false);
+            pTester.expectThat(actualValue).hasMinLength(1).assert();
+        }
+    ),
+
+    new Test("table should load an array of arrays with values",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON")
+                                .where("PERSON.PERSONID", "TEST-5")
+                                .or("PERSON.PERSONID", "TEST-6")
+                                .orderBy("PERSONID asc");
+
+            var actualValue = builder.table();
+            pTester.expectThat(actualValue).elementAt(0).elementAt(0).equals("Franz", {name: "firstname"}).assert();
+            pTester.expectThat(actualValue).elementAt(0).elementAt(1).equals("Müller", {name: "lastname"}).assert();
+
+            pTester.expectThat(actualValue).elementAt(1).elementAt(0).equals("Marco", {name: "firstname"}).assert();
+            pTester.expectThat(actualValue).elementAt(1).elementAt(1).equals("Polo", {name: "lastname"}).assert();
+        }
+    ),
+
+    new Test("table should return an empty array if no condition set but pExecuteOnlyIfConditionExists is true",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON");
+
+            var actualValue = builder.table(true);
+
+            pTester.expectThat(actualValue).hasLength(0).assert();
+        }
+    ),
+
+    new Test("table should return a non-empty array if no condition set and pExecuteOnlyIfConditionExists is false",
+        function(pTester)
+        {
+            var builder = new SqlBuilder()
+                                .select("FIRSTNAME, LASTNAME")
+                                .from("PERSON");
+
+            var actualValue = builder.table(false);
+            pTester.expectThat(actualValue).hasMinLength(1).assert();
+        }
+    ),
+
+    new Test("delete should delete the data and use the from and condition from the builder",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+                                .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
+
+            var actualValue = builder.deleteData();
+            pTester.expectThat(actualValue).equals(1).assert();
+        }
+    ),
+
+    new Test("delete should delete the data from the provided table and use the condition from the builder. It ignores .from if a table is given.",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("PERSON")
+                                .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
+
+            var actualValue = builder.deleteData(false, "SQL_LIB_TEST_TABLE");
+            pTester.expectThat(actualValue).equals(1).assert();
+        }
+    ),
+
+    new Test("delete should delete ALL data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is false",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+
+            var actualValue = builder.deleteData(false);
+            pTester.expectThat(actualValue).equals(2).assert();
+        }
+    ),
+
+    new Test("delete should delete NO data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is true",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+
+            var actualValue = builder.deleteData(true);
+            pTester.expectThat(actualValue).equals(0).assert();
+        }
+    ),
+
+    new Test("update should update the data and use the from and condition from the builder",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+                                .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
+
+            builder.updateData(false, undefined, ["FIRSTNAME"], null, ["Fritz"]);
+
+            var actualValue = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
+
+            pTester.expectThat(actualValue).equals("Fritz").assert();
+        }
+    ),
+
+    new Test("update should update the data from the provided table and use the condition from the builder. It ignores .from if a table is given.",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("PERSON")
+                                .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
+
+            builder.updateData(false, "SQL_LIB_TEST_TABLE", ["FIRSTNAME"], null, ["Fritz"]);
+
+            var actualValue = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
+
+            pTester.expectThat(actualValue).equals("Fritz").assert();
+        }
+    ),
+
+    new Test("update should update ALL data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is false",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Franz", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+
+            builder.updateData(false, undefined, ["FIRSTNAME"], null, ["Fritz"]);
+
+            var actualValue1 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
+            var actualValue2 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-8'")
+
+            pTester.expectThat(actualValue1).equals("Fritz", "TEST-7 should have Firstname Fritz").assert();
+            pTester.expectThat(actualValue2).equals("Fritz", "TEST-8 should have Firstname Fritz").assert();
+        }
+    ),
+
+    new Test("update should update NO data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is true",
+        function(pTester)
+        {
+            db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')")
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
+            db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Franz", "Fischer"]);
+
+            var builder = new SqlBuilder()
+                                .from("SQL_LIB_TEST_TABLE")
+
+            builder.updateData(true, undefined, ["FIRSTNAME"], null, ["Fritz"]);
+
+            var actualValue1 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
+            var actualValue2 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-8'")
+
+            pTester.expectThat(actualValue1).equals("Ludwig", "TEST-7 should have Firstname Ludwig").assert();
+            pTester.expectThat(actualValue2).equals("Franz", "TEST-8 should have Firstname Franz").assert();
+        }
+    ),
+], function preAll()
+{
+    // remove data which may exist if previous test-run failed and postAll was not executed
+    cleanWrapperTests();
+
+    // add table for save testing of deletes
+    db.runStatement("create table SQL_LIB_TEST_TABLE (TESTID varchar(36), FIRSTNAME varchar(100), LASTNAME varchar(100))");
+
+    // add test persons
+    db.insertData("PERSON", ["PERSONID", "FIRSTNAME", "LASTNAME", "USER_NEW", "DATE_NEW"], null, ["TEST-5", "Franz", "Müller", "testuser", vars.get("$sys.date")])
+    db.insertData("PERSON", ["PERSONID", "FIRSTNAME", "LASTNAME", "USER_NEW", "DATE_NEW"], null, ["TEST-6", "Marco", "Polo", "testuser", vars.get("$sys.date")])
+}, undefined, undefined, function postAll()
+{
+    cleanWrapperTests();
+})
+
+
+var mandatoryErrorTests = new TestSuite("SqlLib.mandatoryError", [
+// and
+    new Test("and without parameter should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or();
+            }).throwsException(SqlBuilder._ERROR_NO_PARAMETER_PROVIDED()).assert();
+        }
+    ),
+
+    new Test("and with null as value should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", null);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("and with undefined as value should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", undefined);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("and with a jdito-var containing null should error",
+        function(pTester)
+        {
+            vars.set("$global.TestingVarNull", null);
+
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", "$global.TestingVarNull");
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR()).assert();
+        }
+    ),
+
+    new Test("and with an empty sql-builder as subquery should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", new SqlBuilder());
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("and with an empty prepared statement as subquery should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", ["", []]);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+
+// or
+    new Test("or without parameter should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or();
+            }).throwsException(SqlBuilder._ERROR_NO_PARAMETER_PROVIDED()).assert();
+        }
+    ),
+
+    new Test("or with null as value should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", null);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("or with undefined as value should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", undefined);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("or with a jdito-var containing null should error",
+        function(pTester)
+        {
+            vars.set("$global.TestingVarNull", null);
+
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", "$global.TestingVarNull");
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR()).assert();
+        }
+    ),
+
+    new Test("or with an empty sql-builder as subquery should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", new SqlBuilder());
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+
+    new Test("or with an empty prepared statement as subquery should error",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where().or("PERSON.FIRSTNAME", ["", []]);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+]);
+
+
+var inStatementTests = new TestSuite("SqlLib.inStatement", [
+    new Test("simple and in",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.LASTNAME", ["Franz", "Fritz"], SqlBuilder.IN()) // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter
+                                .or("PERSON.LASTNAME", ["Peter", "Mayer"], SqlBuilder.IN());
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.LASTNAME in  (?, ?)  )  or  ( PERSON.LASTNAME in  (?, ?)  ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(4).assert();
+        }
+    ),
+
+    new Test("simple and not in",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .where("PERSON.LASTNAME", ["Franz", "Fritz"], "# not in ?"); // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( PERSON.LASTNAME not in  (?, ?)  ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(2).assert();
+        }
+    ),
+
+    new Test("in with subquery",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .where("PERSON.FIRSTNAME", new SqlBuilder()
+                                                            .select("PERSON.FIRSTNAME")
+                                                            .from("PERSON")
+                                                            .where("PERSON.LASTNAME", "Fritz")
+                                                    , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?"
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME in  ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("in with prepared statement-array",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                            .where("PERSON.FIRSTNAME", ["select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ?", [["Fritz", SQLTYPES.VARCHAR]]]
+                                                    , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?"
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("PERSON.FIRSTNAME in  ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("andIfSet should ignore empty array",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                                .whereIfSet("PERSON.LASTNAME", []);
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals("").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+
+    new Test("and should error on an empty array",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.LASTNAME", []);
+            }).throwsException(SqlBuilder._ERROR_VALUE_IS_MANDATORY()).assert();
+        }
+    ),
+]);
+
+
+var testConstantFunctions = new TestSuite("SqlLib.testConstantFunc", [
+    new Test("SqlBuilder.IN()",
+        function(pTester)
+        {
+            var actualValue = SqlBuilder.IN();
+            pTester.expectThat(actualValue).equals("# in ?").assert();
+        }
+    ),
+
+    new Test("SqlBuilder.NOT_IN()",
+        function(pTester)
+        {
+            var actualValue = SqlBuilder.NOT_IN();
+            pTester.expectThat(actualValue).equals("# not in ?").assert();
+        }
+    ),
+
+    new Test("SqlBuilder.EXISTS()",
+        function(pTester)
+        {
+            var actualValue = SqlBuilder.EXISTS();
+            pTester.expectThat(actualValue).equals("exists ?").assert();
+        }
+    ),
+]);
+
+
+var selectTests = new TestSuite("SqlLib.select", [
+    new Test("a sql-builder in a fields-array is translated to sql correctly",
+        function(pTester)
+        {
+            var countSubQuery = newSelect("count(*)")
+                                    .from("AB_ATTRIBUTEUSAGE")
+                                    .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", "myType")
+                                    .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
+
+            var actualValue = new SqlBuilder()
+                .select(["AB_ATTRIBUTEID", "AB_ATTRIBUTEUSAGEID", countSubQuery])
+                .from("AB_ATTRIBUTE")
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, (select count(*) from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.OBJECT_TYPE = ? and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID)").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("a sql-builder in from is used as subselect",
+        function(pTester)
+        {
+            var subQuery = newSelect("FIRSTNAME")
+                                    .from("PERSON")
+                                    .where("PERSON.LASTNAME", "Meier")
+
+            var actualValue = new SqlBuilder()
+                .select("*")
+                .from(subQuery)
+
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select FIRSTNAME from PERSON where PERSON.LASTNAME = ?)").assert();
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+]);
+
+
+var joinTests = new TestSuite("SqlLib.join", [
+    new Test("SqlBuilder as on-condition should only add the conditon of the builder",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+
+            var actualValue = new SqlBuilder()
+                .select("*")
+                .from("PERSON")
+                .join("ORGANISATION", subQuery)
+
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join ORGANISATION on ORGANISATION.NAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("SqlBuilder as table for join is added as subselect",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+
+            var actualValue = new SqlBuilder()
+                .select("*")
+                .from("PERSON")
+                .join(subQuery, "orgname.NAME = TABLE2.NAME", "orgname")
+
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join (select NAME from ORGANISATION where ORGANISATION.NAME = ?) orgname on orgname.NAME = TABLE2.NAME").assert();
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("just use a string also containing a condition as join without additional condition",
+        function(pTester)
+        {
+            var actualValue = new SqlBuilder()
+                .select("*")
+                .from("PERSON")
+                .join("TABLE1 on TABLE1.NAME = TABLE2.NAME")
+
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join TABLE1 on TABLE1.NAME = TABLE2.NAME").assert();
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(0).assert();
+        }
+    ),
+]);
+
+
+var subqueryAsFieldTests = new TestSuite("SqlLib.subqueryAsField", [
+    new Test("Test if a Subselect as field works if pValue is provided. This should be added as subselect.",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
+                                    .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
+
+            var actualValue = new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR)
+                                         .and("PERSON.FIRSTNAME", "val3");
+
+
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("_sqlStorage").equals(" ( ( select NAME from ORGANISATION where ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and PERSON.FIRSTNAME = ? ) = ? )  and PERSON.FIRSTNAME = ?").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").hasLength(3).assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(0).elementAt(0).equals("val1").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(1).elementAt(0).equals("val2").assert();
+            pTester.expectThat(actualValue).elementAt("_where").elementAt("preparedValues").elementAt(2).elementAt(0).equals("val3").assert();
+        }
+    ),
+
+    new Test("Test if a Subselect as field should error if no SQLTYPE is provided.",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                var subQuery = newSelect("NAME")
+                                .from("ORGANISATION")
+                                .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
+                                .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
+                new SqlBuilder().where(subQuery, "val2", "# = ?");
+            }).throwsException(SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE()).assert();
+        }
+    ),
+
+    new Test("Test if a Subselect as field should error if it is not a full select.",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                var subQuery = newSelect("NAME")
+                                .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
+                                .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
+
+                new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR);
+            }).throwsException(SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE()).assert();
+        }
+    ),
+]);
+
+
+var conditionFormatTests = new TestSuite("SqlLib.conditionFormat", [
+    new Test("pCondition should not fail if # an ? exist in correct order",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder()
+                    .where("PERSON.FIRSTNAME", "val1", "# = ?")
+                    .and("PERSON.FIRSTNAME", "val1", "asdf # fdsa=asdf ?fdas")
+                    ;
+            }).not().throwsException().assert();
+        }
+    ),
+
+    new Test("pCondition should not fail if # an ? exist in correct order and there are additional, escaped # and ?",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "\\? # \\#= ?");
+            }).not().throwsException().assert();
+        }
+    ),
+
+    new Test("pCondition should not fail if only ? exists",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "?")
+            }).not().throwsException().assert();
+        }
+    ),
+
+    new Test("pCondition should fail if more than one ? exists",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? test ?")
+            }).throwsException(SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()).assert();
+        }
+    ),
+
+    new Test("pCondition should fail if more than one # exists",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "# test #")
+            }).throwsException(SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()).assert();
+        }
+    ),
+
+    new Test("pCondition should fail if # and ? are in wrong order",
+        function(pTester)
+        {
+            pTester.expectThat(function() {
+                new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? = #")
+            }).throwsException(SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()).assert();
+        }
+    ),
+]);
+
+
+var subqueryAliasTests = new TestSuite("SqlLib.subqueryAlias", [
+    new Test("subselectAlias should be added for subquery in .select",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+                                    .subselectAlias("testAlias")
+
+            var actualValue = new SqlBuilder()
+                .select([subQuery, "FIRSTNAME"])
+                .from("PERSON")
+
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("_sqlStorage").equals("select (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias, FIRSTNAME").assert();
+            pTester.expectThat(actualValue).elementAt("_select").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("subselectAlias should be added for subquery in .from",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+                                    .subselectAlias("testAlias")
+
+            var actualValue = new SqlBuilder()
+                .from(subQuery)
+
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias").assert();
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("subselectAlias should be overruled by the param in in .from",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+                                    .subselectAlias("testAlias")
+
+            var actualValue = new SqlBuilder()
+                .from(subQuery, "overwriteAlias")
+
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("_sqlStorage").equals("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias").assert();
+            pTester.expectThat(actualValue).elementAt("_from").elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+
+    new Test("subselectAlias should be added for subquery in .join",
+        function(pTester)
+        {
+            var subQuery = newSelect("NAME, ORGANISATIONID")
+                                    .from("ORGANISATION")
+                                    .where("ORGANISATION.NAME", "Adito")
+                                    .subselectAlias("testAlias")
+
+            var actualValue = new SqlBuilder()
+                .from("CONTACT")
+                .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID")
+                .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID", "overwriteAlias")
+
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("_sqlStorage").equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) testAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert();
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(0).elementAt("preparedValues").hasLength(1).assert();
+
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(1).elementAt("_sqlStorage").equals("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias on testAlias.ORGANISATIONID = ORGANISATION_ID").assert();
+            pTester.expectThat(actualValue).elementAt("_joins").elementAt(1).elementAt("preparedValues").hasLength(1).assert();
+        }
+    ),
+]);
+
+
+var tester = new Tester("Test SqlBuilder");
+tester.test(newSelectTests);
+tester.test(validAndUsageTests);
+tester.test(validOrUsageTests);
+tester.test(combinedAndOrTests);
+tester.test(ifSetTests);
+tester.test(dbWrapperTests);
+tester.test(mandatoryErrorTests);
+tester.test(inStatementTests);
+tester.test(testConstantFunctions);
+tester.test(selectTests);
+tester.test(joinTests);
+tester.test(subqueryAsFieldTests);
+tester.test(conditionFormatTests);
+tester.test(subqueryAliasTests);
+
+tester.summary();
+
+result.object(tester.getResults());
\ No newline at end of file
diff --git a/process/SqlLib_tests/process.js b/process/SqlLib_tests/process.js
deleted file mode 100644
index 924162e2a4ef98f5dc93515f648fdad355ced736..0000000000000000000000000000000000000000
--- a/process/SqlLib_tests/process.js
+++ /dev/null
@@ -1,1015 +0,0 @@
-import("system.db");
-import("system.vars");
-import("system.translate");
-import("system.logging");
-import("system.SQLTYPES");
-import("Sql_lib");
-import("UnitTest_lib");
-
-var newSelectTests = new TestSuite([
-    ["newSelect with just a string schould just use it as select", function(pTester)
-    {
-        var actual = newSelect("MySuper, Field, String")
-
-        pTester.assert("select MySuper, Field, String", actual._select._sqlStorage, "prepared sql");
-        pTester.assert(0, actual._select.preparedValues.length, "number of params");
-    }],
-
-    ["newSelect with just an array of strings schould just use them concatenated as select", function(pTester)
-    {
-        var actual = newSelect(["MySuper", "Field", "String"])
-
-        pTester.assert("select MySuper, Field, String", actual._select._sqlStorage, "prepared sql");
-        pTester.assert(0, actual._select.preparedValues.length, "number of params");
-    }],
-
-    ["newSelect with just an SqlBuilder should use it as subselect", function(pTester)
-    {
-        var actual = newSelect(new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz"))
-
-        pTester.assert("select (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)", actual._select._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._select.preparedValues.length, "number of params");
-    }],
-
-    ["newSelect an array of Strings and SqlBuilders should add them all", function(pTester)
-    {
-        var actual = newSelect(["MySuper", "Field", "String", new SqlBuilder().select("PERSONID").from("PERSON").where("PERSON.FIRSTNAME", "Fritz")])
-
-        pTester.assert("select MySuper, Field, String, (select PERSONID from PERSON where PERSON.FIRSTNAME = ?)", actual._select._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._select.preparedValues.length, "number of params");
-    }],
-]);
-
-var validAndUsageTests = new TestSuite([
-    ["and should just add simple strings as condition just as it is", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?"
-                            .and("PERSON.LASTNAME = 'Admin'")
-
-        pTester.assert("PERSON.FIRSTNAME = 'Tim' and PERSON.LASTNAME = 'Admin'", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(0, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and should add a condition if field and value are passed", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "Tim")
-                            .and("PERSON.LASTNAME", "Admin")
-                            
-        pTester.assert("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and should add a condition if value is an empty string", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "")
-                            .and("PERSON.LASTNAME", "")
-                            
-        pTester.assert("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and should add a condition if field and value as jdito-var are passed", function(pTester)
-    {
-        vars.set("$global.TestUnitValueName", "Tim");
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "$global.TestUnitValueName")
-                            
-        pTester.assert("( PERSON.FIRSTNAME = 'Tim' ) ", actual.toString());
-    }],
-
-    ["$ should be escaped by a second $ and the string should therefore just be used as string and not as jdito variable", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "$$mySuperString")
-                            
-        pTester.assert("( PERSON.FIRSTNAME = '$mySuperString' ) ", actual.toString());
-    }],
-
-    ["and should add a condition if value is a jdito-var containing an empty string", function(pTester)
-    {
-        vars.set("$global.TestingVarEmptyString", "");
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "$global.TestingVarEmptyString")
-                            .and("PERSON.LASTNAME", "$global.TestingVarEmptyString")
-                            
-        pTester.assert("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and should use the given condition pattern", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "Tim", "# <> ?")
-                            .and("PERSON.LASTNAME", "Admin")
-                            
-        pTester.assert("PERSON.FIRSTNAME <> ? and PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and should use the given SQLTYPE if provided", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", 6, null, SQLTYPES.INTEGER)
-                            .and("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER)
-                            .and("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER)
-        
-        pTester.assert("PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and PERSON.LASTNAME <> ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(3, actual._where.preparedValues.length, "number of params");
-        pTester.assert(SQLTYPES.INTEGER, actual._where.preparedValues[0][1], "sql type of param 1 is the provided type");
-        pTester.assert(SQLTYPES.INTEGER, actual._where.preparedValues[1][1], "sql type of param 2 is the provided type");
-        pTester.assert(SQLTYPES.INTEGER, actual._where.preparedValues[2][1], "sql type of param 3 is the provided type");
-    }],
-
-    ["and only with a prepared statement-array should just use it as it is", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where([
-                                "PERSON.FIRSTNAME = ?", [["Peter", 12]]
-                            ])
-                            .and([
-                                "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", []
-                            ])
-                            
-        pTester.assert(" ( PERSON.FIRSTNAME = ? )  and  ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and only with a SqlBulder object should just use the condition from it", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(new SqlBuilder()
-                                .select("TEST")
-                                .from("PERSON")
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .and("PERSON.LASTNAME", "Admin"))
-                            
-        pTester.assert(" ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and with a builder as value and condition (field is null|undefined) should add the whole builder as subquery", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(null, new SqlBuilder()
-                                .select("FIRSTNAME")
-                                .from("PERSON")
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .and("PERSON.LASTNAME", "Admin"),
-                                "exists ?")  // Note: you can use SqlBuilder.EXISTS() instead of  "exists ?"
-        
-        pTester.assert("exists  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and with a builder as value and field should add the whole builder as subquery with field = (subquery)", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", new SqlBuilder()
-                                .select("FIRSTNAME")
-                                .from("PERSON")
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .and("PERSON.LASTNAME", "Admin"))
-        
-        pTester.assert("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and with a prepared statement-array as value and field is null|undefined should add the whole statement as subquery", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?")
-                            .and(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement
-        
-        pTester.assert("exists  ( select FIRSTNAME from PERSON.FIRSTNAME = ? )  and  ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["and with a prepared statement-array as value and field should add the whole statement as subquery with field = (subquery)", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]])
-        
-        pTester.assert("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON.FIRSTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._where.preparedValues.length, "number of params");
-    }]
-]);
-
-var validOrUsageTests = new TestSuite([
-    ["or should just add simple strings as condition just as it is", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME = 'Tim'") // NOTE: you should not do this as this does not add a real prepared statement with "?"
-                            .or("PERSON.LASTNAME = 'Admin'")
-
-        pTester.assert("PERSON.FIRSTNAME = 'Tim' or PERSON.LASTNAME = 'Admin'", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(0, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or should add a condition if field and value are passed", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "Tim")
-                            .or("PERSON.LASTNAME", "Admin")
-                            
-        pTester.assert("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or should use the given condition pattern", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", "Tim", "# <> ?")
-                            .or("PERSON.LASTNAME", "Admin")
-                            
-        pTester.assert("PERSON.FIRSTNAME <> ? or PERSON.LASTNAME = ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or should use the given SQLTYPE if provided", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", 6, null, SQLTYPES.INTEGER)
-                            .or("PERSON.LASTNAME", 7, undefined, SQLTYPES.INTEGER)
-                            .or("PERSON.LASTNAME", 8, "# <> ?", SQLTYPES.INTEGER)
-                            
-        pTester.assert("PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? or PERSON.LASTNAME <> ?", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(3, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or only with a prepared statement-array should just use it as it is", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where([
-                                "PERSON.FIRSTNAME = ?", [["Peter", 12]]
-                            ])
-                            .or([
-                                "exists (select * FROM CONTACT where PERSON_ID = PERSONID)", []
-                            ])
-                            
-        pTester.assert(" ( PERSON.FIRSTNAME = ? )  or  ( exists (select * FROM CONTACT where PERSON_ID = PERSONID) ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or only with a SqlBulder object should just use the condition from it", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .or("PERSON.LASTNAME", "Admin"))
-                            
-        pTester.assert(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or with a builder as value and condition (field is null|undefined) should add the whole builder as subquery", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(null, new SqlBuilder()
-                                .select("FIRSTNAME")
-                                .from("PERSON")
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .or("PERSON.LASTNAME", "Admin"),
-                                "exists ?")
-        
-        pTester.assert("exists  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or with a builder as value and field should add the whole builder as subquery with field = (subquery)", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.FIRSTNAME", new SqlBuilder()
-                                .select("FIRSTNAME")
-                                .from("PERSON")
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .or("PERSON.LASTNAME", "Admin"))
-        
-        pTester.assert("PERSON.FIRSTNAME =  ( select FIRSTNAME from PERSON where PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["or with a prepared statement-array as value and field is null|undefined should add the whole statement as subquery", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where(null, ["select FIRSTNAME from PERSON.FIRSTNAME = ?", [["Peter", 12]]], "exists ?")
-                            .or(null, ["exists (select FIRSTNAME from PERSON.FIRSTNAME = ?)", [["Peter", 12]]]) // also without pCond it should work as the condition could be included in the prep statement
-        
-        pTester.assert("exists  ( select FIRSTNAME from PERSON.FIRSTNAME = ? )  or  ( exists (select FIRSTNAME from PERSON.FIRSTNAME = ?) ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }]
-]);
-
-var combinedAndOrTests = new TestSuite([
-    ["or combining two and", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .where("PERSON.FIRSTNAME", "Tim")
-                        .and("PERSON.LASTNAME", "Admin")
-                        .or(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Peter")
-                                .and("PERSON.LASTNAME", "Müller"))
-        
-        pTester.assert("(PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ?) or  ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(4, actual._where.preparedValues.length, "number of params");
-    }],
-    
-    ["and combining two or", function(pTester)
-    {                 
-        var actual = new SqlBuilder()
-                        .where(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Tim")
-                                .or("PERSON.LASTNAME", "Admin"))
-                        .and(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Peter")
-                                .or("PERSON.LASTNAME", "Müller"))
-        
-        pTester.assert(" ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(4, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["some and/or combinations in one select", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .where("PERSON.FIRSTNAME", "Tim")
-                        .or("PERSON.FIRSTNAME", "Franz")
-                        .and("PERSON.LASTNAME", "Admin")
-                        .and(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Peter")
-                                .or("PERSON.LASTNAME", "Müller"))
-                        .or("PERSON.FIRSTNAME", "Franz")
-                        .and("PERSON.FIRSTNAME", "Franz")
-                        .or(new SqlBuilder()
-                                .where("PERSON.FIRSTNAME", "Peter")
-                                .and("PERSON.LASTNAME", "Müller")
-                                .and(new SqlBuilder()
-                                        .where("PERSON.FIRSTNAME", "Peter")
-                                        .or("PERSON.LASTNAME", "Müller")))
-                        
-        pTester.assert("PERSON.FIRSTNAME = ? or PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  or (PERSON.FIRSTNAME = ?) and PERSON.FIRSTNAME = ? or  ( PERSON.FIRSTNAME = ? and PERSON.LASTNAME = ? and  ( PERSON.FIRSTNAME = ? or PERSON.LASTNAME = ? )  ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(11, actual._where.preparedValues.length, "number of params");
-    }]
-]);
-
-var ifSetTests = new TestSuite([
-    ["simple and if set with all types of empty values.", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .whereIfSet("PERSON.LASTNAME", null)
-                        .andIfSet("PERSON.LASTNAME", undefined)
-                        
-        pTester.assert("", actual._where._sqlStorage, "no sql should be added");
-        pTester.assert(0, actual._where.preparedValues.length, "no params should be added");
-    }],
-
-    ["jdito variable with null", function(pTester)
-    {
-        vars.set("$global.TestingVarNull", null);
-        
-        var actual = new SqlBuilder()
-                        .whereIfSet("PERSON.FIRSTNAME", "$global.TestingVarNull")
-                        
-        pTester.assert("", actual._where._sqlStorage, "no sql should be added");
-        pTester.assert(0, actual._where.preparedValues.length, "no params should be added");
-    }],
-
-    ["empty simple conditions", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .whereIfSet("")
-                        .andIfSet(["", []])
-                        .andIfSet(new SqlBuilder())
-                        
-        pTester.assert("", actual._where._sqlStorage, "no sql should be added");
-        pTester.assert(0, actual._where.preparedValues.length, "no params should be added");
-    }],
-
-    ["empty subqueries", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .whereIfSet("PERSON.FIRSTNAME", ["", []])
-                        .andIfSet("PERSON.LASTNAME", new SqlBuilder())
-                        
-        pTester.assert("", actual._where._sqlStorage, "no sql should be added");
-        pTester.assert(0, actual._where.preparedValues.length, "no params should be added");
-    }]
-]);
-
-function cleanWrapperTests()
-{
-    try {
-        db.runStatement("drop table SQL_LIB_TEST_TABLE");
-    } catch(ex) {}
-    
-    try {
-        db.deleteData("PERSON", "PERSONID in ('TEST-5', 'TEST-6')")
-    } catch(ex) {}
-    
-}
-
-var dbWrapperTests = new TestSuite([
-    ["cell should load only one value", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME")
-                            .from("PERSON")
-                            .where("PERSON.PERSONID", "TEST-5")
-        pTester.assert("Franz", builder.cell());
-    }],
-
-    ["cell should just return '' if no condition set but pExecuteOnlyIfConditionExists is true", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME")
-                            .from("PERSON")
-        pTester.assert("", builder.cell(true));
-    }],
-
-    ["cell should return a value if no condition set and pExecuteOnlyIfConditionExists is false", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME")
-                            .from("PERSON");
-                            
-        var actual = builder.cell(false)
-        pTester.assert(true, actual !== "" && actual !== null && actual !== undefined);
-    }],
-
-    ["array should load an array of values", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON")
-                            .where("PERSON.PERSONID", "TEST-5");
-                            
-        var actual = builder.array(db.ROW);
-        pTester.assert("Franz", actual[0], "firstname should be 'Franz'");
-        pTester.assert("Müller", actual[1], "lastname should be 'Müller'");
-    }],
-
-    ["array should return an empty array if no condition set but pExecuteOnlyIfConditionExists is true", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON")
-                            
-        var actual = builder.array(db.ROW, true);
-        pTester.assert(0, actual.length);
-    }],
-
-    ["array should return a non-empty array if no condition set and pExecuteOnlyIfConditionExists is false", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON")
-                            
-        var actual = builder.array(db.ROW, false);
-        pTester.assert(true, actual.length > 0);
-    }],
- 
-    ["table should load an array of arrays with values", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON")
-                            .where("PERSON.PERSONID", "TEST-5")
-                            .or("PERSON.PERSONID", "TEST-6")
-                            .orderBy("PERSONID asc");
-                            
-        var actual = builder.table();
-        pTester.assert("Franz", actual[0][0], "firstname should be 'Franz'");
-        pTester.assert("Müller", actual[0][1], "lastname should be 'Müller'");
-        
-        pTester.assert("Marco", actual[1][0], "firstname should be 'Marco'");
-        pTester.assert("Polo", actual[1][1], "lastname should be 'Polo'");
-    }],
-
-    ["table should return an empty array if no condition set but pExecuteOnlyIfConditionExists is true", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON");
-                            
-        var actual = builder.table(true);
-        pTester.assert(0, actual.length);
-    }],
-
-    ["table should return a non-empty array if no condition set and pExecuteOnlyIfConditionExists is false", function(pTester)
-    {
-        var builder = new SqlBuilder()
-                            .select("FIRSTNAME, LASTNAME")
-                            .from("PERSON");
-                            
-        var actual = builder.table(false);
-        pTester.assert(true, actual.length > 0);
-    }],
-
-    ["delete should delete the data and use the from and condition from the builder", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
-                            
-        var deletedRows = builder.deleteData();
-        pTester.assert(1, deletedRows);
-    }],
-
-    ["delete should delete the data from the provided table and use the condition from the builder. It ignores .from if a table is given.", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("PERSON")
-                            .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
-                            
-        var deletedRows = builder.deleteData(false, "SQL_LIB_TEST_TABLE");
-        pTester.assert(1, deletedRows);
-    }],
-
-    ["delete should delete ALL data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is false", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            
-        var deletedRows = builder.deleteData(false);
-        pTester.assert(2, deletedRows);
-    }],
-
-    ["delete should delete NO data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is true", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            
-        var deletedRows = builder.deleteData(true);
-        pTester.assert(0, deletedRows);
-    }],
-
-    ["update should update the data and use the from and condition from the builder", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
-                            
-        builder.updateData(false, undefined, ["FIRSTNAME"], null, ["Fritz"]);
-        
-        var actual = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
-        
-        pTester.assert("Fritz", actual);
-    }],
-
-    ["update should update the data from the provided table and use the condition from the builder. It ignores .from if a table is given.", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID = 'TEST-7'");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("PERSON")
-                            .where("SQL_LIB_TEST_TABLE.TESTID", "TEST-7");
-                            
-        builder.updateData(false, "SQL_LIB_TEST_TABLE", ["FIRSTNAME"], null, ["Fritz"]);
-        
-        var actual = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
-        
-        pTester.assert("Fritz", actual);
-    }],
-
-    ["update should update ALL data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is false", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')");
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Franz", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            
-        builder.updateData(false, undefined, ["FIRSTNAME"], null, ["Fritz"]);
-        
-        var actual1 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
-        var actual2 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-8'")
-        
-        pTester.assert("Fritz", actual1, "TEST-7 should have Firstname Fritz");        
-        pTester.assert("Fritz", actual2, "TEST-8 should have Firstname Fritz");
-    }],
-
-    ["update should update NO data from a table if the builder has no condition and pExecuteOnlyIfConditionExists is true", function(pTester)
-    {
-        db.deleteData("SQL_LIB_TEST_TABLE", "TESTID in ('TEST-7', 'TEST-8')")        
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-7", "Ludwig", "Fischer"]);
-        db.insertData("SQL_LIB_TEST_TABLE", ["TESTID", "FIRSTNAME", "LASTNAME"], null, ["TEST-8", "Franz", "Fischer"]);
-        
-        var builder = new SqlBuilder()
-                            .from("SQL_LIB_TEST_TABLE")
-                            
-        builder.updateData(true, undefined, ["FIRSTNAME"], null, ["Fritz"]);
-        
-        var actual1 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-7'")
-        var actual2 = db.cell("select FIRSTNAME from SQL_LIB_TEST_TABLE where SQL_LIB_TEST_TABLE.TESTID = 'TEST-8'")
-        
-        pTester.assert("Ludwig", actual1, "TEST-7 should have Firstname Ludwig");        
-        pTester.assert("Franz", actual2, "TEST-8 should have Firstname Franz");
-    }]
-], function preAll() 
-{
-    // remove data which may exist if previous test-run failed and postAll was not executed
-    cleanWrapperTests()
-    
-    // add table for save testing of deletes
-    db.runStatement("create table SQL_LIB_TEST_TABLE (TESTID varchar(36), FIRSTNAME varchar(100), LASTNAME varchar(100))");
-    
-    // add test persons
-    db.insertData("PERSON", ["PERSONID", "FIRSTNAME", "LASTNAME", "USER_NEW", "DATE_NEW"], null, ["TEST-5", "Franz", "Müller", "testuser", vars.get("$sys.date")])
-    db.insertData("PERSON", ["PERSONID", "FIRSTNAME", "LASTNAME", "USER_NEW", "DATE_NEW"], null, ["TEST-6", "Marco", "Polo", "testuser", vars.get("$sys.date")])
-}, undefined, undefined, function postAll()
-{
-    cleanWrapperTests()
-})
-
-var mandatoryErrorTests = new TestSuite([
-// and
-    ["and without parameter should error", function(pTester)
-    {
-        new SqlBuilder().where().or();
-    }, SqlBuilder._ERROR_NO_PARAMETER_PROVIDED()],
-
-    ["and with null as value should error", function(pTester)
-    {
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", null);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["and with undefined as value should error", function(pTester)
-    {
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", undefined);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["and with a jdito-var containing null should error", function(pTester)
-    {
-        vars.set("$global.TestingVarNull", null);
-        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", "$global.TestingVarNull");
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR()],
-
-    ["and with an empty sql-builder as subquery should error", function(pTester)
-    {        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", new SqlBuilder());
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["and with an empty prepared statement as subquery should error", function(pTester)
-    {        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", ["", []]);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-
-// or
-    ["or without parameter should error", function(pTester)
-    {
-        new SqlBuilder().where().or();
-    }, SqlBuilder._ERROR_NO_PARAMETER_PROVIDED()],
-
-    ["or with null as value should error", function(pTester)
-    {
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", null);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["or with undefined as value should error", function(pTester)
-    {
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", undefined);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["or with a jdito-var containing null should error", function(pTester)
-    {
-        vars.set("$global.TestingVarNull", null);
-        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", "$global.TestingVarNull");
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY_JDITO_VAR()],
-
-    ["or with an empty sql-builder as subquery should error", function(pTester)
-    {        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", new SqlBuilder());
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-
-    ["or with an empty prepared statement as subquery should error", function(pTester)
-    {        
-        new SqlBuilder().where().or("PERSON.FIRSTNAME", ["", []]);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()],
-]);
-
-var inStatementTests = new TestSuite([
-    ["simple and in", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.LASTNAME", ["Franz", "Fritz"], SqlBuilder.IN()) // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter
-                            .or("PERSON.LASTNAME", ["Peter", "Mayer"], SqlBuilder.IN());
-                            
-        pTester.assert(" ( PERSON.LASTNAME in  (?, ?)  )  or  ( PERSON.LASTNAME in  (?, ?)  ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(4, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["simple and not in", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .where("PERSON.LASTNAME", ["Franz", "Fritz"], "# not in ?"); // Note: you can use SqlBuilder.IN(), SqlBuilder.NOT_IN(), "# in ?", etc. as 3rd parameter
-                            
-        pTester.assert(" ( PERSON.LASTNAME not in  (?, ?)  ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(2, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["in with subquery", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .where("PERSON.FIRSTNAME", new SqlBuilder()
-                                                        .select("PERSON.FIRSTNAME")
-                                                        .from("PERSON")
-                                                        .where("PERSON.LASTNAME", "Fritz")
-                                                , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?"
-                            
-        pTester.assert("PERSON.FIRSTNAME in  ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["in with prepared statement-array", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                        .where("PERSON.FIRSTNAME", ["select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ?", [["Fritz", SQLTYPES.VARCHAR]]]
-                                                , "# in ?"); // Note: you can use SqlBuilder.IN() instead of "# in ?"
-                            
-        pTester.assert("PERSON.FIRSTNAME in  ( select PERSON.FIRSTNAME from PERSON where PERSON.LASTNAME = ? ) ", actual._where._sqlStorage, "prepared sql");
-        pTester.assert(1, actual._where.preparedValues.length, "number of params");
-    }],
-
-    ["andIfSet should ignore empty array", function(pTester)
-    {
-        var actual = new SqlBuilder()
-                            .whereIfSet("PERSON.LASTNAME", []);
-                            
-        pTester.assert("", actual._where._sqlStorage, "prepared sql should be empty");
-        pTester.assert(0, actual._where.preparedValues.length, "number of params should be 0");
-    }],
-
-    ["and should error on an empty array", function(pTester)
-    {
-        new SqlBuilder()
-                .where("PERSON.LASTNAME", []);
-    }, SqlBuilder._ERROR_VALUE_IS_MANDATORY()]
-]);
-
-var testConstantFunctions = new TestSuite([
-    ["SqlBuilder.IN()", function(pTester)
-    {
-        pTester.assert("# in ?", SqlBuilder.IN());
-    }],
-    
-    ["SqlBuilder.NOT_IN()", function(pTester)
-    {
-        pTester.assert("# not in ?", SqlBuilder.NOT_IN());
-    }],
-
-    ["SqlBuilder.EXISTS()", function(pTester)
-    {
-        pTester.assert("exists ?", SqlBuilder.EXISTS());
-    }]
-]);
-
-var selectTests = new TestSuite([
-    ["a sql-builder in a fields-array is translated to sql correctly", function(pTester)
-    {
-        var countSubQuery = newSelect("count(*)")
-                                .from("AB_ATTRIBUTEUSAGE")
-                                .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", "myType")
-                                .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
-    
-        var actual = new SqlBuilder()
-            .select(["AB_ATTRIBUTEID", "AB_ATTRIBUTEUSAGEID", countSubQuery])
-            .from("AB_ATTRIBUTE")
-                        
-        pTester.assert("select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, (select count(*) from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.OBJECT_TYPE = ? and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID)", actual._select._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._select.preparedValues.length, "number of params");
-    }],
-
-    ["a sql-builder in from is used as subselect", function(pTester)
-    {
-        var subQuery = newSelect("FIRSTNAME")
-                                .from("PERSON")
-                                .where("PERSON.LASTNAME", "Meier")
-    
-        var actual = new SqlBuilder()
-            .select("*")
-            .from(subQuery)
-            
-        pTester.assert("from (select FIRSTNAME from PERSON where PERSON.LASTNAME = ?)", actual._from._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._from.preparedValues.length, "number of params");
-    }]
-]);
-
-var joinTests = new TestSuite([
-    ["SqlBuilder as on-condition should only add the conditon of the builder", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-    
-        var actual = new SqlBuilder()
-            .select("*")
-            .from("PERSON")
-            .join("ORGANISATION", subQuery)
-            
-        pTester.assert("join ORGANISATION on ORGANISATION.NAME = ?", actual._joins[0]._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._joins[0].preparedValues.length, "number of params");
-    }],
-
-    ["SqlBuilder as table for join is added as subselect", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-    
-        var actual = new SqlBuilder()
-            .select("*")
-            .from("PERSON")
-            .join(subQuery, "orgname.NAME = TABLE2.NAME", "orgname")
-            
-        pTester.assert("join (select NAME from ORGANISATION where ORGANISATION.NAME = ?) orgname on orgname.NAME = TABLE2.NAME", actual._joins[0]._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._joins[0].preparedValues.length, "number of params");
-    }],
-
-    ["just use a string also containing a condition as join without additional condition", function(pTester)
-    {
-        var actual = new SqlBuilder()
-            .select("*")
-            .from("PERSON")
-            .join("TABLE1 on TABLE1.NAME = TABLE2.NAME")
-            
-        pTester.assert("join TABLE1 on TABLE1.NAME = TABLE2.NAME", actual._joins[0]._sqlStorage, "prepared select-sql");
-        pTester.assert(0, actual._joins[0].preparedValues.length, "number of params");
-    }]
-]);
-
-var subqueryAsFieldTests = new TestSuite([
-    ["Test if a Subselect as field works if pValue is provided. This should be added as subselect.", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
-                                .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
-    
-        var actual = new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR)
-                                     .and("PERSON.FIRSTNAME", "val3");
-                
-            
-        pTester.assert(" ( ( select NAME from ORGANISATION where ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID and PERSON.FIRSTNAME = ? ) = ? )  and PERSON.FIRSTNAME = ?", actual._where._sqlStorage, "prepared select-sql");
-        pTester.assert(3, actual._where.preparedValues.length, "number of params");
-        pTester.assert("val1", actual._where.preparedValues[0][0], "param 1 is correct value");
-        pTester.assert("val2", actual._where.preparedValues[1][0], "param 2 is correct value");
-        pTester.assert("val3", actual._where.preparedValues[2][0], "param 3 is correct value");
-    }],
-
-    ["Test if a Subselect as field should error if no SQLTYPE is provided.", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
-                                .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
-        new SqlBuilder().where(subQuery, "val2", "# = ?");
-    }, SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NO_FIELD_TYPE()],
-
-    ["Test if a Subselect as field should error if it is not a full select.", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .where("ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
-                                .and("PERSON.FIRSTNAME", "val1") // test if the value is added at the correct place
-    
-        new SqlBuilder().where(subQuery, "val2", "# = ?", SQLTYPES.VARCHAR);
-    }, SqlBuilder._ERROR_SUBSELECT_AS_FIELD_NOT_COMPLETE()]
-]);
-
-var conditionFormatTests = new TestSuite([
-    ["pCondition should not fail if # an ? exist in correct order", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "# = ?")
-                        .and("PERSON.FIRSTNAME", "val1", "asdf # fdsa=asdf ?fdas")
-    }],
-
-    ["pCondition should not fail if # an ? exist in correct order and there are additional, escaped # and ?", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "\\? # \\#= ?");
-    }],
-
-    ["pCondition should not fail if only ? exists", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "?")
-    }],
-
-    ["pCondition should fail if more than one ? exists", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? test ?")
-    }, SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()],
-
-    ["pCondition should fail if more than one # exists", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "# test #")
-    }, SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()],
-
-    ["pCondition should fail if # and ? are in wrong order", function(pTester)
-    {
-        new SqlBuilder().where("PERSON.FIRSTNAME", "val1", "? = #")
-    }, SqlBuilder._ERROR_CONDITION_WRONG_FORMAT()]
-]);
-
-var subqueryAliasTests = new TestSuite([
-    ["subselectAlias should be added for subquery in .select", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-                                .subselectAlias("testAlias")
-    
-        var actual = new SqlBuilder()
-            .select([subQuery, "FIRSTNAME"])
-            .from("PERSON")
-            
-        pTester.assert("select (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias, FIRSTNAME", actual._select._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._select.preparedValues.length, "number of params");
-    }],
-
-    ["subselectAlias should be added for subquery in .from", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-                                .subselectAlias("testAlias")
-    
-        var actual = new SqlBuilder()
-            .from(subQuery)
-            
-        pTester.assert("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) testAlias", actual._from._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._from.preparedValues.length, "number of params");
-    }],
-
-    ["subselectAlias should be overruled by the param in in .from", function(pTester)
-    {
-        var subQuery = newSelect("NAME")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-                                .subselectAlias("testAlias")
-    
-        var actual = new SqlBuilder()
-            .from(subQuery, "overwriteAlias")
-            
-        pTester.assert("from (select NAME from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias", actual._from._sqlStorage, "prepared select-sql");
-        pTester.assert(1, actual._from.preparedValues.length, "number of params");
-    }],
-
-    ["subselectAlias should be added for subquery in .join", function(pTester)
-    {
-        var subQuery = newSelect("NAME, ORGANISATIONID")
-                                .from("ORGANISATION")
-                                .where("ORGANISATION.NAME", "Adito")
-                                .subselectAlias("testAlias")
-    
-        var actual = new SqlBuilder()
-            .from("CONTACT")
-            .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID")
-            .join(subQuery, "testAlias.ORGANISATIONID = ORGANISATION_ID", "overwriteAlias")
-            
-        pTester.assert("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) testAlias on testAlias.ORGANISATIONID = ORGANISATION_ID", actual._joins[0]._sqlStorage, "prepared select-sql join 1");
-        pTester.assert(1, actual._joins[0].preparedValues.length, "number of params join 1");
-        
-        pTester.assert("join (select NAME, ORGANISATIONID from ORGANISATION where ORGANISATION.NAME = ?) overwriteAlias on testAlias.ORGANISATIONID = ORGANISATION_ID", actual._joins[1]._sqlStorage, "prepared select-sql join 2");
-        pTester.assert(1, actual._joins[1].preparedValues.length, "number of params join 2");
-    }]
-])
-
-var tester = new Tester("Test SqlBuilder");
-tester.test(newSelectTests);
-tester.test(validAndUsageTests);
-tester.test(validOrUsageTests);
-tester.test(combinedAndOrTests);
-tester.test(ifSetTests);
-tester.test(dbWrapperTests);
-tester.test(mandatoryErrorTests);
-tester.test(inStatementTests);
-tester.test(testConstantFunctions);
-tester.test(selectTests);
-tester.test(joinTests);
-tester.test(subqueryAsFieldTests);
-tester.test(conditionFormatTests);
-tester.test(subqueryAliasTests);
-
-logging.log("-------------------------");
-tester.printResults();
diff --git a/process/SqlMaskingUtils_test/SqlMaskingUtils_test.aod b/process/SqlMaskingUtils_test/SqlMaskingUtils_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..8636a3707b101a2e7e0b22e10e54c6d596508b53
--- /dev/null
+++ b/process/SqlMaskingUtils_test/SqlMaskingUtils_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>SqlMaskingUtils_test</name>
+  <title>[TEST] Sql_lib - SqlMaskingUtils</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/SqlMaskingUtils_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/SqlMaskingUtils_test/process.js b/process/SqlMaskingUtils_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..533a1b32e9411f7e4af25222bfb28a6758b5edf3
--- /dev/null
+++ b/process/SqlMaskingUtils_test/process.js
@@ -0,0 +1,158 @@
+import("system.result");
+import("system.db");
+import("Sql_lib");
+import("UnitTest_lib");
+
+function _createDummyMaskingUtil(pDbType)
+{
+    var currentAlias = db.getCurrentAlias();
+    if (!currentAlias)
+        throw new Error("alias required for test is not set");
+    var currentAliasType = db.getDatabaseType(currentAlias);
+    if (!currentAliasType)
+        throw new Error("alias type required for test is not set");
+    var createdObject = new SqlMaskingUtils(currentAlias);
+    createdObject.dbType = pDbType;
+    return createdObject;
+}
+
+var constructorTest = new TestSuite("SqlMaskingUtils.constructor", [
+    new Test("constructor sets correct specified alias",
+        function(pTester)
+        {
+            var currentAlias = db.getCurrentAlias();
+            if (!currentAlias)
+                throw new Error("alias required for test is not set");
+            var createdObject = new SqlMaskingUtils(currentAlias);
+            pTester.expectThat(createdObject.alias).equals(currentAlias).assert();
+        }
+        ),
+    new Test("constructor sets correct default alias",
+        function(pTester)
+        {
+            var currentAlias = db.getCurrentAlias();
+            if (!currentAlias)
+                throw new Error("alias required for test is not set");
+            var createdObject = new SqlMaskingUtils();
+            pTester.expectThat(createdObject.alias).equals(currentAlias).assert();
+        }
+        ),
+    new Test("constructor sets correct alias type",
+        function(pTester)
+        {
+            var currentAlias = db.getCurrentAlias();
+            if (!currentAlias)
+                throw new Error("alias required for test is not set");
+            var currentAliasType = db.getDatabaseType(currentAlias);
+            if (!currentAliasType)
+                throw new Error("alias type required for test is not set");
+            var createdObject = new SqlMaskingUtils(currentAlias);
+            pTester.expectThat(createdObject.dbType).equals(currentAliasType).assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with derby",
+     function(pTester)
+     {
+        var res = _createDummyMaskingUtil(db.DBTYPE_DERBY10);
+        pTester.expectThat(res.dbType).equals(db.DBTYPE_DERBY10).assert();
+        pTester.expectThat(res.alias).isNull().assert();
+     }
+     ),
+    new Test("overwrite constructors dbType with mariaDB",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_MARIADB10);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_MARIADB10).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with mySql",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_MYSQL4);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_MYSQL4).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with oracle-cluster",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_ORACLE10_CLUSTER);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_ORACLE10_CLUSTER).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with oracle-oci",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_ORACLE10_OCI);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_ORACLE10_OCI).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with oracle-thin",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_ORACLE10_THIN);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_ORACLE10_THIN).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with postgresql",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_POSTGRESQL8);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_POSTGRESQL8).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        ),
+    new Test("overwrite constructors dbType with ms sql",
+        function(pTester)
+        {
+            var res = _createDummyMaskingUtil(db.DBTYPE_SQLSERVER2000);
+            pTester.expectThat(res.dbType).equals(db.DBTYPE_SQLSERVER2000).assert();
+            pTester.expectThat(res.alias).isNull().assert();
+        }
+        )
+    ]);
+
+
+var getConcatSymbolTest = new TestSuite("SqlMaskingUtils.getConcatSymbol", [
+    new Test("getConcatSymbol returns a non empty string",
+        function(pTester)
+        {
+            var maskingHelper = _createDummyMaskingUtil(db.DBTYPE_SQLSERVER2000);
+            var res = maskingHelper.getConcatSymbol();
+            pTester.expectThat(res).not().isNull().assert();
+            pTester.expectThat(res).not().isUndefined().assert();
+            pTester.expectThat(res).isString().assert();
+            pTester.expectThat(res.length).isInteger().assert();
+            pTester.expectThat(res.length).isGreater(0).assert();
+        }
+        ),
+    new Test("+ for MS SQL",
+        function(pTester)
+        {
+            var maskingHelper = _createDummyMaskingUtil(db.DBTYPE_SQLSERVER2000);
+            var res = maskingHelper.getConcatSymbol();
+            pTester.expectThat(res.trim()).equals("+").assert();
+        }
+        ),
+    new Test("|| for Oracle",
+        function(pTester)
+        {
+            var maskingHelper = _createDummyMaskingUtil(db.DBTYPE_ORACLE10_THIN);
+            var res = maskingHelper.getConcatSymbol();
+            pTester.expectThat(res.trim()).equals("||").assert();
+        }
+        )
+    ]);
+
+var tester = new Tester("Test SqlMaskingUtils");
+tester.initCoverage(SqlMaskingUtils);
+tester.test(constructorTest);
+tester.test(getConcatSymbolTest);
+//TODO: add full test coverage
+tester.summary();
+
+result.object(tester.getResults());
\ No newline at end of file
diff --git a/process/Sql_lib/documentation.adoc b/process/Sql_lib/documentation.adoc
index 8b903c2155e9d54f160b06ef6fa80a586a8335e6..417d8aa237bf0311576e048625e9b055ff1e2db8 100644
--- a/process/Sql_lib/documentation.adoc
+++ b/process/Sql_lib/documentation.adoc
@@ -17,10 +17,10 @@ include::_default_attributes_EN.adoc[]
 
 This document describes the functionality and the usage of the SqlBuilder, which is included in the library *Sql_lib* of the ADITO xRM project (see "Projects" window, under process > libraries). The documentation may not contain all features of the SqlBuilder. It is supplemental to the documentation you find in the code itself: A usage will often be possible in an intuitive way, so just try coding using code completion and JSDoc, where all parameters are documented.
 
-You may also take a look at the library *SqlLib_tests* (also under process > libraries), as it contains many possible ways to use the SqlBuilder.
+You may also take a look at the library *Sql_test* (also under process > libraries), as it contains many possible ways to use the SqlBuilder.
 
 [NOTE]
-The tests included in the library SqlLib_tests use the UnitTest_lib for unit testing. You can use this functionality also in other contexts, according to your requirements.
+The tests included in the library Sql_test use the UnitTest_lib for unit testing. You can use this functionality also in other contexts, according to your requirements.
 
 == Benefits
 
diff --git a/process/Sql_lib/process.js b/process/Sql_lib/process.js
index 35b87999459da0cd88f21f0b11c737c3060ce26e..75c6923e24957c4f574efec077d73d60d9a52680 100644
--- a/process/Sql_lib/process.js
+++ b/process/Sql_lib/process.js
@@ -801,8 +801,6 @@ function SqlBuilder (pAlias)
     
     this._where = {};
     this._initWhere();
-    
-    SqlBuilder.defineCanBuildSql(this);
 }
 
 /**
@@ -823,6 +821,8 @@ SqlBuilder.checkCanBuildSql = function (pObject)
     return pObject[SqlBuilder.getCanBuildSqlSymbol()];
 }
 
+SqlBuilder.defineCanBuildSql(SqlBuilder.prototype);
+
 /**
  * Deep copies the SqlBuilder object and returns a new one.<br/>
  * Use this if you want to add for example add additional parameters without modifying the current builder.
@@ -830,25 +830,7 @@ SqlBuilder.checkCanBuildSql = function (pObject)
  */
 SqlBuilder.prototype.copy = function()
 {
-    var newBuilder = _deepCopyByJson(this, new SqlBuilder());
-    return newBuilder;
-    
-    // NOTE: this works only with simple data types. 
-    // Here we only use strings, arrays, booleans and null, so this should work
-    function _deepCopyByJson(pObject, pNewObject)
-    {
-        // deep copy by using json
-        var deepCopied = JSON.parse(JSON.stringify(pObject));
-        
-        // set the props of the new object to the deepCopied ones.
-        // without this all functions would be lost
-        for (let prop in deepCopied)
-        {
-            pNewObject[prop] = deepCopied[prop]
-        }
-        
-        return pNewObject;
-    }
+    return Utils.clone(this);
 }
 
 // errors which are thrown by the SqlBuilder
@@ -3100,6 +3082,14 @@ SqlBuilder.caseWhen = function (pFieldOrCond, pValue, pCondition, pFieldType)
     return new SqlBuilder._CaseStatement().when(pFieldOrCond, pValue, pCondition, pFieldType);
 }
 
+/**
+ * @return {SqlBuilder._CaseStatement}
+ */
+SqlBuilder.caseStatement = function ()
+{
+    return new SqlBuilder._CaseStatement();
+}
+
 /**
  * Represents a case-when statement
  */
@@ -3109,9 +3099,10 @@ SqlBuilder._CaseStatement = function ()
     this._whenThens = [];
     this._elseValue = null;
     this._afterWhenMask = new SqlBuilder._CaseWhen(this);
-    SqlBuilder.defineCanBuildSql(this);
 }
 
+SqlBuilder.defineCanBuildSql(SqlBuilder._CaseStatement.prototype);
+
 /**
  * @param {String|String[]|SqlBuilder|PreparedSqlArray} [pFieldOrCond] If this is the only parameter, it is used as Subselect <br/>
  *                                                                     else it is used as Field. <br/>
@@ -3167,7 +3158,7 @@ SqlBuilder._CaseStatement.prototype.toString = function (pAlias)
     return db.translateStatement(this.build(), pAlias || db.getCurrentAlias());
 }
 
-SqlBuilder._CaseStatement.prototype.build = function ()
+SqlBuilder._CaseStatement.prototype.build = function (pParameters)
 {
     var caseStatement = ["case"];
     var preparedValues = [];
@@ -3373,6 +3364,8 @@ SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLengt
                 return "char";
             case SQLTYPES.VARCHAR:
                 return "char";
+            case SQLTYPES.NVARCHAR:
+                return "nvarchar";
             case SQLTYPES.INTEGER:
                 return "int";
             case SQLTYPES.DECIMAL:
@@ -3388,6 +3381,7 @@ SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLengt
         case db.DBTYPE_DERBY10:
             switch(pTargetDatatype) 
             {
+                case SQLTYPES.NVARCHAR:
                 case SQLTYPES.VARCHAR:
                     // Because of a Derby bug, you can't cast INTEGER into VARCHAR
                     // Therefor first cast to char then to varchar
@@ -3412,6 +3406,7 @@ SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLengt
         case db.DBTYPE_MYSQL4:
             switch(pTargetDatatype) 
             {
+                case SQLTYPES.NVARCHAR:
                 case SQLTYPES.VARCHAR:
                 case SQLTYPES.CHAR:
                 case SQLTYPES.INTEGER:
@@ -3429,6 +3424,9 @@ SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLengt
                 case SQLTYPES.VARCHAR:
                     sqlDataType = "varchar2";
                     break;
+                case SQLTYPES.NVARCHAR:
+                    sqlDataType = "nvarchar2";
+                    break;
                 case SQLTYPES.INTEGER:
                     sqlDataType = "number";
                     pTargetLength = "10"
@@ -3448,18 +3446,23 @@ SqlMaskingUtils.prototype.cast = function (pField, pTargetDatatype, pTargetLengt
                 case SQLTYPES.INTEGER:
                 case SQLTYPES.CHAR:
                 case SQLTYPES.VARCHAR:
+                case SQLTYPES.NVARCHAR:
                     sqlDataType = _mapDefaults(pTargetDatatype);
                     break;
             }
             break;
         case db.DBTYPE_SQLSERVER2000:
-        case SQLTYPES.DATE:
-        case SQLTYPES.DECIMAL:
-        case SQLTYPES.INTEGER:
-        case SQLTYPES.CHAR:
-        case SQLTYPES.VARCHAR:
-            sqlDataType = _mapDefaults(pTargetDatatype);
-            break;
+            switch(pTargetDatatype)
+            {
+                case SQLTYPES.DATE:
+                case SQLTYPES.DECIMAL:
+                case SQLTYPES.INTEGER:
+                case SQLTYPES.CHAR:
+                case SQLTYPES.VARCHAR:
+                case SQLTYPES.NVARCHAR:
+                    sqlDataType = _mapDefaults(pTargetDatatype);
+                    break;
+            }
             //TODO: firebird support?
     }
 
@@ -3589,7 +3592,7 @@ SqlMaskingUtils.prototype.concat = function (pFields, pSeparator, pAutoTrimField
     
     if (pSeparator === null || pSeparator === undefined)
         pSeparator = "' '";
-    else if (pSeparator)
+    else if (pSeparator || pSeparator === "")
         pSeparator = "'" + db.quote(pSeparator, this.alias) + "'";
     
     var doEmptyStringCheck = true;
@@ -4152,7 +4155,7 @@ SqlUtils.getResolvingCaseWhen = function(pKeyValueArray, pDbFieldName, pLocale)
         else
             return translate.text(value);
     };
-    
+    //!SqlBuilder
     var resSql = "case ", preparedValues = [];
     var colTypeKeyId = SQLTYPES.CHAR;
     var colTypeTitle = SQLTYPES.NVARCHAR;
diff --git a/process/SqlLib_tests/SqlLib_tests.aod b/process/Terminal_lib/Terminal_lib.aod
similarity index 67%
rename from process/SqlLib_tests/SqlLib_tests.aod
rename to process/Terminal_lib/Terminal_lib.aod
index 9f2866f8f91251c708400d6998ee92c248385334..9fa0811425e089b731f59a376c4089e526725b17 100644
--- a/process/SqlLib_tests/SqlLib_tests.aod
+++ b/process/Terminal_lib/Terminal_lib.aod
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
-  <name>SqlLib_tests</name>
+  <name>Terminal_lib</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
-  <process>%aditoprj%/process/SqlLib_tests/process.js</process>
-  <alias>Data_alias</alias>
+  <documentation>%aditoprj%/process/Terminal_lib/documentation.adoc</documentation>
+  <process>%aditoprj%/process/Terminal_lib/process.js</process>
   <variants>
     <element>LIBRARY</element>
   </variants>
diff --git a/process/Terminal_lib/process.js b/process/Terminal_lib/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..13a0188495d15d6728d4560c288546ccc0c9d283
--- /dev/null
+++ b/process/Terminal_lib/process.js
@@ -0,0 +1,342 @@
+import("system.logging");
+import("Util_lib");
+
+
+/**
+ * This library gives you helpful colored output on the terminal
+ * You can use the predefined helper methods or use your own color- and format combinations
+ *
+ * @example
+ * var t = new Terminal();
+ * logging.log(t.success("It works!"));
+ * logging.log(t.color(t.fg.green).background(t.bg.red).output("I would never use green text on red background..."));
+ *
+ * @see https://en.wikipedia.org/wiki/ANSI_escape_code
+ * @class
+ */
+function Terminal()
+{
+    this.mod = {
+        reset : 0
+    };
+
+    this.fg = {
+        reset         : 39,
+        black         : 30,
+        red           : 31,
+        green         : 32,
+        yellow        : 33,
+        blue          : 34,
+        magenta       : 35,
+        cyan          : 36,
+        white         : 37,
+        brightBlack   : 90,
+        brightRed     : 91,
+        brightGreen   : 92,
+        brightYellow  : 93,
+        brightBlue    : 94,
+        brightMagenta : 95,
+        brightCyan    : 96,
+        brightWhite   : 97
+    };
+
+    this.bg = {
+        reset         : 49,
+        black         : 40,
+        red           : 41,
+        green         : 42,
+        yellow        : 43,
+        blue          : 44,
+        magenta       : 45,
+        cyan          : 46,
+        white         : 47,
+        brightBlack   : 100,
+        brightRed     : 101,
+        brightGreen   : 102,
+        brightYellow  : 103,
+        brightBlue    : 104,
+        brightMagenta : 105,
+        brightCyan    : 106,
+        brightWhite   : 107
+    };
+
+    this.formats = {
+        weightBold      : 1,
+        weightLight     : 2,
+        weightOff       : 22,
+        italic          : 3,
+        italicOff       : 23,
+        underline       : 4,
+        underlineDouble : 21,
+        underlineOff    : 24,
+        strike          : 9,
+        strikeOff       : 29
+    };
+
+    this._defaultConfig = {
+        colorText: this.fg.reset,
+        colorBackground: this.bg.reset,
+        bold: this.formats.weightOff,
+        italic: this.formats.italicOff,
+        underline: this.formats.underlineOff,
+        strike: this.formats.strikeOff
+    };
+
+    this.outputConfig = Utils.clone(this._defaultConfig);
+}
+
+
+/**
+ * Reset the output config by cloning and setting the default
+ *
+ * @return {void}
+ */
+Terminal.prototype._resetConfig = function ()
+{
+    this.outputConfig = Utils.clone(this._defaultConfig);
+}
+
+/**
+ * Set or reset the text color
+ *
+ * @param {int} pValue
+ * @return {Terminal}
+ */
+Terminal.prototype.color = function (pValue)
+{
+    this.outputConfig.colorText = this._contains(this.fg, pValue) ? pValue : this.fg.reset;
+    
+    return this;
+}
+
+/**
+ * Set or reset the background color
+ *
+ * @param {int} pValue
+ * @return {Terminal}
+ */
+Terminal.prototype.background = function (pValue)
+{
+    this.outputConfig.colorBackground = this._contains(this.bg, pValue) ? pValue : this.bg.reset;
+    
+    return this;
+}
+
+/**
+ * Set, unset or add a formatting to the output config
+ *
+ * @param {int} pValue
+ * @return {Terminal}
+ */
+Terminal.prototype.format = function (pValue)
+{    
+    var allowedWeightValues = [this.formats.weightBold, this.formats.weightLight, this.formats.weightOff];
+    this.outputConfig.bold = this._contains(allowedWeightValues, pValue) ? pValue : this.formats.weightOff;
+
+    var allowedItalicValues = [this.formats.italic, this.formats.italicOff];
+    this.outputConfig.italic = this._contains(allowedItalicValues, pValue) ? pValue : this.formats.italicOff;
+
+    var allowedUnderlineValues = [this.formats.underline, this.formats.underlineDouble, this.formats.underlineOff];
+    this.outputConfig.underline = this._contains(allowedUnderlineValues, pValue) ? pValue : this.formats.underlineOff;
+
+    var allowedStrikeValues = [this.formats.strike, this.formats.strikeOff];
+    this.outputConfig.strike = this._contains(allowedStrikeValues, pValue) ? pValue : this.formats.strikeOff;
+    
+    return this;
+}
+
+/**
+ * Returns a string wrapped in escape sequences according to previous formatting instructions
+ * Must be called after `.color()`, `.background()` and/or `.format()`.
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.output = function (pString)
+{
+    var output = "";
+    var seqValues = [];
+    
+    Object.keys(this.outputConfig).forEach(function(pKey) {
+        seqValues.push(this.outputConfig[pKey]);
+    }, this);
+    
+    output += this._generateSequence(seqValues);
+    output += pString;
+    output += this._generateSequence(this.mod.reset);
+    
+    this._resetConfig();
+    return output;
+}
+
+/**
+ * Returns the given string formatted as light-colored text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.debug = function (pString)
+{
+    return this.color(this.fg.white).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold black text on green background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.successBlock = function (pString)
+{
+    return this.color(this.fg.brightBlack).background(this.bg.green).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold green text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.success = function (pString)
+{
+    return this.color(this.fg.green).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold black text on green background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.successBlock = function (pString)
+{
+    return this.color(this.fg.brightBlack).background(this.bg.green).format(this.formats.weightBold).output(" " + pString + " ");
+}
+
+/**
+ * Returns the given string formatted as bold blue text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.info = function (pString)
+{
+    return this.color(this.fg.blue).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold white text on blue background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.infoBlock = function (pString)
+{
+    return this.color(this.fg.brightWhite).background(this.bg.blue).format(this.formats.weightBold).output(" " + pString + " ");
+}
+
+/**
+ * Returns the given string formatted as bold magenta text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.important = function (pString)
+{
+    return this.color(this.fg.magenta).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold white text on magenta background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.importantBlock = function (pString)
+{
+    return this.color(this.fg.brightWhite).background(this.bg.magenta).format(this.formats.weightBold).output(" " + pString + " ");
+}
+
+/**
+ * Returns the given string formatted as bold yellow text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.warning = function (pString)
+{
+    return this.color(this.fg.yellow).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold black text on yellow background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.warningBlock = function (pString)
+{
+    return this.color(this.fg.brightBlack).background(this.bg.yellow).format(this.formats.weightBold).output(" " + pString + " ");
+}
+
+/**
+ * Returns the given string formatted as bold red text
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.error = function (pString)
+{
+    return this.color(this.fg.red).format(this.formats.weightBold).output(pString);
+}
+
+/**
+ * Returns the given string formatted as bold white text on red background
+ *
+ * @param {String} pString the string to format
+ * @return {String}
+ */
+Terminal.prototype.errorBlock = function (pString)
+{
+    return this.color(this.fg.brightWhite).background(this.bg.red).format(this.formats.weightBold).output(" " + pString + " ");
+}
+
+/**
+ * Checks if a specific value is in a given array or object
+ *
+ * @param {(Array|Object)} pType
+ * @param {Any} pValue
+ * @return {Boolean}
+ */
+Terminal.prototype._contains = function (pType, pValue)
+{
+    var res = false;
+    
+    if(Array.isArray(pType)) {
+        res = pType.indexOf(pValue) !== -1;
+    } else {
+        Object.keys(pType).forEach(function(pKey) {
+            if(pType[pKey] === pValue) {
+                res = true;
+            }
+        }, this);
+    }
+
+    return res;
+}
+
+/**
+ * Generates and returns a terminal escape sequence
+ *
+ * @param {(int|int[])} pSequences A single or multiple escape sequence commands
+ */
+Terminal.prototype._generateSequence = function (pSequences)
+{
+    var seq = Array.isArray(pSequences) ? pSequences : [pSequences];
+    
+    seq = seq.filter(function(pElement) {
+        return Number.isInteger(parseFloat(pElement)) && isFinite(pElement);
+    });
+    
+    return "\033[" + seq.join(";") + "m";
+}
diff --git a/process/Turnover_lib/process.js b/process/Turnover_lib/process.js
index 14080be4ee9b533ea4a1c88331e3d6f45f64f364..1e69ad37d5f05c94e3779b5fe5c6edb3fe81cbc5 100644
--- a/process/Turnover_lib/process.js
+++ b/process/Turnover_lib/process.js
@@ -26,24 +26,45 @@ function TurnoverUtil() {}
  */
 TurnoverUtil.getTurnoverData = function (pMaxYear, pYearCount, pSalesprojectId)
 {
-    var turnoverCategory = translate.text('Turnover');
-
+    var turnoverCategory = translate.text("Turnover");
     var minYear = pMaxYear - pYearCount + 1;
+    var sqlMask = new SqlMaskingUtils();
         
     // load data
-    return newSelect("'" + turnoverCategory + "', year(SALESORDERDATE) yearNum, month(SALESORDERDATE) monthNum, SALESORDERITEM.DISCOUNT discount, SALESORDERITEM.VAT vat, SALESORDERITEM.PRICE price, sum(SALESORDERITEM.QUANTITY) quantity, SALESORDERITEM.GROUPCODEID prodGroup, (" + KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.productGroupcode(), "SALESORDERITEM.GROUPCODEID") + ") prodGroupName")
-                    .from("SALESORDER")
-                    .join("SALESORDERITEM", "SALESORDERITEM.SALESORDER_ID = SALESORDER.SALESORDERID")
-                    .where("SALESORDER.ORDERTYPE = 'ORDTYPEINVO'")
-                    .and("SALESORDER.ORDERSTATUS = 1")
-                    .and("SALESORDER.CANCELLATION <> 1")
-                    .and("SALESORDERITEM.OPTIONAL <> 1")
-                    .and("SALESORDER.SALESORDERDATE", pMaxYear, "year(#) <= ?", SQLTYPES.INTEGER)
-                    .and("SALESORDER.SALESORDERDATE", minYear, "year(#) >= ?", SQLTYPES.INTEGER)
-                    .andIfSet("SALESORDER.SALESPROJECT_ID", pSalesprojectId)
-                    .groupBy("year(SALESORDERDATE), month(SALESORDERDATE), SALESORDERITEM.GROUPCODEID, SALESORDERITEM.DISCOUNT, SALESORDERITEM.VAT, SALESORDERITEM.PRICE")
-                    .orderBy("yearNum, monthNum")
-                    .table();
+    
+    return newSelect([
+            "'" + turnoverCategory + "'", 
+            sqlMask.yearFromDate("SALESORDERDATE"), 
+            sqlMask.monthFromDate("SALESORDERDATE"),
+            "SALESORDERITEM.DISCOUNT",
+            "SALESORDERITEM.VAT",
+            "SALESORDERITEM.PRICE",
+            "sum(SALESORDERITEM.QUANTITY)",
+            "SALESORDERITEM.GROUPCODEID",
+            KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.productGroupcode(), "SALESORDERITEM.GROUPCODEID")
+        ])
+        .from("SALESORDER")
+        .join("SALESORDERITEM", "SALESORDERITEM.SALESORDER_ID = SALESORDER.SALESORDERID")
+        .where("SALESORDER.ORDERTYPE", "ORDTYPEINVO")
+        .and("SALESORDER.ORDERSTATUS", "1")
+        .and("SALESORDER.CANCELLATION", "1", SqlBuilder.NOT_EQUAL())
+        .and("SALESORDERITEM.OPTIONAL", "1", SqlBuilder.NOT_EQUAL())
+        .and("SALESORDER.SALESORDERDATE", pMaxYear, sqlMask.yearFromDate("#") + " <= ?", SQLTYPES.INTEGER)
+        .and("SALESORDER.SALESORDERDATE", minYear, sqlMask.yearFromDate("#") + " >= ?", SQLTYPES.INTEGER)
+        .andIfSet("SALESORDER.SALESPROJECT_ID", pSalesprojectId)
+        .groupBy([
+            sqlMask.yearFromDate("SALESORDERDATE"), 
+            sqlMask.monthFromDate("SALESORDERDATE"),
+            "SALESORDERITEM.GROUPCODEID",
+            "SALESORDERITEM.DISCOUNT",
+            "SALESORDERITEM.VAT",
+            "SALESORDERITEM.PRICE"
+        ])
+        .orderBy([
+            sqlMask.yearFromDate("SALESORDERDATE"), 
+            sqlMask.monthFromDate("SALESORDERDATE")
+        ])
+        .table();
 }
 
 /**
diff --git a/process/UnitTest_lib/UnitTest_lib.aod b/process/UnitTest_lib/UnitTest_lib.aod
index 225ac2303844a684c92ac95b0426318362598cb0..a44993167651207727a423951deff7db60238dfb 100644
--- a/process/UnitTest_lib/UnitTest_lib.aod
+++ b/process/UnitTest_lib/UnitTest_lib.aod
@@ -2,6 +2,7 @@
 <process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
   <name>UnitTest_lib</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE_O</icon>
   <process>%aditoprj%/process/UnitTest_lib/process.js</process>
   <alias>Data_alias</alias>
   <variants>
diff --git a/process/UnitTest_lib/process.js b/process/UnitTest_lib/process.js
index 0ce141d4828e7b396f4cc4368c4daae06aec5dd1..382d37e021567c924734a1350a7a368acd1796d3 100644
--- a/process/UnitTest_lib/process.js
+++ b/process/UnitTest_lib/process.js
@@ -1,235 +1,1265 @@
 import("system.logging");
+import("Terminal_lib");
+import("Util_lib");
+
+/**
+ * @param {String} pName the name/description of the test
+ * @param {Function} pCallback the actual test callback to execute
+ * @param {Function} pDataProviderCallback an optional callback to deliver multiple datasets to the test
+ * @param {Boolean} pRerunOnError an optional flag indicating if a test should get rerun without try-catch if an error occurs (default: false)
+ *
+ * @class
+ */
+function Test(pName, pCallback, pDataProviderCallback, pRerunOnError)
+{
+    this.name = pName;
+    this.callback = pCallback;
+    this.dataProviderCallback = pDataProviderCallback || function() { return null; };
+    this.rerunOnError = !!pRerunOnError;
+}
 
 /**
  * A TestSuite combines several tests
- * @param {Array} pTests this is a n array of Tests in the following form:<br/>
- *                  [<br/>
- *                      ["Test description", function(pTester) <br/>
- *                          {<br/>
- *                              // the test which may use pTester.assert(...)<br/>
- *                          }, {Optional: an expected error}<br/>
- *                      ],<br/>
- *                      [...]<br/>
- *                  ]<br/>
+ *
+ * @param {String} pName name of the TestSuite
+ * @param {Array} pTests this is a n array of Tests
+ *
+ * @example
+ * var myTest = new TestSuite("MyObject.myTest", [
+ *     new Test("Test description",
+ *         function(pTester) {
+ *             var expectValue = 5;
+ *             var var actualValue = 20;
+ *             pTester.expectThat(actualValue).isGreater(expectValue).assert();
+ *         },
+ *     ),
+ *     new Test(...)
+ * ];
+
+ * @example
+ * // Using a DataProvider
+ * var myTest = new TestSuite("MyObject.myTest", [
+ *     new Test("Test description",
+ *         function(pTester, pDataProvider) {
+ *             var obj = new MyObject("Something");
+ *             var expectValue = pDataProvider[0];
+ *             var var actualValue = obj.getNumber();
+ *             pTester.expectThat(actualValue).isGreater(expectValue).assert();
+ *         },
+ *         function dataProvider() {
+ *             return [
+ *                 [0],
+ *                 [10],
+ *                 [100],
+ *             ];
+ *         },
+ *     ),
+ *     new Test(...)
+ * ];
+ *
  * @param {functionCallback} [pPreAll] a callback, called once before all tests
  * @param {functionCallback} [pPreTest] a callback, called once before each test
  * @param {functionCallback} [pPostTest] a callback, called once after each test
  * @param {functionCallback} [pPostAll] a callback, called once after all tests
- * 
+ *
+ * TODO: set callbacks via separate methods
+ *
  * @class
  */
-function TestSuite(pTests, pPreAll, pPreTest, pPostTest, pPostAll)
+function TestSuite(pName, pTests, pPreAll, pPreTest, pPostTest, pPostAll)
 {
+    this.name = pName;
     this.tests = pTests;
-    this.preAll = (pPreAll ? pPreAll : function() {});
-    this.preTest = (pPreTest ? pPreTest : function() {});
-    this.postTest = (pPostTest ? pPostTest : function() {});
-    this.postAll = (pPostAll ? pPostAll : function() {});
+    this.preAll = pPreAll || function() {};
+    this.preTest = pPreTest || function() {};
+    this.postTest = pPostTest || function() {};
+    this.postAll = pPostAll || function() {};
 }
 
 /**
- * The tester can test TestSuites.
- * It will be passed as paramter to each test.
- * 
+ * The tester runs TestSuites.
+ * It will be passed as parameter to each test.
+ *
  * @param {String} pCollectionName
+ *
+ * @example
+ * var myTest = new TestSuite("MyObject.myTest", [...]);
+ * var tester = new Tester("Test SqlBuilder", SqlBuilder);
+ * tester.test(myTest);
+ * // More test suites using tester.test()
+ * // ...
+ *
+ * tester.summary();
+ *
+ * @class
+ *
+ * TODO: Implement `isEmpty()`
+ * TODO: Implement `hasKey()`
+ * TODO: Implement `contains()`
+ * TODO: Implement `containsIgnoreCase()`
+ * TODO: Implement `isPrimitive()`
+ * TODO: Implement `matches()`
+ * TODO: Implement `matchesIgnoreCase()`
+ * TODO: Move multiple used code blocks into separate methods (like the logging output + reset)
+ * TODO: Use more getters, especially when implementing `elementAt()`
+ * TODO: Add examples to test methods
+ */
+function Tester(pCollectionName)
+{
+    this.collectionName = pCollectionName;
+    this.instanceName = undefined;
+    this.methods = [];
+    this.methodsCalled = [];
+
+    this.actualValue = undefined;
+    this.actualDisplayValue = undefined;
+    this.actualOriginalValue = undefined;
+    this.actualOriginalDisplayValue = undefined;
+    this.actualValueElementHierarchy = [];
+    this.expectedValue = undefined;
+    this.expectedDisplayValue = undefined;
+    this._assertDescription = "";
+    this._testResult = false;
+    this._useNegation = false;
+    this.dataProvider = undefined;
+    this.currentTestSuite = undefined;
+
+    this.testCount = 0;
+    this.successCount = 0;
+    this.failCount = 0;
+    this.startTime = new Date().getTime();
+    this.endTime = null;
+
+    this.t = new Terminal();
+    this.outputEnabled = true;
+    this.output = "";
+}
+
+/**
+ * Set the value to test
+ *
+ * @param {*} pValue
+ * @return {Tester}
+ */
+Tester.prototype.expectThat = function(pValue)
+{
+    this.actualValue = pValue;
+    this.actualDisplayValue = pValue;
+    this.actualOriginalValue = pValue;
+    this.actualOriginalDisplayValue = pValue;
+    this._useNegation = false;
+
+    return this;
+}
+
+/**
+ * Retrieve the result of the current test
+ *
+ * @return {Boolean}
+ */
+Tester.prototype.getTestResult = function()
+{
+    return this._useNegation ? !this._testResult : this._testResult;
+}
+
+/**
+ * Enable or disable the output
+ *
+ * @param {Boolean} pIsEnabled whether the output should be enabled or not
+ * @return {Tester}
+ */
+Tester.prototype.setOutputEnabled = function(pIsEnabled)
+{
+    this.outputEnabled = pIsEnabled;
+    return this;
+}
+
+/**
+ * Assert makes the test eventually pass or fail by looking at the result and writing into the output
+ *
+ * @return {void}
+ */
+Tester.prototype.assert = function()
+{
+    if(this.getTestResult() === true)
+    {
+        this.successCount++;
+        this._log("success", "\t\t\u2705 " + this._assertDescription);
+    }
+    else
+    {
+        this.failCount++;
+        this._log("error", "\t\t\u274C " + this._assertDescription);
+
+        if(this.actualDisplayValue !== undefined && this.expectedDisplayValue !== undefined)
+        {
+            this._log("warning", "\t\t\t expected: " + JSON.stringify(this.expectedDisplayValue, null, "\t"));
+            this._log("error", "\t\t\t actual:   " + JSON.stringify(this.actualDisplayValue, null, "\t"));
+        }
+    }
+}
+
+/**
+ * Activate test negation
+ *
+ * @return {Tester}
+ */
+Tester.prototype.not = function()
+{
+    this._useNegation = true;
+
+    return this;
+}
+
+/**
+ * Select a child element of `actualValue` to work with
+ *
+ * @param {(String|Number)} pKey the key of the child
+ * @return {Tester}
  * 
  * @example
- * var tester = new Tester("Test SqlBuilder");
- * tester.test(newSelectTests);
- * tester.test(validAndUsageTests);
- * tester.test(validOrUsageTests);
+ * // Grab first array element to test
+ * pTester.expectThat(actualValue).elementAt(0).equals("MyValue").assert();
  * 
- * logging.log("-------------------------");
- * tester.printResults();
+ * // Grab last array element to test
+ * pTester.expectThat(actualValue).elementAt(-1).equals("MyValue").assert();
  * 
- * @class
+ * // Grab object element to test
+ * pTester.expectThat(actualValue).elementAt("myKey").equals("MyValue").assert();
+ * 
+ * // Grab nested element in array/object structure to test, in this case `actualValue[5].myKey[2].someChild`
+ * pTester.expectThat(actualValue).elementAt(5).elementAt("myKey").elementAt(2).elementAt("someChild").equals("MyValue").assert();
  */
-function Tester(pCollectionName) 
+Tester.prototype.elementAt = function(pKey)
 {
-    this.collectionName = pCollectionName;
-    this.testResults = [];
+    if(!Array.isArray(this.actualValue) && !Utils.isObject(this.actualValue))
+    {
+        var error = new Error("actualValue must be array or object to use elementAt('" + pKey + "')");
+        error.name = "UnitTest_lib Error";
+        throw error;
+    }
+    
+    var key = Array.isArray(this.actualValue) && pKey === -1 ? this.actualValue.length - 1 : pKey;
     
-    // if no assert was called during a test, no test-message would be added. -> Add a success message that no error occoured.
-    this.currentTestHadAlreadyAssert = false;
+    this.actualValueElementHierarchy.push(key);
+    this.actualValue = this.actualValue[key];
+    this.actualDisplayValue = this.actualDisplayValue[key];
+
+    return this;
 }
 
 /**
- * generates a summary of the test results
- * 
- * @return {Obect}
+ * Test if a value is equal to a given value
+ *
+ * @param {*} pExpect the value to compare
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.equals = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+
+    if(Utils.isObject(this.actualValue) || Utils.isObject(this.expectedValue))
+    {
+        this.expectedDisplayValue = JSON.stringify(this.actualValue);
+        this._testResult = Utils.isEqual(this.actualValue, this.expectedValue);
+        this._generateAssertDescription({custom: pCustomDescription, operator: "===", name: "Object value"});
+    }
+    else
+    {
+        this._testResult = this.actualValue === this.expectedValue;
+        this._generateAssertDescription({custom: pCustomDescription, operator: "==="});
+    }
+
+    return this;
+}
+
+/**
+ * Test if a value is greater than a given compare value
+ *
+ * @param {Number} pExpect the value to compare
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isGreater = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = this.actualValue > this.expectedValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: ">"});
+
+    return this;
+}
+
+/**
+ * Test if a value is greater or equal than a given compare value
+ *
+ * @param {Number} pExpect the value to compare
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isGreaterEqual = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = this.actualValue >= this.expectedValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: ">="});
+
+    return this;
+}
+
+/**
+ * Test if a value is lower than a given compare value
+ *
+ * @param {Number} pExpect the value to compare
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isLower = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = this.actualValue < this.expectedValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "<"});
+
+    return this;
+}
+
+/**
+ * Test if a value is lower or equal than a given compare value
+ *
+ * @param {Number} pExpect the value to compare
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isLowerEqual = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = this.actualValue <= this.expectedValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "<="});
+
+    return this;
+}
+
+/**
+ * Test if a value is a boolean
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isBoolean = function(pCustomDescription)
+{
+    this._setExpectValue("boolean");
+    this._testResult = Utils.isBoolean(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is a number
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isNumber = function(pCustomDescription)
+{
+    this._setExpectValue("number");
+    this._testResult = Utils.isNumber(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is numeric
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isNumeric = function(pCustomDescription)
+{
+    this._setExpectValue("numeric");
+    this._testResult = Utils.isNumeric(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is NaN (not a number)
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isNaN = function(pCustomDescription)
+{
+    this._setExpectValue("NaN");
+    this._testResult = Number.isNaN(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is an integer
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isInteger = function(pCustomDescription)
+{
+    this._setExpectValue("integer");
+    this._testResult = Utils.isInteger(this.actualValue);
+
+    this.actualDisplayValue = typeof this.actualValue;
+    this.expectedDisplayValue = "number";
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is", expected: this.expectedValue});
+
+    return this;
+}
+
+/**
+ * Test if a value is a float
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isFloat = function(pCustomDescription)
+{
+    this._setExpectValue("float");
+    this._testResult = Utils.isFloat(this.actualValue);
+
+    this.actualDisplayValue = typeof this.actualValue;
+    this.expectedDisplayValue = "number";
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is", expected: this.expectedValue});
+
+    return this;
+}
+
+/**
+ * Test if a value is a string
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isString = function(pCustomDescription)
+{
+    this._setExpectValue("string");
+    this._testResult = Utils.isString(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is an array
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isArray = function(pCustomDescription)
+{
+    this._setExpectValue("array");
+    this._testResult = Array.isArray(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is an object
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isObject = function(pCustomDescription)
+{
+    this._setExpectValue("object");
+    this._testResult = Utils.isObject(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is a function
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isFunction = function(pCustomDescription)
+{
+    this._setExpectValue("function");
+    this._testResult = Utils.isFunction(this.actualValue);
+    this.actualDisplayValue = typeof this.actualValue;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "type is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is null
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isNull = function(pCustomDescription)
+{
+    this._setExpectValue("null");
+    this._testResult = this.actualValue === null;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is"});
+
+    return this;
+}
+
+/**
+ * Test if a value is undefined
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isUndefined = function(pCustomDescription)
+{
+    this._setExpectValue("undefined");
+    this._testResult = this.actualValue === undefined || typeof this.actualValue === "undefined";
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is"});
+
+    return this;
+}
+
+/**
+ * Test if a string value is lowercase
+ *
+ * An additional check of the opposite must be made to prevent strings with non-letter characters passing the test
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isUpperCase = function(pCustomDescription)
+{
+    this._setExpectValue("uppercase");
+    this._testResult = Utils.isString(this.actualValue)
+                        && this.actualValue === this.actualValue.toUpperCase()
+                        && this.actualValue !== this.actualValue.toLowerCase()
+                        ;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is"});
+
+    return this;
+}
+
+/**
+ * Test if a string value is lowercase
+ *
+ * An additional check of the opposite must be made to prevent strings with non-letter characters passing the test
+ *
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isLowerCase = function(pCustomDescription)
+{
+    this._setExpectValue("lowercase");
+    this._testResult = Utils.isString(this.actualValue)
+                        && this.actualValue === this.actualValue.toLowerCase()
+                        && this.actualValue !== this.actualValue.toUpperCase()
+                        ;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is"});
+
+    return this;
+}
+
+/**
+ * Test if a value starts with a given string
+ *
+ * An additional check of the opposite must be made to prevent strings with non-letter characters passing the test
+ *
+ * @param {Number} pExpect the expected string beginning
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.startsWith = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = Utils.isString(this.actualValue) && this.actualValue.startsWith(this.expectedValue);
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "starts with"});
+
+    return this;
+}
+
+/**
+ * Test if a value end with a given string
+ *
+ * An additional check of the opposite must be made to prevent strings with non-letter characters passing the test
+ *
+ * @param {Number} pExpect the expected string beginning
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.endsWith = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+    this._testResult = Utils.isString(this.actualValue) && this.actualValue.endsWith(this.expectedValue);
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "ends with"});
+
+    return this;
+}
+
+/**
+ * Test if value is an instance of the expected "class"
+ *
+ * @param {Number} pExpect the expected instance name
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.isInstanceOf = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+
+    // Checks the name of the constructor and runs a function to check the actual instance.
+    // To prevent security issues by using `eval()`, a double nested function was needed.
+    // The outer one to evaluate the code, the inner one to be able to pass an argument, that will be evaluated.
+    // TODO: Also check for isObject() before
+    this._testResult = Utils.isObject(this.actualValue)
+                        && this.actualValue.constructor.name === this.expectedValue
+                        && Function('"use strict"; return function(obj) {return obj instanceof ' + this.expectedValue + ';};')()(this.actualValue);
+    this.actualDisplayValue = this.actualValue.constructor.name;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "is instance of"});
+
+    return this;
+}
+
+/**
+ * Test if value (array, string, object) has the expected length
+ *
+ * @param {Number} pExpect the expected length
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.hasLength = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+
+    var length = this._getLength(this.actualValue);
+    this._testResult = length === this.expectedValue;
+    this.actualDisplayValue = length;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "===", nameSuffix: ".length"});
+
+    return this;
+}
+
+/**
+ * Test if value (array, string, object) has a minimum expected length
+ *
+ * @param {Number} pExpect the expected length
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.hasMinLength = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+
+    var length = this._getLength(this.actualValue);
+    this._testResult = length >= this.expectedValue;
+    this.actualDisplayValue = length;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: ">=", nameSuffix: ".length"});
+
+    return this;
+}
+
+/**
+ * Test if value (array, string, object) has a maximum expected length
+ *
+ * @param {Number} pExpect the expected length
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
+ */
+Tester.prototype.hasMaxLength = function(pExpect, pCustomDescription)
+{
+    this._setExpectValue(pExpect);
+
+    var length = this._getLength(this.actualValue);
+    this._testResult = length <= this.expectedValue;
+    this.actualDisplayValue = length;
+
+    this._generateAssertDescription({custom: pCustomDescription, operator: "<=", nameSuffix: ".length"});
+
+    return this;
+}
+
+
+/**
+ * Test if a callback function throws an exception
+ *
+ * @param {Number} pExpect the expected error
+ * @param {(String|Object)} pCustomDescription an optional custom assert description or config object overwrite
+ * @return {Tester}
  */
-Tester.prototype.getSummary = function ()
+Tester.prototype.throwsException = function(pExpect, pCustomDescription)
 {
-    var summary = {
-        failures : 0,
-        successes : 0,
-        failedTests : [],
-        getMessage : function ()
+    this._setExpectValue(pExpect);
+    this._generateAssertDescription({custom: pCustomDescription, operator: "throws", expected: "exception", name: " "});
+
+    if(this._useNegation)
+    {
+        try
+        {
+            this.actualValue();
+        }
+        catch(e)
+        {
+            this._testResult = true; // Actually `false` and therefore failed, but must be `true` to make negation work
+            this.actualDisplayValue = e;
+            throw e;
+        }
+
+        this._testResult = false; // Actually `true` and therefore successful, but must be `false` to make negation work
+    }
+    else
+    {
+        try
         {
-            var message = "-------------------------\n"
-                + (this.failures ? "Test failure" : "Test success")
-                + "\n-------------------------\nTests performed: " + (this.successes + this.failures)
-                + "\nTests successful: " + this.successes
-                + "\nTests failed: " + this.failures;
-            if (this.failedTests.length)
+            this.actualValue();
+        }
+        catch(e)
+        {
+            this._testResult = e.toSource() === this.expectedValue.toSource();
+
+            if (!this._testResult)
             {
-                message += "\nFailures:";
-                this.failedTests.forEach(function (testName)
-                {
-                    message += "\n\t" + testName;
-                });
+                this.actualDisplayValue = e;
+                throw e;
             }
-            return message;
         }
     }
-    this.testResults.forEach(function ([testName,, successful])
-    {
-        if (successful)
-            summary.successes++;
+
+    return this;
+}
+
+/**
+ * Executes all tests in a TestSuite
+ *
+ * @param {TestSuite} pTestSuite the TestSuite which should be tested
+ * @return {void}
+ */
+Tester.prototype.test = function(pTestSuite)
+{
+    this.currentTestSuite = pTestSuite;
+    this._log("important", "\u2692 Suite: " + this.currentTestSuite.name);
+    this._registerMethodTestCall();
+
+    this.currentTestSuite.preAll(this);
+
+    this.currentTestSuite.tests.forEach(function(pTest) {
+        this._resetTest();
+
+        this.currentTestSuite.preTest(this);
+        this.dataProvider = pTest.dataProviderCallback();
+
+        if(Array.isArray(this.dataProvider) && this.dataProvider.length > 0)
+        {
+            this.dataProvider.forEach(function(pElement, pIndex) {
+                this._runTest(pTest, pIndex);
+            }, this);
+        }
         else
         {
-            summary.failures++;
-            summary.failedTests.push(testName);
+            this._runTest(pTest);
         }
+
+        this.currentTestSuite.postTest(this);
+    }, this);
+
+    this.currentTestSuite.postAll(this);
+}
+
+/**
+ * Calculate test duration
+ *
+ * Will we used in the results and summary output.
+ *
+ * @return {Number} the duration in milliseconds
+ */
+Tester.prototype.getDuration = function()
+{
+    if(this.endTime === null)
+    {
+        this.endTime = new Date().getTime();
+    }
+
+    return this.endTime - this.startTime;
+}
+
+/**
+ * Initializes test coverage by registering available and testable methods of given class
+ *
+ * @param {Object=} pInstanceName an optional class instance to make test coverage work
+ * @return {void}
+ */
+Tester.prototype.initCoverage = function(pInstanceName)
+{
+    if(pInstanceName !== undefined)
+    {
+        this.instanceName = pInstanceName;
+
+        var instanceMethods = Object.getOwnPropertyNames(this.instanceName.prototype).filter(function(property) {
+            return typeof this.instanceName.prototype[property] === 'function';
+        }, this);
+        var staticMethods = Object.getOwnPropertyNames(this.instanceName).filter(function(property) {
+            return typeof this.instanceName[property] === 'function';
+        }, this);
+
+        this.methods = instanceMethods.concat(staticMethods);
+    }
+}
+
+/**
+ * Get the results of current tester as object
+ *
+ * @return {Object} the test results
+ */
+Tester.prototype.getResults = function()
+{
+    return {
+        testCount: this.testCount,
+        assertCount: this.successCount + this.failCount,
+        successCount: this.successCount,
+        failCount: this.failCount,
+        duration: this.getDuration(),
+        coverage: this._getCoverageRate(),
+        methodsTotal: this.methods.length,
+        methodsCalled: this.methodsCalled.length
+    };
+}
+
+/**
+ * Output the summary for a single or multiple test suites to the server log
+ *
+ * The results of multiple tests get cumulated.
+ * There will be no log output if it has been disabled.
+ *
+ * @param {(Object|Object[])} pTestSuiteResults the test suite result(s) to log a summary for
+ * @return {void}
+ */
+Tester.prototype.summary = function(pTestSuiteResults)
+{
+    var testSuiteResults = pTestSuiteResults !== undefined && Array.isArray(pTestSuiteResults) ? pTestSuiteResults : [this.getResults()];
+    var divider = testSuiteResults.length > 1 ? "=" : "-";
+    var title = testSuiteResults.length > 1 ? "All Tests" : this.collectionName;
+
+    var testCount = 0;
+    var assertCount = 0;
+    var successCount = 0;
+    var failCount = 0;
+    var duration = 0;
+
+    testSuiteResults.forEach(function(pTestResult) {
+        testCount += pTestResult.testCount;
+        assertCount += pTestResult.assertCount;
+        successCount += pTestResult.successCount;
+        failCount += pTestResult.failCount;
+        duration += pTestResult.duration;
     });
-    return summary;
+
+    this._log("info", "\n" + title + " Summary:");
+    this._log("info", divider.repeat(32));
+    this._log("", "Total:    " + this.t.important(testCount + " Tests, " + assertCount + " Assertions"));
+    this._log("", "Passed:   " + (successCount === assertCount ? this.t.success(successCount) : this.t.warning(successCount)));
+    this._log("", "Failed:   " + (failCount === 0 ? this.t.success(failCount) : this.t.error(failCount)));
+    this._log("", "Time:     " + this.t.important(duration + "ms"));
+    this._log("", "Coverage: " + this._getCoverageOutput(testSuiteResults));
+    this._log("", "\n\n");
+
+    this._print();
 }
 
 /**
- * With assert you can test if a variable is the same like an expected value.<br/>
- * The test result is added to the Tester<br/>
+ * Write a certain message type to the output
+ *
+ * If the given message type is not valid/allowed, the output will be plain unformatted text.
+ *
+ * @param {String} pType the message type
+ * @param {String} pOutput the actual message
+ * @return {void}
  * 
- * Note: the values are compared by === so also the type is checked.<br/>
- * By using assert you can Test everything you want.<br/>
+ * @private
+ */
+Tester.prototype._log = function(pType, pOutput)
+{
+    var allowedTerminalMethods = ["debug", "success", "info", "error", "warning", "important"];
+
+    if(allowedTerminalMethods.indexOf(pType) !== -1)
+    {
+        this.output += "\n" + this.t[pType](pOutput);
+    }
+    else
+    {
+        this.output += "\n" + pOutput;
+    }
+}
+
+/**
+ * Set the value to test against
+ *
+ * @param {*} pValue
+ * @return {void}
  * 
- * @param {AnyValue} pExpect the expected value
- * @param {AnyValue} pActual the actual value
- * @param {String} [pTestDescription] this is an optional description. You should use it, if you have more than one .assert in your test.
+ * @private
  */
-Tester.prototype.assert = function (pExpect, pActual, pTestDescription)
+Tester.prototype._setExpectValue = function(pValue)
 {
-    this.currentTestHadAlreadyAssert = true;
-    
-    var res = pActual === pExpect;
+    this.expectedValue = pValue;
+    this.expectedDisplayValue = pValue;
+}
+
+/**
+ * Determine, whether an error is expected in the current test run
+ *
+ * @return {Boolean}
+ * 
+ * @private
+ */
+Tester.prototype._testExpectsError = function()
+{
+    return this.expectedValue instanceof Error;
+}
+
+/**
+ * Write the ouput to the log and reset the output to an empty string
+ *
+ * @return {Tester}
+ * 
+ * @private
+ */
+Tester.prototype._print = function()
+{
+    if(this.outputEnabled)
+    {
+        logging.log(this.output);
+    }
     
-    this.testResults.push([
-        this._currentTest,
-        pTestDescription,
-        res,
-        pExpect,
-        pActual
-    ]);
+    this.output = "";
+    return this;
 }
 
 /**
- * Executes all tests in a TestSuite
- * @param {TestSuite} pTestSuite the TestSuite which should be tested
+ * Generates and sets an assert description by given config object or simply set a given custom string
+ *
+ * It also handles using the correct operators when negation via `not()` is used
+ *
+ * @param {Object} pDescriptionConfig a config object
+ * @return {void}
+ * 
+ * @private
  */
-Tester.prototype.test = function (pTestSuite)
+Tester.prototype._generateAssertDescription = function(pDescriptionConfig)
 {
-    pTestSuite.preAll(this);
+    if(Utils.isString(pDescriptionConfig.custom))
+    {
+        this._assertDescription = pDescriptionConfig.custom;
+        return;
+    }
     
-    pTestSuite.tests.forEach(function(pTest) {
-        logging.log("Testing " + pTest[0]);
-        
-        this.currentTestHadAlreadyAssert = false;
-
-        this._currentTest = pTest[0];
-        pTestSuite.preTest(this);
-        
-        if (pTest.length >= 3)
+    Object.assign(pDescriptionConfig, pDescriptionConfig.custom);
+    var valueName = pDescriptionConfig.name || "value";
+    var valueNameSuffix = pDescriptionConfig.nameSuffix || "";
+    var operator = pDescriptionConfig.operator.trim();
+    var expectedValue = pDescriptionConfig.expected || this.expectedDisplayValue;
+    expectedValue = Utils.isString(expectedValue) ? "`" + expectedValue + "`" : expectedValue + "";
+
+    if(this._useNegation)
+    {
+        switch(operator)
         {
-            var errorThrowed = false;
-            
-            try {
-                pTest[1](this);
-            } catch (ex) {
-                errorThrowed = true;
-                
-                if (ex.toSource() == pTest[2].toSource())
-                {
-                    this.testResults.push([
-                        this._currentTest,
-                        "Expected error throwed",
-                        true,
-                        pTest[2].toSource(),
-                        ex.toSource()
-                    ]);
-
-                    // don't throw as it's expected and no detailed stacktrace is needed
-                }
-                else
-                {
-                    this.testResults.push([
-                        this._currentTest,
-                        "Expected error throwed",
-                        false,
-                        pTest[2].toSource(),
-                        ex.toSource()
-                    ]);
-                    
-                    logging.log("Expected error: " + pTest[2].toSource() + ", but wrong error occoured.\nRunning test again without try catch to get detailed error stack trace")
-                    
-                    pTestSuite.preTest(this);
-                    pTest[1](this);
-                    pTestSuite.postTest(this);
-                    return;
-                }
-            }
-            
-            if (!errorThrowed)
-            {
-                // if it didn't fail
-                this.testResults.push([
-                    this._currentTest,
-                    "Expected error throwed",
-                    false,
-                    pTest[2].toSource(),
-                    "no error"
-                ]);
-            }
+            case "===":
+                operator = "!==";
+                break;
+            case ">":
+                operator = "<=";
+                break;
+            case ">=":
+                operator = "<";
+                break;
+            case "<":
+                operator = ">=";
+                break;
+            case "<=":
+                operator = ">";
+                break;
+            case "is":
+            case "type is":
+            case "has":
+                operator += " not";
+                break;
+            case "is instance of":
+                operator += "is not instance of";
+                break;
+            case "starts with":
+            case "ends with":
+            case "throws":
+            default:
+                operator = "not " + operator;
+                break;
         }
-        else
+    }
+
+    this.actualValueElementHierarchy.forEach(function(pElement) {
+        valueName += typeof pElement === "number" ? "[" + pElement + "]" : "." + pElement;
+    }, this);
+    
+    var description = valueName + valueNameSuffix + " " + operator + " " + expectedValue;
+    this._assertDescription = description.trim();
+    this.actualValueElementHierarchy = [];
+}
+
+/**
+ * Reset test-related properties to their initial state
+ *
+ * This is done before every test run to prevent side-effects or expired/invalid data and states
+ *
+ * @return {Tester}
+ * 
+ * @private
+ */
+Tester.prototype._resetTest = function()
+{
+    this.actualValue = undefined;
+    this.actualDisplayValue = undefined;
+    this.actualOriginalValue = undefined;
+    this.actualOriginalDisplayValue = undefined;
+    this.actualValueElementHierarchy = [];
+    this.expectedValue = undefined;
+    this.expectedDisplayValue = undefined;
+    this._assertDescription = "";
+    this._testResult = false;
+    this._useNegation = false;
+
+    return this;
+}
+
+/**
+ * Get the length of a given value or undefined if it is not an array, object or string
+ *
+ * @param {(Array|Object|String)} pActualValue the value to get the length from
+ * @return {Number}
+ * 
+ * @private
+ */
+Tester.prototype._getLength = function(pActualValue)
+{
+    if(Array.isArray(pActualValue) || Utils.isString(pActualValue))
+    {
+        return pActualValue.length;
+    }
+    else if(Utils.isObject(pActualValue))
+    {
+        return Object.keys(pActualValue).length;
+    }
+
+    return undefined;
+}
+
+/**
+ * Get the percentage of test coverage
+ *
+ * Will we used in the results and summary output.
+ *
+ * @return {?Number} the duration in milliseconds
+ */
+Tester.prototype._getCoverageRate = function()
+{
+    if(this.methods.length === 0 && this.methodsCalled.length === 0)
+    {
+        return null;
+    }
+
+    if(this.methods.length === 0 || this.methodsCalled.length === 0)
+    {
+        return 0.00;
+    }
+
+    var coverage = (this.methodsCalled.length / this.methods.length) * 100;
+    coverage = Number.parseFloat(Number.parseFloat(coverage).toFixed(2));
+
+    return coverage > 100 ? 100.00 : coverage;
+}
+
+/**
+ * Register, which method of the given class has been called
+ *
+ * It takes the name of the TestSuite and splits it to get the class name and the method.
+ * Then, it checks if the class has such a method. It gets added to the call-list, if it has not been added yet.
+ * This is necessary to calculate the test coverage.
+ * All available class methods get registered beforehand and will be compared to this list later in `_getCoverageRate()`.
+ *
+ * @return {void}
+ * 
+ * @private
+ */
+Tester.prototype._registerMethodTestCall = function()
+{
+    if(this.instanceName !== undefined)
+    {
+        var callParts = this.currentTestSuite.name.split(".");
+
+        if(callParts[0] === this.instanceName.prototype.constructor.name && this.methods.indexOf(callParts[1]) !== -1)
         {
-            // run without try catch as no error expected
-            pTest[1](this);
+            if(this.methodsCalled.indexOf(callParts[1]) === -1)
+            {
+                this.methodsCalled.push(callParts[1]);
+            }
         }
-        
-        if (!this.currentTestHadAlreadyAssert && pTest.length < 3) // only add message if no error expected
-        {
-            this.testResults.push([
-                    this._currentTest,
-                    "Expected no error",
-                    true,
-                    "no error",
-                    "no error"
-                ]);
+    }
+}
+
+/**
+ * Renders the output for the test coverage
+ *
+ * The results of multiple tests get cumulated.
+ *
+ * @param {Object[]} pTestSuiteResults the test suite(s) to log a summary for
+ * @return {String} the test coverage output
+ */
+Tester.prototype._getCoverageOutput = function(pTestSuiteResults)
+{
+    var coverageOutput = this.t.debug("unknown");
+    var methodsTotal = 0;
+    var methodsCalled = 0;
+    var coverage = null;
+    var avgCoverage = null;
+    var coverageMethodCount = null;
+
+    pTestSuiteResults.forEach(function(pTestResult) {
+        methodsTotal += pTestResult.methodsTotal;
+        methodsCalled += pTestResult.methodsCalled;
+
+        if(pTestResult.coverage !== null) {
+            coverage += pTestResult.coverage;
         }
-            
-        pTestSuite.postTest(this);
-    }, this);
-    
-    pTestSuite.postAll(this);
+    });
+
+    if(coverage !== null)
+    {
+        avgCoverage = coverage / pTestSuiteResults.length;
+        coverageMethodCount = "(" + methodsCalled + "/" + methodsTotal + ")";
+    }
+
+    if(avgCoverage >= 80)
+    {
+        coverageOutput = this.t.success(avgCoverage + "% " + coverageMethodCount);
+    }
+    else if(avgCoverage >= 50)
+    {
+        coverageOutput = this.t.warning(avgCoverage + "% " + coverageMethodCount);
+    }
+    else if(avgCoverage > 0)
+    {
+        coverageOutput = this.t.error(avgCoverage + "% " + coverageMethodCount);
+    }
+
+    return coverageOutput;
 }
 
 /**
- * Prints the test results to the log
+ * Generates the title for the test about to run
+ *
+ * @param {Test} pTest the test to run
+ * @param {Number} pDataProviderIndex the current index of the data provider
+ * @return {void}
+ * 
+ * @private
  */
-Tester.prototype.printResults = function ()
+Tester.prototype._generateTestTitle = function(pTest, pDataProviderIndex)
 {
-    var lastTestDescription = "";
-    logging.log("Test results for \"" + this.collectionName + "\"")
-    this.testResults.forEach(function(pResult) {
-        
-        if (lastTestDescription != pResult[0])
+    if(pDataProviderIndex !== undefined)
+    {
+        var titleValues = [];
+
+        this.dataProvider[pDataProviderIndex].forEach(function(pElement) {
+            titleValues.push(pElement === undefined ? "undefined" : JSON.stringify(pElement));
+        });
+        this._log("", this.t.info("\t\u2699 Test: " + pTest.name) + this.t.debug(" (#" + (pDataProviderIndex + 1) + ": " + titleValues.join(" | ") + ")"));
+    }
+    else
+    {
+        this._log("info", "\t\u2699 Test: " + pTest.name);
+    }
+}
+
+/**
+ * Runs a single test iteration and handles errors
+ *
+ * @param {Test} pTest the test to run
+ * @param {Number} pDataProviderIndex the current index of the data provider
+ * @return {void}
+ * 
+ * @private
+ */
+Tester.prototype._runTest = function(pTest, pDataProviderIndex)
+{
+    this._generateTestTitle(pTest, pDataProviderIndex);
+    this.testCount++;
+
+    var dataProvider = pDataProviderIndex !== undefined ? this.dataProvider[pDataProviderIndex] : undefined;
+
+    try
+    {
+        pTest.callback(this, dataProvider);
+    }
+    catch (e)
+    {
+        this.assert();
+
+        // Expected, but wrong error
+        if (this._testExpectsError() && e.toSource() !== this.expectedValue.toSource())
         {
-            logging.log("Test \"" + pResult[0] + "\":");
+            this._log("error", "Wrong error occurred");
+            this._log("warning", "\t\t expected: " + this.expectedValue.toSource());
+            this._log("error", "\t\t actual:   " + e.toSource());
         }
-        
-        var message = (pResult[1] ? " - " + pResult[1] : " - Test result ") + ": " + (pResult[2] ? "success" : "fail\nexpected: " + JSON.stringify(pResult[3], null, "\t") + "\nactual: " + JSON.stringify(pResult[4], null, "\t"));
-        
-        logging.log(message);
-        lastTestDescription = pResult[0];
-    }, this);
-    logging.log(this.getSummary().getMessage());
-}
\ No newline at end of file
+        else
+        { // Unexpected error
+            this._log("error", "Unexpected error occurred");
+        }
+
+        var exception = logging.toLogString(e["rhinoException"] !== undefined ? e["rhinoException"] : e, true);
+        this._log("error", e.name + ":\n" + exception.replace(/\s+(at|\[->\])\s/g, "\n\t$1 "));
+        this._log("error", "jDito callstack:\n" + e.stack);
+
+        if(pTest.rerunOnError)
+        {
+            this._log("debug", "Rerun without try-catch to get detailed error stack trace");
+            this._print();
+
+            this.currentTestSuite.preTest(this);
+            pTest.callback(this, dataProvider);
+            this.currentTestSuite.postTest(this);
+        }
+    }
+}
diff --git a/process/UnitTest_test/UnitTest_test.aod b/process/UnitTest_test/UnitTest_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..095a395529bdac348b397a4854c6939cccab89f3
--- /dev/null
+++ b/process/UnitTest_test/UnitTest_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>UnitTest_test</name>
+  <title>[TEST] UnitTest_lib</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/UnitTest_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/UnitTest_test/process.js b/process/UnitTest_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..7d80b7995e42cc7b6bc74c8b3f417f2ce3ae0329
--- /dev/null
+++ b/process/UnitTest_test/process.js
@@ -0,0 +1,1141 @@
+import("system.result");
+import("system.vars");
+import("Keyword_lib");
+import("UnitTest_lib");
+
+
+var equals = new TestSuite("Tester.equals", [
+    new Test("should test value equality",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).equals(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            var num = 0;
+            var str = '0';
+            var objStr = new String('0');
+            var obj_1 = {a: 1, b: 2, c: 3};
+            var obj_2 = {x: null, y: "test", z: true};
+            
+            return [
+                [true,  num,     num],
+                [true,  str,     str],
+                [true,  true,    true],
+                [true,  objStr,  objStr],
+                [true,  obj_1,   obj_1],
+                [true,  obj_2,   obj_2],
+                [false, true,    false],
+                [false, obj_1,   obj_2],
+                [false, num,     objStr],
+                [false, num,     str],
+                [false, objStr,  str],
+                [false, null,    undefined],
+                [false, objStr,  null],
+                [false, objStr,  undefined],
+                [false, null,    "null"],
+            ];
+        }
+    )
+]);
+
+var isGreater = new TestSuite("Tester.isGreater", [
+    new Test("should test if value is greater than another",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isGreater(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  1,    0],
+                [true,  100,  10],
+                [false, 3,    3],
+                [false, 0,    5],
+                [false, 1234, 5678],
+            ];
+        }
+    )
+]);
+
+var isGreaterEqual = new TestSuite("Tester.isGreaterEqual", [
+    new Test("should test if value is greater or equal than another",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isGreaterEqual(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  1,    0],
+                [true,  101,  100],
+                [true,  3,    3],
+                [false, 0,    5],
+                [false, 1234, 5678],
+            ];
+        }
+    )
+]);
+
+var isLower = new TestSuite("Tester.isLower", [
+    new Test("should test if value is lower than another",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isLower(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  0,    1],
+                [true,  10,   100],
+                [false, 3,    3],
+                [false, 5,    0],
+                [false, 5678, 1234],
+            ];
+        }
+    )
+]);
+
+var isLowerEqual = new TestSuite("Tester.isLowerEqual", [
+    new Test("should test if value is greater or equal than another",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isLowerEqual(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  0,    1],
+                [true,  100,  101],
+                [true,  3,    3],
+                [false, 5,    0],
+                [false, 5678, 1234],
+            ];
+        }
+    )
+]);
+
+var isBoolean = new TestSuite("Tester.isBoolean", [
+    new Test("should test if value is a boolean",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isBoolean().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  true],
+                [true,  false],
+                [false, "true"],
+                [false, "false"],
+                [false, ""],
+                [false, "0"],
+                [false, 0],
+                [false, new Object()],
+                [false, []],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isNumber = new TestSuite("Tester.isNumber", [
+    new Test("should test if value is a number",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isNumber().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  0],
+                [true,  100],
+                [true,  33.33333],
+                [true,  0xFFFFFF],
+                [true,  0777], // Octal
+                //[true,  0o777], // New octal format since ECMAScript 2015
+                //[true,  0b11111111], // Seems like binary numbers are not supported
+                [true,  2e6], // Exponentiation
+                [true,  0.1e2], // Exponentiation
+                [true,  +Infinity],
+                [true,  -Infinity],
+                [true,  NaN], // This should actually fail. Needs fix in Utils
+                [false, ""],
+                [false, "0"],
+                [false, new Object()],
+                [false, []],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isNumeric = new TestSuite("Tester.isNumeric", [
+    new Test("should test if value is numeric",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isNumeric().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  0],
+                [true,  100],
+                [true,  33.33333],
+                [true,  "0777"], // Octal
+                [true,  "2e6"], // Exponentiation
+                [true,  "0.1e2"], // Exponentiation
+                [true,  +Infinity],
+                [true,  -Infinity],
+                [true,  NaN], // This should actually fail. Needs fix in Utils
+                [true,  "50000"],
+                [true,  "0"],
+                [false, ""],
+                [false, new Object()],
+                [false, []],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isNotANumber = new TestSuite("Tester.isNaN", [
+    new Test("should test if value is not NaN",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isNaN().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  NaN],
+                [false, 0],
+                [false, 100],
+                [false, 33.33333],
+                [false, "0777"], // Octal
+                [false, "2e6"], // Exponentiation
+                [false, "0.1e2"], // Exponentiation
+                [false, +Infinity],
+                [false, -Infinity],
+                [false, "50000"],
+                [false, "0"],
+                [false,  ""],
+                [false,  new Object()],
+                [false,  []],
+                [false,  true],
+                [false,  null],
+                [false,  undefined],
+            ];
+        }
+    )
+]);
+
+var isInteger = new TestSuite("Tester.isInteger", [
+    new Test("should test if value is an integer",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isInteger().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  0],
+                [true,  100],
+                [true,  0xFFFFFF],
+                [true,  0777], // Octal
+                [true,  2e6], // Exponentiation
+                [true,  0.1e2], // Exponentiation
+                [true,  "0"],
+                [false, 33.33333],
+                [false, +Infinity],
+                [false, -Infinity],
+                [false, NaN],
+                [false, ""],
+                [false, new Object()],
+                [false, []],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isFloat = new TestSuite("Tester.isFloat", [
+    new Test("should test if value is a float",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isFloat().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  33.33333],
+                [false, 0],
+                [false, 100],
+                [false, 0xFFFFFF],
+                [false, 0777], // Octal
+                [false, 2e6], // Exponentiation
+                [false, 0.1e2], // Exponentiation
+                [false, +Infinity],
+                [false, -Infinity],
+                [false, NaN],
+                [false, ""],
+                [false, "0"],
+                [false, new Object()],
+                [false, []],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isString = new TestSuite("Tester.isString", [
+    new Test("should test if value is a string",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isString().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  ""],
+                [true,  "Hello"],
+                [true,  "0"],
+                [false, new String('Hello')],
+                [false, []],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isArray = new TestSuite("Tester.isArray", [
+    new Test("should test if value is an array",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isArray().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  []],
+                [true,  ["a", 2, null]],
+                [true,  new Array()],
+                [false, ""],
+                [false, "0"],
+                [false, new Object()],
+                [false, new Function()],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isObject = new TestSuite("Tester.isObject", [
+    new Test("should test if value is an object",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isObject().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  {}],
+                [true,  []],
+                [true,  ["a", 2, null]],
+                [true,  new Array()],
+                [true,  new Object()],
+                [true,  new String("hello")],
+                [false, new Function()],
+                [false, ""],
+                [false, "0"],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isFunction = new TestSuite("Tester.isFunction", [
+    new Test("should test if value is a function",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isFunction().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  new Function()],
+                [true,  function(){}],
+                [true,  Math.sin],
+                [true,  Array.isArray],
+                [false, new Array()],
+                [false, new Object()],
+                [false, ""],
+                [false, "0"],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isNull = new TestSuite("Tester.isNull", [
+    new Test("should test if value is null",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isNull().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  null],
+                [false, ""],
+                [false, "Hello"],
+                [false, "0"],
+                [false, new String('Hello')],
+                [false, []],
+                [false, 0],
+                [false, true],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isUndefined = new TestSuite("Tester.isUndefined", [
+    new Test("should test if value is undefined",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isUndefined().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  undefined],
+                [false, ""],
+                [false, "Hello"],
+                [false, "0"],
+                [false, new String('Hello')],
+                [false, []],
+                [false, 0],
+                [false, true],
+                [false, null],
+            ];
+        }
+    )
+]);
+
+var isUpperCase = new TestSuite("Tester.isUpperCase", [
+    new Test("should test if value is an uppercase string",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isUpperCase().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "HELLO"],
+                [false, "HeLlo"],
+                [false, "hello"],
+                [false, "0"],
+                [false, new String('Hello')],
+                [false, []],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var isLowerCase = new TestSuite("Tester.isLowerCase", [
+    new Test("should test if value is a lowercase string",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isLowerCase().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "hello"],
+                [false, "HELLO"],
+                [false, "HeLlo"],
+                [false, "0"],
+                [false, new String('Hello')],
+                [false, []],
+                [false, 0],
+                [false, true],
+                [false, null],
+                [false, undefined],
+            ];
+        }
+    )
+]);
+
+var startsWith = new TestSuite("Tester.startsWith", [
+    new Test("should test if value starts with a given string",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).startsWith(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "Millenium Falcon", "Mill"],
+                [false, "Millenium Falcon", "MILL"],
+                [false, "Millenium Falcon", "Abc"],
+                [false, "Millenium Falcon", "0"],
+            ];
+        }
+    )
+]);
+
+var endsWith = new TestSuite("Tester.endsWith", [
+    new Test("should test if value ends with a given string",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).endsWith(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "Millenium Falcon", "con"],
+                [false, "Millenium Falcon", "CON"],
+                [false, "Millenium Falcon", "Abc"],
+                [false, "Millenium Falcon", "0"],
+            ];
+        }
+    )
+]);
+
+var isInstanceOf = new TestSuite("Tester.isInstanceOf", [
+    new Test("should test if value is an instance of given class",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).isInstanceOf(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  new Tester(), "Tester"],
+                [true,  [],           "Array"],
+                [true,  {},           "Object"],
+                [false, "abc",        "String"],
+                [false, true,         "Boolean"],
+                [false, 0,            "Number"],
+            ];
+        }
+    )
+]);
+
+var hasLength = new TestSuite("Tester.hasLength", [
+    new Test("should test if value has the expected length",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).hasLength(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "",               0],
+                [true,  [],               0],
+                [true,  {},               0],
+                [true,  "abc",            3],
+                [true,  ["a", "z"],       2],
+                [true,  new String("?!"), 2],
+                [false, 0,                1],
+                [false, true,             1],
+                [false, null,             1],
+                [false, undefined,        1],
+            ];
+        }
+    )
+]);
+
+
+var hasMinLength = new TestSuite("Tester.hasMinLength", [
+    new Test("should test if value has the expected minimum length",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).hasMinLength(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "",               0],
+                [true,  [],               0],
+                [true,  {},               0],
+                [true,  "abc",            3],
+                [true,  "abc",            2],
+                [true,  ["a", "z"],       2],
+                [true,  ["a", "z"],       1],
+                [true,  new String("?!"), 2],
+                [false, "abc",            4],
+                [false, ["a", "z"],       5],
+                [false, 0,                1],
+                [false, true,             1],
+                [false, null,             1],
+                [false, undefined,        1],
+            ];
+        }
+    )
+]);
+
+
+var hasMaxLength = new TestSuite("Tester.hasMaxLength", [
+    new Test("should test if value has the expected maximum length",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).hasMaxLength(pDataProvider[2]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "",               0],
+                [true,  [],               0],
+                [true,  {},               0],
+                [true,  "abc",            3],
+                [true,  "abc",            4],
+                [true,  ["a", "z"],       2],
+                [true,  ["a", "z"],       3],
+                [true,  new String("?!"), 2],
+                [false, "abc",            2],
+                [false, ["a", "z"],       1],
+                [false, 0,                1],
+                [false, true,             1],
+                [false, null,             1],
+                [false, undefined,        1],
+            ];
+        }
+    )
+]);
+
+var not = new TestSuite("Tester.not", [
+    new Test("should test negation of compare tests",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var method = pDataProvider[1];
+            var actualValue = dummyTester.expectThat(pDataProvider[2]).not()[method](pDataProvider[3]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            var num = 0;
+            var str = '0';
+            var objStr = new String('0');
+            var obj_1 = {a: 1, b: 2, c: 3};
+            var obj_2 = {x: null, y: "test", z: true};
+            
+            return [
+                [true,  "equals",         true,               false],
+                [true,  "equals",         obj_1,              obj_2],
+                [true,  "equals",         num,                objStr],
+                [false, "equals",         num,                num],
+                [false, "equals",         str,                str],
+                [false, "equals",         true,               true],
+                [true,  "isGreater",      3,                  3],
+                [true,  "isGreater",      0,                  5],
+                [true,  "isGreater",      1234,               5678],
+                [false, "isGreater",      1,                  0],
+                [false, "isGreater",      100,                10],
+                [true,  "isGreaterEqual", 0,                  5],
+                [true,  "isGreaterEqual", 1234,               5678],
+                [false, "isGreaterEqual", 1,                  0],
+                [false, "isGreaterEqual", 101,                100],
+                [false, "isGreaterEqual", 3,                  3],
+                [true,  "isLower",        3,                  3],
+                [true,  "isLower",        5,                  0],
+                [true,  "isLower",        5678,               1234],
+                [false, "isLower",        0,                  1],
+                [false, "isLower",        10,                 100],
+                [true,  "isLowerEqual",   5,                  0],
+                [true,  "isLowerEqual",   5678,               1234],
+                [false, "isLowerEqual",   0,                  1],
+                [false, "isLowerEqual",   100,                101],
+                [false, "isLowerEqual",   3,                  3],
+                [true,  "startsWith",     "Millenium Falcon", "MILL"],
+                [true,  "startsWith",     "Millenium Falcon", "Abc"],
+                [true,  "startsWith",     "Millenium Falcon", "0"],
+                [false, "startsWith",     "Millenium Falcon", "Mill"],
+                [true,  "endsWith",       "Millenium Falcon", "CON"],
+                [true,  "endsWith",       "Millenium Falcon", "Abc"],
+                [true,  "endsWith",       "Millenium Falcon", "0"],
+                [false, "endsWith",       "Millenium Falcon", "con"],
+                [true,  "isInstanceOf",   "abc",              "String"],
+                [true,  "isInstanceOf",   true,               "Boolean"],
+                [true,  "isInstanceOf",   0,                  "Number"],
+                [false, "isInstanceOf",   new Tester(),       "Tester"],
+                [false, "isInstanceOf",   [],                 "Array"],
+                [false, "isInstanceOf",   {},                 "Object"],
+                [true,  "hasLength",      0,                  1],
+                [true,  "hasLength",      true,               1],
+                [true,  "hasLength",      null,               1],
+                [true,  "hasLength",      undefined,          1],
+                [false, "hasLength",      "",                 0],
+                [false, "hasLength",      [],                 0],
+                [false, "hasLength",      {},                 0],
+                [false, "hasLength",      "abc",              3],
+                [false, "hasLength",      ["a", "z"],         2],
+                [false, "hasLength",      new String("?!"),   2],
+            ];
+        }
+    ),
+    new Test("should test negation of type tests",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var method = pDataProvider[1];
+            var actualValue = dummyTester.expectThat(pDataProvider[2]).not()[method]().getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  "isBoolean",   "true"],
+                [true,  "isBoolean",   "false"],
+                [true,  "isBoolean",   ""],
+                [true,  "isBoolean",   "0"],
+                [true,  "isBoolean",   0],
+                [true,  "isBoolean",   new Object()],
+                [true,  "isBoolean",   []],
+                [true,  "isBoolean",   null],
+                [true,  "isBoolean",   undefined],
+                [false, "isBoolean",   true],
+                [false, "isBoolean",   false],
+                [true,  "isNumber",    ""],
+                [true,  "isNumber",    "0"],
+                [true,  "isNumber",    new Object()],
+                [true,  "isNumber",    []],
+                [true,  "isNumber",    true],
+                [true,  "isNumber",    null],
+                [true,  "isNumber",    undefined],
+                [false, "isNumber",    0],
+                [false, "isNumber",    100],
+                [false, "isNumber",    33.33333],
+                [false, "isNumber",    0xFFFFFF],
+                [false, "isNumber",    0777], // Octal
+                [false, "isNumber",    2e6], // Exponentiation
+                [false, "isNumber",    0.1e2], // Exponentiation
+                [false, "isNumber",    +Infinity],
+                [false, "isNumber",    -Infinity],
+                [false, "isNumber",    NaN],
+                [true,  "isNumeric",   ""],
+                [true,  "isNumeric",   new Object()],
+                [true,  "isNumeric",   []],
+                [true,  "isNumeric",   true],
+                [true,  "isNumeric",   null],
+                [true,  "isNumeric",   undefined],
+                [false, "isNumeric",   0],
+                [false, "isNumeric",   100],
+                [false, "isNumeric",   33.33333],
+                [false, "isNumeric",   "0777"], // Octal
+                [false, "isNumeric",   "2e6"], // Exponentiation
+                [false, "isNumeric",   "0.1e2"], // Exponentiation
+                [false, "isNumeric",   +Infinity],
+                [false, "isNumeric",   -Infinity],
+                [false, "isNumeric",   NaN],
+                [false, "isNumeric",   "50000"],
+                [false, "isNumeric",   "0"],
+                [true,  "isNaN",       0],
+                [true,  "isNaN",       100],
+                [true,  "isNaN",       33.33333],
+                [true,  "isNaN",       "0777"], // Octal
+                [true,  "isNaN",       "2e6"], // Exponentiation
+                [true,  "isNaN",       "0.1e2"], // Exponentiation
+                [true,  "isNaN",       +Infinity],
+                [true,  "isNaN",       -Infinity],
+                [true,  "isNaN",       "50000"],
+                [true,  "isNaN",       "0"],
+                [true,  "isNaN",       ""],
+                [true,  "isNaN",       new Object()],
+                [true,  "isNaN",       []],
+                [true,  "isNaN",       true],
+                [true,  "isNaN",       null],
+                [true,  "isNaN",       undefined],
+                [false, "isNaN",       NaN],
+                [true,  "isInteger",   33.33333],
+                [true,  "isInteger",   +Infinity],
+                [true,  "isInteger",   -Infinity],
+                [true,  "isInteger",   NaN],
+                [true,  "isInteger",   ""],
+                [true,  "isInteger",   new Object()],
+                [true,  "isInteger",   []],
+                [true,  "isInteger",   true],
+                [true,  "isInteger",   null],
+                [true,  "isInteger",   undefined],
+                [false, "isInteger",   0],
+                [false, "isInteger",   100],
+                [false, "isInteger",   0xFFFFFF],
+                [false, "isInteger",   0777], // Octal
+                [false, "isInteger",   2e6], // Exponentiation
+                [false, "isInteger",   0.1e2], // Exponentiation
+                [false, "isInteger",   "0"],
+                [true,  "isFloat",     0],
+                [true,  "isFloat",     100],
+                [true,  "isFloat",     0xFFFFFF],
+                [true,  "isFloat",     0777], // Octal
+                [true,  "isFloat",     2e6], // Exponentiation
+                [true,  "isFloat",     0.1e2], // Exponentiation
+                [true,  "isFloat",     +Infinity],
+                [true,  "isFloat",     -Infinity],
+                [true,  "isFloat",     NaN],
+                [true,  "isFloat",     ""],
+                [true,  "isFloat",     "0"],
+                [true,  "isFloat",     new Object()],
+                [true,  "isFloat",     []],
+                [true,  "isFloat",     true],
+                [true,  "isFloat",     null],
+                [true,  "isFloat",     undefined],
+                [false, "isFloat",     33.33333],
+                [true,  "isString",    new String('Hello')],
+                [true,  "isString",    []],
+                [true,  "isString",    0],
+                [true,  "isString",    true],
+                [true,  "isString",    null],
+                [true,  "isString",    undefined],
+                [false, "isString",    ""],
+                [false, "isString",    "Hello"],
+                [false, "isString",    "0"],
+                [true,  "isArray",     ""],
+                [true,  "isArray",     "0"],
+                [true,  "isArray",     new Object()],
+                [true,  "isArray",     new Function()],
+                [true,  "isArray",     0],
+                [true,  "isArray",     true],
+                [true,  "isArray",     null],
+                [true,  "isArray",     undefined],
+                [false, "isArray",     []],
+                [false, "isArray",     ["a", 2, null]],
+                [false, "isArray",     new Array()],
+                [true,  "isObject",    new Function()],
+                [true,  "isObject",    ""],
+                [true,  "isObject",    "0"],
+                [true,  "isObject",    0],
+                [true,  "isObject",    true],
+                [true,  "isObject",    null],
+                [true,  "isObject",    undefined],
+                [false, "isObject",    {}],
+                [false, "isObject",    []],
+                [false, "isObject",    ["a", 2, null]],
+                [false, "isObject",    new Array()],
+                [false, "isObject",    new Object()],
+                [false, "isObject",    new String("hello")],
+                [true,  "isFunction",  new Array()],
+                [true,  "isFunction",  new Object()],
+                [true,  "isFunction",  ""],
+                [true,  "isFunction",  "0"],
+                [true,  "isFunction",  0],
+                [true,  "isFunction",  true],
+                [true,  "isFunction",  null],
+                [true,  "isFunction",  undefined],
+                [false, "isFunction",  new Function()],
+                [false, "isFunction",  function(){}],
+                [false, "isFunction",  Math.sin],
+                [false, "isFunction",  Array.isArray],
+                [true,  "isNull",      ""],
+                [true,  "isNull",      "Hello"],
+                [true,  "isNull",      "0"],
+                [true,  "isNull",      new String('Hello')],
+                [true,  "isNull",      []],
+                [true,  "isNull",      0],
+                [true,  "isNull",      true],
+                [true,  "isNull",      undefined],
+                [false, "isNull",      null],
+                [true,  "isUndefined", ""],
+                [true,  "isUndefined", "Hello"],
+                [true,  "isUndefined", "0"],
+                [true,  "isUndefined", new String('Hello')],
+                [true,  "isUndefined", []],
+                [true,  "isUndefined", 0],
+                [true,  "isUndefined", true],
+                [true,  "isUndefined", null],
+                [false, "isUndefined", undefined],
+                [true,  "isUpperCase", "HeLlo"],
+                [true,  "isUpperCase", "hello"],
+                [true,  "isUpperCase", "0"],
+                [true,  "isUpperCase", new String('Hello')],
+                [true,  "isUpperCase", []],
+                [true,  "isUpperCase", 0],
+                [true,  "isUpperCase", true],
+                [true,  "isUpperCase", null],
+                [true,  "isUpperCase", undefined],
+                [false, "isUpperCase", "HELLO"],
+                [true,  "isLowerCase", "HELLO"],
+                [true,  "isLowerCase", "HeLlo"],
+                [true,  "isLowerCase", "0"],
+                [true,  "isLowerCase", new String('Hello')],
+                [true,  "isLowerCase", []],
+                [true,  "isLowerCase", 0],
+                [true,  "isLowerCase", true],
+                [true,  "isLowerCase", null],
+                [true,  "isLowerCase", undefined],
+                [false, "isLowerCase", "hello"],
+            ];
+        }
+    ),
+    new Test("should not affect other assertions",
+    function(pTester) {
+
+        var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+        //first assertion with not()-call
+        var expectValue = true;
+        var actualValue = dummyTester.expectThat("this value").not().equals("that other value").getTestResult();
+        pTester.expectThat(actualValue).equals(expectValue).assert();
+
+        //second test without not()-call => the first not()-call should not affect the second assertion
+        expectValue = false;
+        actualValue = dummyTester.expectThat("this value").equals("that other value").getTestResult();
+        pTester.expectThat(actualValue).equals(expectValue).assert();
+    }
+    )
+]);
+
+var elementAt = new TestSuite("Tester.elementAt", [
+    new Test("should test if value element at key position passes test",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var key = pDataProvider[2];
+            var method = pDataProvider[3];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).elementAt(key)[method](pDataProvider[4]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  ["a", "z", ["x", true, 42]],                0, "isString",  undefined],
+                [true,  ["a", "z", ["x", true, 42]],                1, "equals",    "z"],
+                [true,  ["a", "z", ["x", true, 42]],                2, "isArray",   undefined],
+                [true,  ["a", "z", ["x", true, 42]],                2, "hasLength", 3],
+                [true,  ["a", "z", {"first": "x", "second": true}], 2, "isObject",  undefined],
+                [true,  ["a", "z", {"first": "x", "second": true}], 2, "hasLength", 2],
+                [false, ["a", "z", ["x", true, 42]],                0, "isNumeric", undefined],
+                [false, ["a", "z", ["x", true, 42]],                1, "equals",    "a"],
+                [false, ["a", "z", ["x", true, 42]],                2, "isString",  undefined],
+                [false, ["a", "z", ["x", true, 42]],                2, "hasLength", 1],
+                [false, ["a", "z", {"first": "x", "second": true}], 2, "isArray",   undefined],
+                [false, ["a", "z", {"first": "x", "second": true}], 2, "hasLength", 1],
+            ];
+        }
+    ),
+    new Test("should test if nested value element at key position passes test",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            var key1 = pDataProvider[2];
+            var key2 = pDataProvider[3];
+            var method = pDataProvider[4];
+            var actualValue = dummyTester.expectThat(pDataProvider[1]).elementAt(key1).elementAt(key2)[method](pDataProvider[5]).getTestResult();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [true,  [["x", true, [42]]],            0,       0,        "isString",  undefined],
+                [true,  [["x", true, [42]]],            0,       0,        "equals",    "x"],
+                [true,  [["x", true, [42]]],            0,       1,        "isBoolean", undefined],
+                [true,  [["x", true, [42]]],            0,       1,        "equals",    true],
+                [true,  [["x", true, [42]]],            0,       2,        "isArray",   undefined],
+                [true,  [{"first": "a", "second": 42}], 0,       "first",  "equals",    "a"],
+                [true,  [{"first": "a", "second": 42}], 0,       "second", "equals",    42],
+                [true,  {"outer": {"inner": "a"}},      "outer", "inner",  "isString",  undefined],
+                [true,  {"outer": {"inner": "a"}},      "outer", "inner",  "equals",    "a"],
+                [false, [["x", true, [42]]],            0,       1,        "equals",    "a"],
+                [false, [["x", true, [42]]],            0,       2,        "isNumber",  undefined],
+                [false, {"outer": {"inner": "a"}},      "outer", "inner",  "isNumber",  undefined],
+            ];
+        }
+    )
+]);
+
+var getCoverageRate = new TestSuite("Tester._getCoverageRate", [
+    new Test("should test if coverage is correctly calculated",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[0];
+            dummyTester.methods = pDataProvider[1];
+            dummyTester.methodsCalled = pDataProvider[2];
+            var actualValue = dummyTester._getCoverageRate();
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                [100,   ["method1", "method2", "method3"], ["method1", "method2", "method3"]],
+                [33.33, ["method1", "method2", "method3"], ["method1"]],
+                [66.67, ["method1", "method2", "method3"], ["method1", "method2"]],
+                [100,   ["method1"],                       ["method1", "method2", "method3"]],
+                [0,     ["method1"],                       []],
+                [0,     [],                                ["method1"]],
+                [null,  [],                                []],
+            ];
+        }
+    )
+]);
+
+var getLength = new TestSuite("Tester._getLength", [
+    new Test("should test if value has the expected length",
+        function(pTester, pDataProvider) {
+            
+            var dummyTester = (new Tester("Test Dummy")).setOutputEnabled(false);
+            var expectValue = pDataProvider[1];
+            var actualValue = dummyTester._getLength(pDataProvider[0]);
+
+            pTester.expectThat(actualValue).equals(expectValue).assert();
+        },
+        function dataProvider() {
+            return [
+                ["",               0],
+                [[],               0],
+                [{},               0],
+                ["abc",            3],
+                [["a", "z"],       2],
+                [new String("?!"), 2],
+                [{first: "xyz"},   1],
+                [0,                undefined],
+                [true,             undefined],
+                [null,             undefined],
+                [undefined,        undefined],
+            ];
+        }
+    )
+]);
+
+
+
+var tester = new Tester("Test UnitTest_lib");
+tester.initCoverage(Tester);
+tester.test(equals);
+tester.test(isGreater);
+tester.test(isGreaterEqual);
+tester.test(isLower);
+tester.test(isLowerEqual);
+tester.test(isBoolean);
+tester.test(isNumber);
+tester.test(isNumeric);
+tester.test(isNotANumber);
+tester.test(isInteger);
+tester.test(isFloat);
+tester.test(isString);
+tester.test(isArray);
+tester.test(isObject);
+tester.test(isFunction);
+tester.test(isNull);
+tester.test(isUndefined);
+tester.test(isUpperCase);
+tester.test(isLowerCase);
+tester.test(startsWith);
+tester.test(endsWith);
+tester.test(isInstanceOf);
+tester.test(hasLength);
+tester.test(hasMinLength);
+tester.test(hasMaxLength);
+tester.test(not);
+tester.test(elementAt);
+tester.test(getCoverageRate);
+tester.test(getLength);
+
+tester.summary();
+    
+result.object(tester.getResults());
diff --git a/process/Util_lib/process.js b/process/Util_lib/process.js
index 9dc7208091da2758a798b3804912729a0fb6c775..778e4ae46cb0b4b5a7c5e2887baef1e71b2884a2 100644
--- a/process/Util_lib/process.js
+++ b/process/Util_lib/process.js
@@ -53,7 +53,7 @@ Utils.isNullOrEmpty = function (pObject)
 }
 
 /**
- * Creates a deep copy of the given object. Also works with arrays.
+ * Creates a deep copy of the given object. Also works with arrays, maps and sets.
  * 
  * @param {Object} pObject the object to create a copy of
  * @return {Object} the cloned object
@@ -69,7 +69,7 @@ Utils.isNullOrEmpty = function (pObject)
  * 
  * logging.log(original.name != copy.name);    //true
  * logging.log(copy instanceof MyObject());    //true, prototypes are set correctly
- * logging.log(copy.obj1 === copy.obj2);       //true, relative object references are kept
+ * logging.log(copy.obj1 === copy.obj2);       //true, relative object references are preserved
  */
 Utils.clone = function (pObject)
 {
@@ -84,21 +84,48 @@ Utils.clone = function (pObject)
         if (referenceMap.has(pObject))
             return referenceMap.get(pObject);
         
-        var clonedObject = Array.isArray(pObject) 
-            ? [] 
-            : Object.create(Object.getPrototypeOf(pObject)); //set the prototype of the given object
+        var clonedObject;
+        if (Array.isArray(pObject))
+            clonedObject = [];
+        else if (pObject instanceof Map)
+            clonedObject = new Map();
+        else if (pObject instanceof Set)
+            clonedObject = new Set();
+        else
+            clonedObject = Object.create(Object.getPrototypeOf(pObject)); //set the prototype of the given object
         
         /* keeps track of all encountered objects and maps the original to the copy, this makes it possible to:
            - have the same relative references in the copy as in the original
            - copy cyclic references without error */
         referenceMap.set(pObject, clonedObject);
         
-        for (let key in pObject) 
+        if (pObject instanceof Map)
+        {
+            pObject.forEach(function (value, key)
+            {
+                clonedObject.set(_clone(key), _clone(value));
+            });
+        }
+        else if (pObject instanceof Set)
+        {
+            pObject.forEach(function (value)
+            {
+                clonedObject.add(_clone(value));
+            });
+        }
+        else
         {
-            var value = pObject[key];
-            clonedObject[key] = _clone(value); //Recursively (deep) copy for nested objects, including arrays
+            for (let key in pObject) 
+            {
+                clonedObject[key] = _clone(pObject[key]); //Recursively (deep) copy for nested objects, including arrays
+            }
         }
         
+        Object.getOwnPropertySymbols(pObject).forEach(function (sym)
+        {
+            clonedObject[sym] = _clone(pObject[sym]);
+        });
+        
         return clonedObject;
     }
 }
@@ -183,6 +210,39 @@ Utils.isNumber = function (pValue)
     return typeof pValue === "number";
 }
 
+/**
+ * Checks if the given value is numeric.
+ * 
+ * @param {Object} pValue the value to check
+ * @return {Boolean} true if the value is numeric
+ */
+Utils.isNumeric = function (pValue)
+{
+    return Utils.isNumber(pValue) || (!isNaN(parseFloat(pValue)) && isFinite(pValue));
+}
+
+/**
+ * Checks if the given value is an integer.
+ * 
+ * @param {Object} pValue the value to check
+ * @return {Boolean} true if the value is an integer
+ */
+Utils.isInteger = function (pValue)
+{
+    return Number.isInteger(parseFloat(pValue)) && isFinite(pValue);
+}
+
+/**
+ * Checks if the given value is a floating point number.
+ * 
+ * @param {Object} pValue the value to check
+ * @return {Boolean} true if the value is a float
+ */
+Utils.isFloat = function (pValue)
+{
+    return !Number.isInteger(parseFloat(pValue)) && !isNaN(parseFloat(pValue)) && isFinite(pValue);
+}
+
 /**
  * Checks if the given value is an object. Be careful, null is also considered "object".
  * 
@@ -191,7 +251,7 @@ Utils.isNumber = function (pValue)
  */
 Utils.isObject = function (pValue)
 {
-    return typeof pValue === "object";
+    return typeof pValue === "object" && pValue !== null;
 }
 
 /**
diff --git a/process/Workflow_lib/process.js b/process/Workflow_lib/process.js
index d8705a1e95f0b7475a3e0301ba5ef9fe4fc4283b..38627a87e22539c50660b586e22538dee5c0ad9a 100644
--- a/process/Workflow_lib/process.js
+++ b/process/Workflow_lib/process.js
@@ -46,15 +46,18 @@ WorkflowUtils.openNewInstance = function (pVariables, pTargetIds, pTargetContext
 {
     if ((!pTargetIds || pTargetIds.length === 0) && pSelectionFilter)
         pTargetIds = [];
+    else if (!pTargetIds)
+        pTargetIds = [WorkflowVariables.TARGET_ID.getDefaultValue()];
     if (!pVariables)
         pVariables = {};
     
     Object.assign(pVariables, WorkflowVariables.getTargetVariables(pTargetIds, pTargetContext));
     
     neon.openContext("WorkflowLauncher", "WorkflowLauncherEdit_view", null, neon.OPERATINGSTATE_VIEW, {
-        "ProcessVariables_param" : JSON.stringify(pVariables),
-        "TargetContext_param" : pVariables[WorkflowVariables.TARGET_CONTEXT()],
-        "TargetFilter_param" : pSelectionFilter ? JSON.stringify(pSelectionFilter) : ""
+        "ProcessVariables_param": JSON.stringify(pVariables),
+        "TargetContext_param": pVariables[WorkflowVariables.TARGET_CONTEXT()],
+        "TargetFilter_param": pSelectionFilter ? JSON.stringify(pSelectionFilter) : "",
+        "Targets_param": JSON.stringify(pTargetIds)
     });
 }
 
diff --git a/process/_all_test/_all_test.aod b/process/_all_test/_all_test.aod
new file mode 100644
index 0000000000000000000000000000000000000000..4336a9ddb94b6e8c5741887bbfd4e636f23b0541
--- /dev/null
+++ b/process/_all_test/_all_test.aod
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>_all_test</name>
+  <title>[TEST] .All</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:CHECK_CIRCLE</icon>
+  <process>%aditoprj%/process/_all_test/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/_all_test/process.js b/process/_all_test/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbc75f35849b71f8514f1c4330683ee568e08ba0
--- /dev/null
+++ b/process/_all_test/process.js
@@ -0,0 +1,23 @@
+import("system.logging");
+import("system.process");
+import("UnitTest_lib");
+import("Terminal_lib");
+
+
+var testResults = [];
+
+process.getProcesses([process.VARIANT_EXECUTABLE]).forEach(function(pProcessName) {
+    // Run all executable processes ending with "_test" except THIS one
+    if(pProcessName !== "_all_test" && pProcessName.substr(-5, 5) === "_test") {
+        var startConfig = process.createStartConfig().setName(pProcessName);
+        var testResult = JSON.parse(process.start(startConfig));
+        
+        if(testResult !== null && typeof testResult === "object") {
+            testResults.push(testResult);
+        }
+    }
+});
+
+var tester = new Tester("");
+tester.summary(testResults);
+
diff --git a/report/Reminder_report/reportData.jrxml b/report/Reminder_report/reportData.jrxml
index 5e0e92752aff29c9212d7fed72e0a86f5592701e..1347b1ef959b34c58be528e8eecfb4bc139ff51a 100644
--- a/report/Reminder_report/reportData.jrxml
+++ b/report/Reminder_report/reportData.jrxml
@@ -2,7 +2,7 @@
 <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Mahnung" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="e7a916c8-3f9a-497d-84bb-3909b15271ea">
 	<property name="ireport.zoom" value="2.1435888100000016"/>
 	<property name="ireport.x" value="0"/>
-	<property name="ireport.y" value="144"/>
+	<property name="ireport.y" value="0"/>
 	<parameter name="myAddr" class="java.lang.String"/>
 	<parameter name="Kontenabstimmung" class="java.lang.String"/>
 	<parameter name="Rech.-Nr" class="java.lang.String"/>
@@ -22,6 +22,7 @@
 	<parameter name="Due" class="java.lang.String"/>
 	<parameter name="DUEDATE" class="java.lang.String"/>
 	<parameter name="Dunninglevel" class="java.lang.String"/>
+	<parameter name="OutstandingAmount" class="java.lang.String"/>
 	<field name="PAYED" class="java.lang.String"/>
 	<field name="RELATION_ID" class="java.lang.String"/>
 	<field name="CURRENCY" class="java.lang.String"/>
@@ -41,89 +42,89 @@
 				<textField>
 					<reportElement x="13" y="0" width="68" height="15" uuid="0a515534-8d2a-4e3e-9370-6cac4c65ef68"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$P{Ordernumber}]]></textFieldExpression>
 				</textField>
 				<textField>
 					<reportElement x="104" y="0" width="63" height="15" uuid="87fc2f40-ffdc-47d5-9fd8-7a8caf821114"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$P{Orderdate}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="202" y="0" width="62" height="15" uuid="86eacb27-6bb5-4ce8-b8cf-c3f0993380ad"/>
+					<reportElement x="202" y="0" width="76" height="15" uuid="86eacb27-6bb5-4ce8-b8cf-c3f0993380ad"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$P{DueDate}]]></textFieldExpression>
 				</textField>
-				<textField>
-					<reportElement x="429" y="0" width="59" height="15" uuid="62a0909f-ef03-4242-969a-8a9532d1aa9a"/>
-					<textElement textAlignment="Right">
-						<font  size="8"/>
-					</textElement>
-					<textFieldExpression><![CDATA[$P{Due}]]></textFieldExpression>
-				</textField>
 				<line>
 					<reportElement x="13" y="33" width="527" height="1" uuid="d5108302-191f-4e27-8920-fcd330d335e8"/>
 				</line>
 				<textField>
 					<reportElement x="104" y="36" width="64" height="15" uuid="ca4c366a-1954-4aee-91c4-05c093b04df5"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{ORDERDATE}]]></textFieldExpression>
 				</textField>
 				<textField>
 					<reportElement x="13" y="36" width="68" height="15" uuid="3fcb3ee0-fe4d-409e-8cb8-7a0e6ceca5ab"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{ORDERCODE}]]></textFieldExpression>
 				</textField>
 				<textField>
 					<reportElement x="202" y="36" width="62" height="15" uuid="b5e006cf-5d37-42a1-bf74-0b49d3a39b27"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{DUEDATE}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="428" y="36" width="60" height="15" uuid="d57eeb7b-f517-4c85-a925-cdc3c1ea0830"/>
+					<reportElement x="451" y="36" width="60" height="15" uuid="d57eeb7b-f517-4c85-a925-cdc3c1ea0830"/>
 					<textElement textAlignment="Right">
-						<font  size="8" isBold="true"/>
+						<font size="8" isBold="true"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{OFFEN}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="307" y="35" width="56" height="15" uuid="72a49ece-7fc0-4cd7-a00c-94580845d150"/>
+					<reportElement x="332" y="35" width="56" height="15" uuid="72a49ece-7fc0-4cd7-a00c-94580845d150"/>
 					<textElement textAlignment="Right">
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{BRUTTO}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="307" y="15" width="56" height="15" uuid="a991ccab-0b04-4550-bbd2-2033a5fad0f0"/>
+					<reportElement x="332" y="15" width="56" height="15" uuid="a991ccab-0b04-4550-bbd2-2033a5fad0f0"/>
 					<textElement textAlignment="Right">
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{CURRENCY}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="429" y="15" width="59" height="15" uuid="8e257160-536a-418a-b9df-80fa020268b8"/>
+					<reportElement x="452" y="15" width="59" height="15" uuid="8e257160-536a-418a-b9df-80fa020268b8"/>
 					<textElement textAlignment="Right">
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
 					<textFieldExpression><![CDATA[$F{CURRENCY}]]></textFieldExpression>
 				</textField>
 				<textField>
-					<reportElement x="326" y="0" width="62" height="15" uuid="000feb72-9b7d-42e6-ade9-8e12359c3eef"/>
+					<reportElement x="307" y="0" width="81" height="15" uuid="000feb72-9b7d-42e6-ade9-8e12359c3eef"/>
+					<textElement>
+						<font size="8"/>
+					</textElement>
+					<textFieldExpression><![CDATA[$R{Rechnungsbetrag}]]></textFieldExpression>
+				</textField>
+				<textField>
+					<reportElement x="430" y="0" width="81" height="15" uuid="8eee46c0-f3c3-4563-b1b1-746aeb7e73c6"/>
 					<textElement>
-						<font  size="8"/>
+						<font size="8"/>
 					</textElement>
-					<textFieldExpression><![CDATA[$R{Rech.-Betrag}]]></textFieldExpression>
+					<textFieldExpression><![CDATA[$P{OutstandingAmount}]]></textFieldExpression>
 				</textField>
 			</band>
 		</groupHeader>
@@ -140,7 +141,7 @@
 			<textField>
 				<reportElement x="13" y="88" width="527" height="20" forecolor="#000000" uuid="a47fa80c-6b70-4c8a-938d-3d44273b1ffb"/>
 				<textElement textAlignment="Center" verticalAlignment="Middle">
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$P{Address}]]></textFieldExpression>
 			</textField>
@@ -157,35 +158,35 @@
 			<textField isStretchWithOverflow="true">
 				<reportElement x="13" y="122" width="152" height="20" uuid="c8514bfb-5e34-43f4-a256-7899cb58f0c1"/>
 				<textElement>
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$P{ReminderAddress}]]></textFieldExpression>
 			</textField>
 			<textField>
 				<reportElement x="13" y="172" width="132" height="20" uuid="d21620a9-4a49-496e-80f6-db842f0b70a9"/>
 				<textElement>
-					<font  size="14" isBold="true"/>
+					<font size="14" isBold="true"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$P{Reminder}]]></textFieldExpression>
 			</textField>
 			<textField pattern="dd.MM.yyyy">
 				<reportElement x="388" y="192" width="100" height="20" uuid="5141d92f-3325-445b-b616-90630c1648f7"/>
 				<textElement textAlignment="Right">
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[new java.util.Date()]]></textFieldExpression>
 			</textField>
 			<textField>
 				<reportElement x="13" y="232" width="294" height="20" uuid="3f7d1c4a-a326-436b-831d-5da9fd255bf0"/>
 				<textElement>
-					<font  size="8" isBold="true"/>
+					<font size="8" isBold="true"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$F{DUNNINGTEXT}]]></textFieldExpression>
 			</textField>
 			<textField>
 				<reportElement x="13" y="212" width="214" height="20" uuid="52834aec-d107-4cc7-9662-1cdd73ba15c1"/>
 				<textElement>
-					<font  size="8" isBold="true"/>
+					<font size="8" isBold="true"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$P{Dunninglevel} + " " + $F{DUNNINGLEVEL}]]></textFieldExpression>
 			</textField>
@@ -196,21 +197,21 @@
 			<textField pattern="EEEEE dd MMMMM yyyy">
 				<reportElement x="340" y="13" width="112" height="15" forecolor="#999999" uuid="26510f19-a203-4b34-8093-f6a9f7f6050e"/>
 				<textElement>
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[new java.util.Date()]]></textFieldExpression>
 			</textField>
 			<textField>
 				<reportElement x="452" y="13" width="48" height="15" forecolor="#999999" uuid="218ef560-390f-4807-b7f6-98e6f6162892"/>
 				<textElement textAlignment="Right">
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[$R{Seite}+" "+$V{PAGE_NUMBER}+" "+$R{von}]]></textFieldExpression>
 			</textField>
 			<textField evaluationTime="Report">
 				<reportElement x="500" y="13" width="40" height="15" forecolor="#999999" uuid="7ac354ed-0f8f-41fa-80a5-c2a0f74f7e09"/>
 				<textElement>
-					<font  size="8"/>
+					<font size="8"/>
 				</textElement>
 				<textFieldExpression><![CDATA[" " + $V{PAGE_NUMBER}]]></textFieldExpression>
 			</textField>