From 747351c3391589a57bd90f847a54b53c8fbb2e3a Mon Sep 17 00:00:00 2001
From: Pascal Neub <p.neub@adito.de>
Date: Tue, 11 Jan 2022 15:18:04 +0100
Subject: [PATCH] 2001215 | Performance optimieren

---
 entity/Contract_entity/Contract_entity.aod    |  4 +
 entity/Offeritem_entity/Offeritem_entity.aod  |  4 +-
 entity/Offeritem_entity/afterSave.js          | 75 ++++++++++++-------
 .../entityfields/offeritemid/valueProcess.js  |  9 ---
 .../recordcontainers/db/onDBUpdate.js         | 46 +-----------
 .../duplicateobject_param/valueProcess.js     |  2 +-
 .../Productprice_entity.aod                   | 30 ++++++--
 .../pricelist/displayValueProcess.js          | 19 +----
 .../product_id/displayValueProcess.js         | 15 ----
 .../recordcontainers/db/fromClauseProcess.js  |  6 ++
 .../pricelist.displayvalue/expression.js      | 13 ++++
 .../product_id.displayvalue/expression.js     |  1 -
 .../entityfields/reasons/stateProcess.js      | 11 ++-
 process/Keyword_lib/process.js                |  5 +-
 process/OfferOrder_lib/process.js             | 26 +++++++
 process/Offer_lib/process.js                  | 10 +++
 process/Order_lib/process.js                  | 10 +++
 process/Util_lib/process.js                   |  1 -
 18 files changed, 159 insertions(+), 128 deletions(-)
 delete mode 100644 entity/Offeritem_entity/entityfields/offeritemid/valueProcess.js
 delete mode 100644 entity/Productprice_entity/entityfields/product_id/displayValueProcess.js
 create mode 100644 entity/Productprice_entity/recordcontainers/db/fromClauseProcess.js
 create mode 100644 entity/Productprice_entity/recordcontainers/db/recordfieldmappings/pricelist.displayvalue/expression.js
 delete mode 100644 entity/Productprice_entity/recordcontainers/db/recordfieldmappings/product_id.displayvalue/expression.js

diff --git a/entity/Contract_entity/Contract_entity.aod b/entity/Contract_entity/Contract_entity.aod
index 2044c84e8e6..926a736422b 100644
--- a/entity/Contract_entity/Contract_entity.aod
+++ b/entity/Contract_entity/Contract_entity.aod
@@ -65,6 +65,10 @@
       <inputFormat>dd.MM.yyyy</inputFormat>
       <groupable v="true" />
       <onValueChange>%aditoprj%/entity/Contract_entity/entityfields/contractend/onValueChange.js</onValueChange>
+      <onValueChangeTypes>
+        <element>MASK</element>
+        <element>PROCESS_SETVALUE</element>
+      </onValueChangeTypes>
     </entityField>
     <entityField>
       <name>CONTRACTID</name>
diff --git a/entity/Offeritem_entity/Offeritem_entity.aod b/entity/Offeritem_entity/Offeritem_entity.aod
index d79f7aa05ee..ae86a0ba510 100644
--- a/entity/Offeritem_entity/Offeritem_entity.aod
+++ b/entity/Offeritem_entity/Offeritem_entity.aod
@@ -52,7 +52,6 @@
     </entityField>
     <entityField>
       <name>OFFERITEMID</name>
-      <valueProcess>%aditoprj%/entity/Offeritem_entity/entityfields/offeritemid/valueProcess.js</valueProcess>
     </entityField>
     <entityField>
       <name>OFFER_ID</name>
@@ -89,7 +88,6 @@
       <onValueChange>%aditoprj%/entity/Offeritem_entity/entityfields/product_id/onValueChange.js</onValueChange>
       <onValueChangeTypes>
         <element>MASK</element>
-        <element>PROCESS</element>
       </onValueChangeTypes>
     </entityField>
     <entityField>
@@ -270,6 +268,7 @@
         <entityActionField>
           <name>moveUp</name>
           <onActionProcess>%aditoprj%/entity/Offeritem_entity/entityfields/group/children/moveup/onActionProcess.js</onActionProcess>
+          <selectionType>MULTI</selectionType>
           <iconId>VAADIN:ARROW_UP</iconId>
           <state>DISABLED</state>
           <stateProcess>%aditoprj%/entity/Offeritem_entity/entityfields/group/children/moveup/stateProcess.js</stateProcess>
@@ -277,6 +276,7 @@
         <entityActionField>
           <name>moveDown</name>
           <onActionProcess>%aditoprj%/entity/Offeritem_entity/entityfields/group/children/movedown/onActionProcess.js</onActionProcess>
+          <selectionType>MULTI</selectionType>
           <iconId>VAADIN:ARROW_DOWN</iconId>
           <state>DISABLED</state>
           <stateProcess>%aditoprj%/entity/Offeritem_entity/entityfields/group/children/movedown/stateProcess.js</stateProcess>
diff --git a/entity/Offeritem_entity/afterSave.js b/entity/Offeritem_entity/afterSave.js
index 2041fcb06eb..aca155cf0b5 100644
--- a/entity/Offeritem_entity/afterSave.js
+++ b/entity/Offeritem_entity/afterSave.js
@@ -1,45 +1,68 @@
+import("system.db");
 import("system.neon");
 import("system.entities");
 import("system.vars");
 import("Sql_lib");
 import("Offer_lib");
 
+var statements = [];
 if(vars.get("$local.recordstate") == neon.OPERATINGSTATE_EDIT)
 {
     var offerId = vars.get("$field.OFFER_ID");
     var offerItemUtils = new OfferItemUtils(offerId);
-    offerItemUtils.initItemTree();
-    var childIds = new Set();
-    function _traverseChilds(pId)
+    
+    var rowmap = new Map();
+    newSelect(["OFFERITEM.OFFERITEMID", "OFFERITEM.QUANTITY"])
+        .from("OFFERITEM")
+        .where("OFFERITEM.OFFER_ID", offerId)
+        .table()
+        .forEach(function(pRow){rowmap.set(pRow[0],pRow)});
+    
+    var updateOffer = false;
+    vars.get("$context.rows").forEach(function([pInitialRowdata, pRowdata])
     {
-        if(!childIds.has(pId))
+        var oldQuantity = parseFloat(pInitialRowdata["OFFERITEM.QUANTITY"]);
+        var newQuantity = parseFloat(pRowdata["OFFERITEM.QUANTITY"]);
+        var quantityMultiplier = newQuantity / oldQuantity;
+        var childIds = offerItemUtils.traverse(pRowdata["OFFERITEM.OFFERITEMID"], function(pChildId)
         {
-            childIds.add(pId);
-            if(pId in offerItemUtils.ItemTree)
+            if(oldQuantity != newQuantity)
             {
-                // catch errors if the item tree got calculated while inserting / deleting items
-                offerItemUtils.ItemTree[pId].ids.forEach(_traverseChilds);
+                var quantity = rowmap.get(pChildId)[1] * quantityMultiplier;
+                statements.push([
+                    "OFFERITEM", ["QUANTITY"], null, [quantity],
+                    newWhere("OFFERITEM.OFFERITEMID", pChildId).toString()
+                ]);
             }
+        });
+        if(childIds.length && pInitialRowdata["OFFERITEM.SHOWPRICE"] != pRowdata["OFFERITEM.SHOWPRICE"])
+        {
+            statements.push([
+                "OFFERITEM", ["SHOWPRICE"], null, [pRowdata["OFFERITEM.SHOWPRICE"]],
+                newWhere("OFFERITEM.OFFERITEMID", childIds, SqlBuilder.IN()).toString()
+            ]);
         }
-    }
-    _traverseChilds(vars.get("$sys.uid"));
-    newWhere("OFFERITEM.OFFERITEMID", Array.from(childIds), SqlBuilder.IN())
-        .updateFields({"SHOWPRICE": vars.get("$field.SHOWPRICE")});
+        if(!updateOffer && !pRowdata["OFFERITEM.ITEMPOSITION"].includes(".") && (
+            pInitialRowdata["OFFERITEM.NET"] != pRowdata["OFFERITEM.NET"] ||
+            pInitialRowdata["OFFERITEM.VAT"] != pRowdata["OFFERITEM.VAT"] ||
+            pInitialRowdata["OFFERITEM.DISCOUNT"] != pRowdata["OFFERITEM.DISCOUNT"] ||
+            pInitialRowdata["OFFERITEM.QUANTITY"] != pRowdata["OFFERITEM.QUANTITY"]))
+        {
+            updateOffer = true;
+        }
+    });
     
-    if(!vars.get("$field.ITEMPOSITION").includes("."))//only the topItems affect the Offer price
+    if(updateOffer)
     {
-        var discount = vars.exists("$param.Discount_param") ? vars.get("$param.Discount_param"): "";
-        var cols = ["NET", "VAT"];    
-        var vals = offerItemUtils.getNetAndVat();
-        var fieldValues = {
-            NET: vals[0].toString(),
-            VAT: vals[1].toString()
-        };
-    
-        var config = entities.createConfigForUpdatingRows();
-        config.entity("Offer_entity");
-        config.uid(offerId);
-        config.fieldValues(fieldValues);
-        entities.updateRow(config);
+        statements.push([
+            "OFFER", ["NET", "VAT"], null, offerItemUtils.getNetAndVat(),
+            newWhere("OFFER.OFFERID", offerId).toString()
+        ]);
     }
 }
+if(statements.length)
+{
+    // TODO: replace once a other somewhat performant solution is aviable
+    statements.length && db.execute(statements);
+    neon.refreshAll();
+}
diff --git a/entity/Offeritem_entity/entityfields/offeritemid/valueProcess.js b/entity/Offeritem_entity/entityfields/offeritemid/valueProcess.js
deleted file mode 100644
index 823247cf893..00000000000
--- a/entity/Offeritem_entity/entityfields/offeritemid/valueProcess.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import("system.util");
-import("system.vars");
-import("system.result");
-import("system.neon");
-
-if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && vars.get("$this.value") == null)
-{
-    result.string(util.getNewUUID());
-}
\ No newline at end of file
diff --git a/entity/Offeritem_entity/recordcontainers/db/onDBUpdate.js b/entity/Offeritem_entity/recordcontainers/db/onDBUpdate.js
index 5a02db4f37a..f0f6c420220 100644
--- a/entity/Offeritem_entity/recordcontainers/db/onDBUpdate.js
+++ b/entity/Offeritem_entity/recordcontainers/db/onDBUpdate.js
@@ -1,44 +1,6 @@
-import("system.eMath");
-import("system.entities");
 import("system.vars");
-import("system.db");
-import("system.neon");
-import("Offer_lib");
-import("Sql_lib");
 
-// this processs get's executed for every child of this offerItem since we use writeEntiy, so we use the param to make sure we don't execute it for the children
-var newQuanitity = parseFloat(vars.get("$field.QUANTITY"));
-var oldQuantity = parseFloat(vars.get("$local.initialRowdata")["OFFERITEM.QUANTITY"]);
-var offerItemId = vars.get("$field.OFFERITEMID");
-if(newQuanitity != oldQuantity) //quantity changed -> change quantities of the childitems accordingly
-{
-    var multiplier = newQuanitity/oldQuantity;
-
-    var loadConfig = entities.createConfigForLoadingRows().entity("Offeritem_entity").addParameter("OfferId_param", vars.get("$field.OFFER_ID")).fields(["OFFERITEMID", "ASSIGNEDTO", "PRODUCT_ID", "QUANTITY"])
-
-    var rows = entities.getRows(loadConfig);
-    var potentialAsignees = {};
-    var offerItemsToUpdate = {};
-    var statements = [];
-    var stop = false;
-    while(stop == false)//we have too loop for all the rows for each row that needs updating, since those are also pontially asignees
-    {
-        stop = true;
-        for(var offeritem in rows)//loop trough all the rows and build offerItemsToUpdate
-        {
-            if(!(rows[offeritem]["OFFERITEMID"] in offerItemsToUpdate) &&(rows[offeritem]["ASSIGNEDTO"] == offerItemId || rows[offeritem]["ASSIGNEDTO"] in potentialAsignees))
-            {
-                statements.push(
-                    newWhere("OFFERITEM.OFFERITEMID", rows[offeritem]["OFFERITEMID"]).buildUpdateStatement({
-                        "QUANTITY": parseInt(rows[offeritem]["QUANTITY"])*multiplier
-                    })
-                );
-                offerItemsToUpdate[rows[offeritem]["OFFERITEMID"]] = parseInt(rows[offeritem]["QUANTITY"])*multiplier;
-                potentialAsignees[rows[offeritem]["OFFERITEMID"]] = "";
-                        
-                stop = false;
-            }
-        }
-    }
-    db.execute(statements);// no write entity -> performance reason
-}
+// used to batch all sql statements for performance reasons
+var contextRows = vars.exists("$context.rows") ? vars.get("$context.rows") : [];
+contextRows.push([vars.get("$local.initialRowdata"), vars.get("$local.rowdata")]);
+vars.set("$context.rows", contextRows);
diff --git a/entity/Person_entity/entityfields/duplicatesperson/children/duplicateobject_param/valueProcess.js b/entity/Person_entity/entityfields/duplicatesperson/children/duplicateobject_param/valueProcess.js
index 542e2339659..85b328dc04a 100644
--- a/entity/Person_entity/entityfields/duplicatesperson/children/duplicateobject_param/valueProcess.js
+++ b/entity/Person_entity/entityfields/duplicatesperson/children/duplicateobject_param/valueProcess.js
@@ -33,4 +33,4 @@ result.object(DuplicateScannerUtils.getDataForDuplicateCheck(EntityUtils.getCurr
     STANDARD_PHONE_COMMUNICATION: vars.get("$field.STANDARD_PHONE_COMMUNICATION"),
     STANDARD_ZIP: vars.get("$field.STANDARD_ZIP"),
     ORGANISATION_ID: vars.get("$field.ORGANISATION_ID")
-}));
\ No newline at end of file
+}));
diff --git a/entity/Productprice_entity/Productprice_entity.aod b/entity/Productprice_entity/Productprice_entity.aod
index 177b09df41e..80c122f1565 100644
--- a/entity/Productprice_entity/Productprice_entity.aod
+++ b/entity/Productprice_entity/Productprice_entity.aod
@@ -21,6 +21,10 @@
       <stateProcess>%aditoprj%/entity/Productprice_entity/entityfields/buysell/stateProcess.js</stateProcess>
       <valueProcess>%aditoprj%/entity/Productprice_entity/entityfields/buysell/valueProcess.js</valueProcess>
       <onValueChange>%aditoprj%/entity/Productprice_entity/entityfields/buysell/onValueChange.js</onValueChange>
+      <onValueChangeTypes>
+        <element>MASK</element>
+        <element>PROCESS_SETVALUE</element>
+      </onValueChangeTypes>
     </entityField>
     <entityField>
       <name>CURRENCY</name>
@@ -65,7 +69,6 @@
       <mandatory v="true" />
       <stateProcess>%aditoprj%/entity/Productprice_entity/entityfields/product_id/stateProcess.js</stateProcess>
       <valueProcess>%aditoprj%/entity/Productprice_entity/entityfields/product_id/valueProcess.js</valueProcess>
-      <displayValueProcess>%aditoprj%/entity/Productprice_entity/entityfields/product_id/displayValueProcess.js</displayValueProcess>
     </entityField>
     <entityField>
       <name>CONTACT_ID</name>
@@ -114,6 +117,10 @@
       <valueProcess>%aditoprj%/entity/Productprice_entity/entityfields/pricelist/valueProcess.js</valueProcess>
       <displayValueProcess>%aditoprj%/entity/Productprice_entity/entityfields/pricelist/displayValueProcess.js</displayValueProcess>
       <onValueChange>%aditoprj%/entity/Productprice_entity/entityfields/pricelist/onValueChange.js</onValueChange>
+      <onValueChangeTypes>
+        <element>MASK</element>
+        <element>PROCESS_SETVALUE</element>
+      </onValueChangeTypes>
     </entityField>
     <entityParameter>
       <name>ProductId_param</name>
@@ -202,11 +209,6 @@
       <name>INFO</name>
       <title>Information</title>
       <contentType>TEXT</contentType>
-      <onValueChangeTypes>
-        <element>MASK</element>
-        <element>PROCESS</element>
-        <element>PROCESS_SETVALUE</element>
-      </onValueChangeTypes>
     </entityField>
     <entityConsumer>
       <name>OrganisationConditions</name>
@@ -233,6 +235,7 @@
     <dbRecordContainer>
       <name>db</name>
       <hasDependentRecords v="true" />
+      <fromClauseProcess>%aditoprj%/entity/Productprice_entity/recordcontainers/db/fromClauseProcess.js</fromClauseProcess>
       <conditionProcess>%aditoprj%/entity/Productprice_entity/recordcontainers/db/conditionProcess.js</conditionProcess>
       <orderClauseProcess>%aditoprj%/entity/Productprice_entity/recordcontainers/db/orderClauseProcess.js</orderClauseProcess>
       <onDBInsert>%aditoprj%/entity/Productprice_entity/recordcontainers/db/onDBInsert.js</onDBInsert>
@@ -312,17 +315,28 @@
         </dbRecordFieldMapping>
         <dbRecordFieldMapping>
           <name>PRODUCT_ID.displayValue</name>
-          <expression>%aditoprj%/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/product_id.displayvalue/expression.js</expression>
+          <recordfield>PRODUCT.PRODUCTNAME</recordfield>
+        </dbRecordFieldMapping>
+        <dbRecordFieldMapping>
+          <name>PRICELIST.displayValue</name>
+          <expression>%aditoprj%/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/pricelist.displayvalue/expression.js</expression>
         </dbRecordFieldMapping>
       </recordFieldMappings>
       <linkInformation>
         <linkInformation>
-          <name>85fd1bcf-499f-4708-ad8e-18f5a0f5337d</name>
+          <name>12b86a0c-4151-491a-83be-8729ed65e24e</name>
           <tableName>PRODUCTPRICE</tableName>
           <primaryKey>PRODUCTPRICEID</primaryKey>
           <isUIDTable v="true" />
           <readonly v="false" />
         </linkInformation>
+        <linkInformation>
+          <name>19c76b4d-ec0f-40d8-abab-fe91c882f0b9</name>
+          <tableName>PRODUCT</tableName>
+          <primaryKey>PRODUCTID</primaryKey>
+          <isUIDTable v="false" />
+          <readonly v="true" />
+        </linkInformation>
       </linkInformation>
       <filterExtensions>
         <filterExtension>
diff --git a/entity/Productprice_entity/entityfields/pricelist/displayValueProcess.js b/entity/Productprice_entity/entityfields/pricelist/displayValueProcess.js
index e54cffa4492..b53fdf5bfdc 100644
--- a/entity/Productprice_entity/entityfields/pricelist/displayValueProcess.js
+++ b/entity/Productprice_entity/entityfields/pricelist/displayValueProcess.js
@@ -1,25 +1,10 @@
-import("system.db");
 import("system.result");
 import("system.neon");
 import("system.vars");
 import("Keyword_lib");
-import("Sql_lib");
-import("system.text");
 import("KeywordRegistry_basic");
 
-var pricelistname = vars.get("$field.PRICELIST");
-if (!pricelistname)
-{
-    var orgname = newSelect("ORGANISATION.NAME")
-                    .from("CONTACT")
-                    .join("ORGANISATION", "CONTACT.ORGANISATION_ID = ORGANISATION.ORGANISATIONID", undefined, "inner")
-                    .orderBy("NAME")
-                    .whereIfSet("CONTACT.CONTACTID", "$field.CONTACT_ID")
-                    .cell(true);
-    
-    result.string(orgname);
-}
-else
+if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW)
 {
     result.string(KeywordUtils.getViewValue($KeywordRegistry.productPricelist(), vars.get("$field.PRICELIST")));
-}
\ No newline at end of file
+}
diff --git a/entity/Productprice_entity/entityfields/product_id/displayValueProcess.js b/entity/Productprice_entity/entityfields/product_id/displayValueProcess.js
deleted file mode 100644
index bee4e1ff594..00000000000
--- a/entity/Productprice_entity/entityfields/product_id/displayValueProcess.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import("system.result");
-import("system.neon");
-import("system.vars");
-import("Sql_lib");
-
-
-// TODO: This should only be a temporary solution, till it's possible to also group fields of pageable dbRecordContainers that have their displayValue set via displayValueExpression (#1077182)
-if((vars.exists("$param.ProductId_param") && vars.get("$param.ProductId_param") && vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW)
-    || vars.get("$sys.recordstate") == neon.OPERATINGSTATE_EDIT || vars.get("$sys.recordstate") == neon.OPERATINGSTATE_VIEW)
-{
-    result.string(newSelect("PRODUCTNAME")
-                    .from("PRODUCT")
-                    .whereIfSet("PRODUCT.PRODUCTID", "$field.PRODUCT_ID")
-                    .cell());
-}
\ No newline at end of file
diff --git a/entity/Productprice_entity/recordcontainers/db/fromClauseProcess.js b/entity/Productprice_entity/recordcontainers/db/fromClauseProcess.js
new file mode 100644
index 00000000000..5e449ec059b
--- /dev/null
+++ b/entity/Productprice_entity/recordcontainers/db/fromClauseProcess.js
@@ -0,0 +1,6 @@
+import("system.result");
+import("Sql_lib");
+
+const from = new SqlBuilder().from("PRODUCTPRICE")
+    .join("PRODUCT", "PRODUCTPRICE.PRODUCT_ID = PRODUCT.PRODUCTID");
+result.string(from.toString());
diff --git a/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/pricelist.displayvalue/expression.js b/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/pricelist.displayvalue/expression.js
new file mode 100644
index 00000000000..0536bb3ccd2
--- /dev/null
+++ b/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/pricelist.displayvalue/expression.js
@@ -0,0 +1,13 @@
+import("system.result");
+import("Sql_lib");
+import("Keyword_lib");
+import("KeywordRegistry_basic");
+
+var sql = SqlBuilder.caseWhen("PRODUCTPRICE.CONTACT_ID is null")
+    .then(KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.productPricelist(), "PRODUCTPRICE.PRICELIST"))
+    .elseValue(
+        newSelect("ORGANISATION.NAME").from("ORGANISATION")
+        .join("CONTACT", "CONTACT.ORGANISATION_ID = ORGANISATION.ORGANISATIONID")
+        .where("CONTACT.CONTACTID = PRODUCTPRICE.CONTACT_ID")
+    );
+result.string(sql.toString());
diff --git a/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/product_id.displayvalue/expression.js b/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/product_id.displayvalue/expression.js
deleted file mode 100644
index c7a17f50a06..00000000000
--- a/entity/Productprice_entity/recordcontainers/db/recordfieldmappings/product_id.displayvalue/expression.js
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: Fill this instead of the displayValueProcess once it's possible to also group fields of pageable dbRecordContainers that have their displayValue set via displayValueExpression (#1077182)
\ No newline at end of file
diff --git a/entity/Salesproject_entity/entityfields/reasons/stateProcess.js b/entity/Salesproject_entity/entityfields/reasons/stateProcess.js
index 57434eadd61..a17a7a088c5 100644
--- a/entity/Salesproject_entity/entityfields/reasons/stateProcess.js
+++ b/entity/Salesproject_entity/entityfields/reasons/stateProcess.js
@@ -1,9 +1,12 @@
 import("system.result");
+import("system.neon");
 import("system.vars");
 import("KeywordRegistry_basic");
 
 // IDs: SalesprojectState Order and Lost
-if(vars.get("$field.STATUS") && (vars.get("$field.STATUS") == $KeywordRegistry.salesprojectState$order() || vars.get("$field.STATUS") == $KeywordRegistry.salesprojectState$lost()))
-        result.string("EDITABLE");
-else
-        result.string("INVISIBLE");
+var componentState = neon.COMPONENTSTATE_INVISIBLE;
+if(vars.get("$field.STATUS") == $KeywordRegistry.salesprojectState$order() || vars.get("$field.STATUS") == $KeywordRegistry.salesprojectState$lost())
+{
+    componentState = neon.COMPONENTSTATE_EDITABLE;
+}
+result.string(componentState);
diff --git a/process/Keyword_lib/process.js b/process/Keyword_lib/process.js
index 4f160d58a86..1e0bbe4dd36 100644
--- a/process/Keyword_lib/process.js
+++ b/process/Keyword_lib/process.js
@@ -1,3 +1,4 @@
+import("Util_lib");
 import("KeywordData_lib");
 import("system.entities");
 import("system.db");
@@ -206,7 +207,7 @@ KeywordUtils.getEntryNamesByContainer = function(pContainerName, pLocale)
 */
 KeywordUtils.getEntryArray = function (pContainerName, pLocale, pOnlyActive)
 {
-    if (vars.get("$sys.isserver"))
+    if (Utils.toBoolean(vars.get("$sys.isserver")))
     {
         return KeywordUtils.getEntryNamesAndIdsByContainer(pContainerName, pLocale, pOnlyActive);
     }
@@ -215,7 +216,7 @@ KeywordUtils.getEntryArray = function (pContainerName, pLocale, pOnlyActive)
         .entity("KeywordEntry_entity")
         .provider("SpecificContainerKeywords")
         .fields(["KEYID", "TITLE_TRANSLATED"])
-        .addParameter("ContainerName_param", pContainerName)
+        .addParameter("ContainerName_param", pContainerName);
     
     if (pLocale)
     {
diff --git a/process/OfferOrder_lib/process.js b/process/OfferOrder_lib/process.js
index 567c87350b0..cf718cc1b2f 100644
--- a/process/OfferOrder_lib/process.js
+++ b/process/OfferOrder_lib/process.js
@@ -96,6 +96,32 @@ function ItemUtils(pOfferOrderId, pTableName) {
     }
 }
 
+ItemUtils.prototype.traverse = function(pItemId, pCallback)
+{
+    // Since lamda functions are not supported, we have to store the this object in self.
+    // => Functions dont use the lexical this, but instead use the global this object.
+    var self = this;
+    var childIds = new Set();
+    function _traverseChilds(pId)
+    {
+        if(!childIds.has(pId))
+        {
+            if(pId != pItemId)
+            {
+                childIds.add(pId);
+                pCallback && pCallback(pId);
+            }
+            if(pId in self.ItemTree)
+            {
+                // catch errors if the item tree got calculated while inserting / deleting items
+                self.ItemTree[pId].ids.forEach(_traverseChilds);
+            }
+        }
+    }
+    _traverseChilds(pItemId);
+    return Array.from(childIds);
+}
+
 /**
  * Recalculates the prices of the given items
  * If no items were passed, all elements of the offer/order will be recalculated
diff --git a/process/Offer_lib/process.js b/process/Offer_lib/process.js
index 4f4be22c23c..283b8c612e4 100644
--- a/process/Offer_lib/process.js
+++ b/process/Offer_lib/process.js
@@ -531,6 +531,7 @@ OfferUtils.getOfferTitleById = function (pOfferId)
 /**
  * Provides methods for dealing with offer items.
  * Inherits methods from abstract class ItemUtils.
+ * The inherited methods must be manually forwarded, since the order of imports is not guaranteed and thus 'ItemUtils' may not be always defined.
  * For documentation, see class ItemUtils.
  * 
  * @class
@@ -542,6 +543,15 @@ function OfferItemUtils(pOfferId) {
     OfferItemUtils.prototype.constructor = OfferItemUtils;
 }
 
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.traverse = function(pItemId, pCallback)
+{
+    this.initItemTree();
+    return ItemUtils.prototype.traverse.call(this, pItemId, pCallback);
+}
+
 /**
  * For documentation, see class ItemUtils.
  */
diff --git a/process/Order_lib/process.js b/process/Order_lib/process.js
index 3cfcb2dc8c4..8409b077709 100644
--- a/process/Order_lib/process.js
+++ b/process/Order_lib/process.js
@@ -643,6 +643,7 @@ OrderUtils.openReminderReport = function (pOrderID)
 /**
  * Provides methods for dealing with order items.
  * Inherits methods from abstract class ItemUtils.
+ * The inherited methods must be manually forwarded, since the order of imports is not guaranteed and thus 'ItemUtils' may not be always defined.
  * For documentation, see class ItemUtils.
  * 
  * @class
@@ -654,6 +655,15 @@ function OrderItemUtils(pSourceOfferId) {
     OrderItemUtils.prototype.constructor = OrderItemUtils;
 }
 
+/**
+ * For documentation, see class ItemUtils.
+ */
+OrderItemUtils.prototype.traverse = function(pItemId, pCallback)
+{
+    this.initItemTree();
+    return ItemUtils.prototype.traverse.call(this, pItemId, pCallback);
+}
+
 /**
  * For documentation, see class ItemUtils.
  */
diff --git a/process/Util_lib/process.js b/process/Util_lib/process.js
index 5baa6ba7c65..1e35561b2c5 100644
--- a/process/Util_lib/process.js
+++ b/process/Util_lib/process.js
@@ -11,7 +11,6 @@ import("system.vars");
 import("system.question");
 import("system.eMath");
 import("system.datetime");
-import("Offer_lib");
 import("Date_lib");
 
 
-- 
GitLab