[ofbiz-framework] branch trunk updated: Improved: Convert ProductServices.xml mini lang to groovy (OFBIZ-10231)

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

[ofbiz-framework] branch trunk updated: Improved: Convert ProductServices.xml mini lang to groovy (OFBIZ-10231)

mbrohl
This is an automated email from the ASF dual-hosted git repository.

mbrohl pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new e0a26fc  Improved: Convert ProductServices.xml mini lang to groovy (OFBIZ-10231)
e0a26fc is described below

commit e0a26fce43eec7c84d87c0d5055ff0a87f2af796
Author: Michael Brohl <[hidden email]>
AuthorDate: Fri Feb 21 16:59:37 2020 +0100

    Improved: Convert ProductServices.xml mini lang to groovy
    (OFBIZ-10231)
   
    Thanks Dennis Balkir for reporting and Sebastian Berg for the implementation.
---
 .../product/product/ProductServices.groovy         | 1095 ++++++++++++++++++++
 .../minilang/product/product/ProductServices.xml   | 1051 -------------------
 applications/product/servicedef/services.xml       |  112 +-
 3 files changed, 1151 insertions(+), 1107 deletions(-)

diff --git a/applications/product/groovyScripts/product/product/ProductServices.groovy b/applications/product/groovyScripts/product/product/ProductServices.groovy
new file mode 100644
index 0000000..b4be894
--- /dev/null
+++ b/applications/product/groovyScripts/product/product/ProductServices.groovy
@@ -0,0 +1,1095 @@
+/*
+  * Licensed to the Apache Software Foundation (ASF) under one
+  * or more contributor license agreements.  See the NOTICE file
+  * distributed with this work for additional information
+  * regarding copyright ownership.  The ASF licenses this file
+  * to you under the Apache License, Version 2.0 (the
+  * "License"); you may not use this file except in compliance
+  * with the License.  You may obtain a copy of the License at
+  *
+  * http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing,
+  * software distributed under the License is distributed on an
+  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  * KIND, either express or implied.  See the License for the
+  * specific language governing permissions and limitations
+  * under the License.
+  */
+
+
+import java.sql.Timestamp
+
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.base.util.UtilProperties
+import org.apache.ofbiz.base.util.UtilValidate
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.entity.serialize.XmlSerializer
+import org.apache.ofbiz.entity.util.EntityUtil
+import org.apache.ofbiz.product.product.KeywordIndex
+import org.apache.ofbiz.product.product.ProductWorker
+import org.apache.ofbiz.service.ServiceUtil
+
+
+
+ module = "ProductServices.groovy" // this is used for logging
+
+ /**
+  * Create a Product
+  */
+ def createProduct() {
+    Map result = success()
+    if (!(security.hasEntityPermission("CATALOG", "_CREATE", parameters.userLogin)
+        || security.hasEntityPermission("CATALOG_ROLE", "_CREATE", parameters.userLogin))) {
+            return error(UtilProperties.getMessage("ProductUiLabels", "ProductCatalogCreatePermissionError", parameters.locale))
+    }
+
+    GenericValue newEntity = makeValue("Product")
+    newEntity.setNonPKFields(parameters)
+    
+    newEntity.productId = parameters.productId
+
+    if (UtilValidate.isEmpty(newEntity.productId)) {
+        newEntity.productId = delegator.getNextSeqId("Product")
+    } else {
+        String errorMessage = UtilValidate.checkValidDatabaseId(newEntity.productId)
+        if(errorMessage != null) {
+            logError(errorMessage)
+            return error(errorMessage)
+        }
+        GenericValue dummyProduct = findOne("Product", ["productId": parameters.productId], false)
+        if (UtilValidate.isNotEmpty(dummyProduct)) {
+             errorMessage = UtilProperties.getMessage("CommonErrorUiLabels", CommonErrorDuplicateKey, parameters.locale)
+            logError(errorMessage)
+            return error(errorMessage)
+        }
+    }
+    result.productId = newEntity.productId
+    
+    Timestamp nowTimestamp = UtilDateTime.nowTimestamp()
+    
+    newEntity.createdDate = nowTimestamp
+    newEntity.lastModifiedDate = nowTimestamp
+    newEntity.lastModifiedByUserLogin = userLogin.userLoginId
+    newEntity.createdByUserLogin = userLogin.userLoginId
+    
+    if (UtilValidate.isEmpty(newEntity.isVariant)) {
+        newEntity.isVariant = "N"
+    }
+    if (UtilValidate.isEmpty(newEntity.isVirtual)) {
+        newEntity.isVirtual = "N"
+    }
+    if (UtilValidate.isEmpty(newEntity.billOfMaterialLevel)) {
+        newEntity.billOfMaterialLevel = (Long) 0
+    }
+    
+    newEntity.create()
+    
+    /*
+     *  if setting the primaryProductCategoryId create a member entity too
+     *  THIS IS REMOVED BECAUSE IT CAUSES PROBLEMS FOR WORKING ON PRODUCTION SITES
+     *  <if-not-empty field="newEntity.primaryProductCategoryId">
+     *  <make-value entity-name="ProductCategoryMember" value-field="newMember"/>
+     *  <set from-field="productId" map-name="newEntity" to-field-name="productId" to-map-name="newMember"/>
+     *  <set from-field="primaryProductCategoryId" map-name="newEntity" to-field-name="productCategoryId" to-map-name="newMember"/>
+     *  <now-timestamp field="nowStamp"/>
+     *  <set from-field="nowStamp" field="newMember.fromDate"/>
+     *  <create-value value-field="newMember"/>
+     *   </if-not-empty>
+     */
+
+    // if the user has the role limited position, add this product to the limit category/ies
+
+
+    if (security.hasEntityPermission("CATALOG_ROLE","_CREATE", parameters.userLogin)) {
+        List productCategoryRoles = from("ProductCategoryRole").where("partyId": userLogin.partyId, "roleTypeId": "LTD_ADMIN").queryList()
+        
+        for (GenericValue productCategoryRole : productCategoryRoles) {
+            // add this new product to the category
+            GenericValue newLimitMember = makeValue("ProductCategoryMember")
+            newLimitMember.productId = newEntity.productId
+            newLimitMember.productCateogryId = productCategoryRole.productCategoryId
+            newLimitMember.fromDate = nowTimestamp
+            newLimitMember.create()
+        }
+    }
+
+    return result
+}
+
+/**
+ * Update a product
+ */
+def updateProduct() {
+    Map res = checkProductRelatedPermission("updateProduct", "UPDATE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    GenericValue lookedUpValue = findOne("Product", ["productId": parameters.productId], false)
+    // save this value before overwriting it so we can compare it later
+    Map saveIdMap = ["primaryProductCategoryId": lookedUpValue.primaryProductCategoryId]
+    
+    lookedUpValue.setNonPKFields(parameters)
+    lookedUpValue.lastModifiedDate = UtilDateTime.nowTimestamp()
+    lookedUpValue.lastModifiedByUserLogin = userLogin.userLoginId
+    lookedUpValue.store()
+
+    return success()
+ }
+
+ /**
+  * Update a Product Name from quick admin
+  */
+def updateProductQuickAdminName() {
+    Map res = checkProductRelatedPermission("updateQuickAdminName", "UPDATE")
+
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    GenericValue lookedUpValue = findOne("Product", ["productId": parameters.productId], false)
+    lookedUpValue.productName = parameters.productName
+    if ("Y".equals(lookedUpValue.isVirtual)) {
+        lookedUpValue.internalName = lookedUpValue.productName
+    }
+    
+    lookedUpValue.lastModifiedDate = UtilDateTime.nowTimestamp();
+    lookedUpValue.lastModifiedByUserLogin = userLogin.userLoginId
+    
+    lookedUpValue.store()
+    
+    if ("Y".equals(lookedUpValue.isVirtual)) {
+        // get all variant products, to update their productNames
+        Map variantProductAssocMap = ["productId": parameters.productId, "productAssocTypeId": "PRODUCT_VARIANT"]
+        
+        // get all productAssocs, then get the actual product to update
+        List variantProductAssocs = from("ProductAssoc").where(variantProductAssocMap).queryList()
+        variantProductAssocs = EntityUtil.filterByDate(variantProductAssocs)
+        for(GenericValue variantProductAssoc : variantProductAssocs) {
+            GenericValue variantProduct = null
+            variantProduct = findOne("Product", ["productId": variantProductAssoc.productIdTo], false)
+            
+            variantProduct.productName = parameters.productName
+            variantProduct.lastModifiedDate = UtilDateTime.nowTimestamp()
+            variantProduct.lastModifiedByUserLogin = userLogin.userLoginId
+            variantProduct.store()
+        }
+    }
+    return success()
+}
+
+/**
+ * Duplicate a Product
+ */
+def duplicateProduct() {
+    String callingMethodName = "duplicateProduct"
+    String checkAction = "CREATE"
+    Map res = checkProductRelatedPermission(callingMethodName, checkAction)
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    checkAction = "DELETE"
+    res = checkProductRelatedPermission(callingMethodName, checkAction)
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    GenericValue dummyProduct = findOne("Product", ["productId": parameters.productId], false)
+    if (UtilValidate.isNotEmpty(dummyProduct)) {
+        String errorMessage = UtilProperties.getMessage("CommonErrorUiLabels", CommonErrorDuplicateKey, parameters.locale)
+        logError(errorMessage)
+        return error(errorMessage)
+    }
+    
+    // look up the old product and clone it
+    GenericValue oldProduct = findOne("Product", ["productId": parameters.oldProductId], false)
+    GenericValue newProduct = oldProduct.clone()
+    
+    // set the productId, and write it to the datasource
+    newProduct.productId = parameters.productId
+    
+    // if requested, set the new internalName field
+    if (UtilValidate.isNotEmpty(parameters.newInternalName)) {
+        newProduct.internalName = parameters.newInternalName
+    }
+    
+    // if requested, set the new productName field
+    if (UtilValidate.isNotEmpty(parameters.newProductName)) {
+        newProduct.productName = parameters.newProductName
+    }
+    
+    // if requested, set the new description field
+    if (UtilValidate.isNotEmpty(parameters.newDescription)) {
+        newProduct.description = parameters.newDescription
+    }
+    
+    // if requested, set the new longDescription field
+    if (UtilValidate.isNotEmpty(parameters.newLongDescription)) {
+        newProduct.longDescription = parameters.newLongDescription
+    }
+    
+    newProduct.create()
+    
+    // set up entity filter
+    Map productFindContext = ["productId": parameters.oldProductId]
+    Map reverseProductFindContext = ["productIdTo": parameters.oldProductId]
+    
+    // if requested, duplicate related data as well
+    if (UtilValidate.isNotEmpty(parameters.duplicatePrices)) {
+        List foundValues = from("ProductPrice").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateIDs)) {
+        List foundValues = from("GoodIdentification").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateContent)) {
+        List foundValues = from("ProductContent").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateCategoryMembers)) {
+        List foundValues = from("ProductCategoryMember").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateAssocs)) {
+        List foundValues = from("ProductAssoc").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+        
+        // small difference here, also do the reverse assocs...
+        foundValues = from("ProductAssoc").where("productIdTo": parameters.oldProductId).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productIdTo = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateAttributes)) {
+        List foundValues = from("ProductAttribute").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateFeatureAppls)) {
+        List foundValues = from("ProductFeatureAppl").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            newTempValue.create()
+        }
+    }
+    if (UtilValidate.isNotEmpty(parameters.duplicateInventoryItems)) {
+        List foundValues = from("InventoryItem").where(productFindContext).queryList()
+        for (GenericValue foundValue : foundValues) {
+            /*
+             *      NOTE: new inventory items should always be created calling the
+             *            createInventoryItem service because in this way we are sure
+             *            that all the relevant fields are filled with default values.
+             *            However, the code here should work fine because all the values
+             *            for the new inventory item are inerited from the existing item.
+             *      TODO: is this code correct? What is the meaning of duplicating inventory items?
+             *            What about the InventoryItemDetail entries?
+             */
+            GenericValue newTempValue = foundValue.clone()
+            newTempValue.productId = parameters.productId
+            // this one is slightly different because it needs a new sequenced inventoryItemId
+            newTempValue.inventoryItemId = delegator.getNextSeqId("InventoryItem")
+            newTempValue.create()
+        }
+    }
+    
+    // if requested, remove related data as well
+    if (UtilValidate.isNotEmpty(parameters.removePrices)) {
+        delegator.removeByAnd("ProductPrice", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeIDs)) {
+        delegator.removeByAnd("GoodIdentification", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeContent)) {
+        delegator.removeByAnd("ProductContent", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeCategoryMembers)) {
+        delegator.removeByAnd("ProductCategoryMember", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeAssocs)) {
+        delegator.removeByAnd("ProductAssoc", productFindContext)
+        // small difference here, also do the reverse assocs...
+        delegator.removeByAnd("ProductAssoc", reverseProductFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeAttributes)) {
+        delegator.removeByAnd("ProductAttribute", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeFeatureAppls)) {
+        delegator.removeByAnd("ProductFeatureAppl", productFindContext)
+    }
+    if (UtilValidate.isNotEmpty(parameters.removeInventoryItems)) {
+        delegator.removeByAnd("InventoryItem", productFindContext)
+    }
+    return success()
+}
+
+// Product Keyword Services
+
+/**
+ * induce all the keywords of a product
+ */
+def forceIndexProductKeywords() {
+    GenericValue product = findOne("Product", [productId: parameters.productId], false)
+    KeywordIndex.forceIndexKeywords(product)
+    return success()
+}
+
+/**
+ * delete all the keywords of a produc
+ */
+def deleteProductKeywords() {
+    GenericValue product = findOne("Product", [productId: parameters.productId], false)
+    delegator.removeRelated("ProductKeyword", product)
+    return success()
+}
+
+/**
+ * Index the Keywords for a Product
+ */
+def indexProductKeywords() {
+    //this service is meant to be called from an entity ECA for entities that include a productId
+    //if it is the Product entity itself triggering this action, then a [productInstance] parameter
+    //will be passed and we can save a few cycles looking that up
+    GenericValue productInstance = parameters.productInstance
+    if (productInstance == null) {
+        Map findProductMap = [productId: parameters.productId]
+        productInstance = findOne("Product", findProductMap, false)
+    }
+    //induce keywords if autoCreateKeywords is empty or Y
+    if (UtilValidate.isEmpty(productInstance.autoCreateKeywords) || "Y".equals(productInstance.autoCreateKeywords)) {
+        KeywordIndex.indexKeywords(productInstance)
+    }
+    return success()
+}
+
+/**
+ *  Discontinue Product Sales
+ *  set sales discontinuation date to now
+ */
+def discontinueProductSales() {
+    // set sales discontinuation date to now
+    Timestamp nowTimestamp = UtilDateTime.nowTimestamp()
+    GenericValue product = findOne("Product", parameters, false)
+    product.salesDiscontinuationDate = nowTimestamp
+    product.store()
+    
+    // expire product from all categories
+    List productCategoryMembers = delegator.getRelated("ProductCategoryMember", null, null, product, false)
+    for (GenericValue productCategoryMember : productCategoryMembers) {
+        if (UtilValidate.isEmpty(productCategoryMember.thruDate)) {
+            productCategoryMember.thruDate = UtilDateTime.nowTimestamp()
+            productCategoryMember.store()
+        }
+    }
+    // expire product from all associations going to it
+    List assocProductAssocs = delegator.getRelated("AssocProductAssoc", null, null, product, false)
+    for (GenericValue assocProductAssoc : assocProductAssocs) {
+        if (UtilValidate.isEmpty(assocProductAssoc.thruDate)) {
+            assocProductAssoc.thruDate = UtilDateTime.nowTimestamp()
+            assocProductAssoc.store()
+        }
+    }
+    return success()
+}
+
+
+def countProductView() {
+    if (UtilValidate.isEmpty(parameters.weight)) {
+        parameters.weight = (Long) 1
+    }
+    GenericValue productCalculatedInfo = findOne("ProductCalculatedInfo", ["productId": parameters.productId], false)
+    if (UtilValidate.isEmpty(productCalculatedInfo)) {
+        // go ahead and create it
+        productCalculatedInfo = makeValue("ProductCalculatedInfo")
+        productCalculatedInfo.productId = parameters.productId
+        productCalculatedInfo.totalTimesViewed = parameters.weight
+        productCalculatedInfo.create()
+    } else {
+        productCalculatedInfo.totalTimesViewed = productCalculatedInfo.totalTimesViewed + parameters.weight
+        productCalculatedInfo.store()
+    }
+    
+    // do the same for the virtual product...
+    GenericValue product = findOne("Product", ["productId": parameters.productId], true)
+    ProductWorker productWorker = new ProductWorker()
+    String virtualProductId = productWorker.getVariantVirtualId(product)
+    if (UtilValidate.isNotEmpty(virtualProductId)) {
+        Map callSubMap = ["productId": virtualProductId, "weight": parameters.weight]
+        run service: "countProductView", with: callSubMap
+    }
+    return success()
+    
+}
+
+/**
+ * Create a ProductReview
+ */
+def createProductReview() {
+    GenericValue newEntity = makeValue("ProductReview", parameters)
+    newEntity.userLoginId = userLogin.userLoginId
+    newEntity.statusId = "PRR_PENDING"
+    
+    // code to check for auto-approved reviews (store setting)
+    GenericValue productStore = findOne("ProductStore", ["productStoreId": parameters.productStoreId], false)
+    
+    if (!UtilValidate.isEmpty(productStore)) {
+        if ("Y".equals(productStore.autoApproveReviews)) {
+            newEntity.statusId = "PRR_APPROVED"
+        }
+    }
+    
+    // create the new ProductReview
+    newEntity.productReviewId = delegator.getNextSeqId("ProductReview")
+    Map result = success()
+    result.productReviewId = newEntity.productReviewId
+    
+    if (UtilValidate.isEmpty(newEntity.postedDateTime)) {
+        newEntity.postedDateTime = UtilDateTime.nowTimestamp()
+    }
+    
+    newEntity.create()
+    
+    String productId = newEntity.productId
+    String successMessage = UtilProperties.getMessage("ProductUiLabels", "ProductCreateProductReviewSuccess", parameters.locale)
+    updateProductWithReviewRatingAvg(productId)
+    
+    return result
+}
+
+/**
+ *  Update ProductReview
+ */
+def updateProductReview() {
+    Map res = checkProductRelatedPermission("updateProductReview", "UPDATE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    GenericValue lookupPKMap = makeValue("ProductReview")
+    lookupPKMap.setPKFields(parameters)
+    GenericValue lookedUpValue = findOne("ProductReview", lookupPKMap, false)
+    lookupPKMap.setNonPKFields(parameters)
+    lookupPKMap.store()
+    
+    String productId = lookedUpValue.productId
+    updateProductWithReviewRatingAvg(productId)
+    
+    return success()
+}
+
+/**
+ * change the product review Status
+ */
+def setProductReviewStatus(){
+    Map res = checkProductRelatedPermission("setProductReviewStatus", "UPDATE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    GenericValue productReview = findOne("ProductReview", parameters, false)
+    if (UtilValidate.isNotEmpty(productReview)) {
+        if (!productReview.statusId.equals(parameters.statusId)) {
+            GenericValue statusChange = from("StatusValidChange")
+                .where("statusId", productReview.statusId, "statusIdTo", parameters.statusId)
+                .queryOne()
+            if (UtilValidate.isEmpty(statusChange)) {
+                String msg = "Status is not a valid change: from " + productReview.statusId + " to " + parameters.statusId
+                logError(msg)
+                String errorMessage = UtilProperties.getMessage("ProductErrorUiLabels", ProductReviewErrorCouldNotChangeOrderStatusFromTo, parameters.locale)
+                logError(errorMessage)
+                return error(errorMessage)
+            }
+        }
+    }
+    
+    productReview.statusId = parameters.statusId
+    productReview.store()
+    Map result = success()
+    result.productReviewId = productReview.productReviewId
+    
+    return result
+}
+
+/**
+ * Update Product with new Review Rating Avg
+ * this method is meant to be called in-line and depends in a productId parameter
+ */
+def updateProductWithReviewRatingAvg(String productId) {
+    ProductWorker productWorker = new ProductWorker()
+    BigDecimal averageCustomerRating = productWorker.getAverageProductRating(delegator, productId)
+    logInfo("Got new average customer rating "+ averageCustomerRating)
+    
+    if (averageCustomerRating == 0) {
+        return success()
+    }
+    
+    // update the review average on the ProductCalculatedInfo entity
+    GenericValue productCalculatedInfo = findOne("ProductCalculatedInfo", parameters, false)
+    if (UtilValidate.isEmpty(productCalculatedInfo)) {
+        // go ahead and create it
+        productCalculatedInfo = makeValue("ProductCalculatedInfo")
+        productCalculatedInfo.productId = productId
+        productCalculatedInfo.averageCustomerRating = averageCustomerRating
+        productCalculatedInfo.create()
+    } else {
+        productCalculatedInfo.averageCustomerRating = averageCustomerRating
+        productCalculatedInfo.store()
+    }
+    
+    return success()
+}
+
+/**
+ * Updates the Product's Variants
+ */
+def copyToProductVariants() {
+    String callingMethodName = "copyToProductVariants"
+    String checkAction = "CREATE"
+    Map res = checkProductRelatedPermission(callingMethodName, checkAction)
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    checkAction = "DELETE"
+    res = checkProductRelatedPermission(callingMethodName, checkAction)
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    Map productFindContext = ["productId": parameters.virtualProductId]
+    GenericValue oldProduct = findOne("Product", productFindContext, false)
+    
+    Map variantsFindContext = ["productId": parameters.virtualProductId, "productAssocTypeId": "PRODUCT_VARIANT"]
+
+    List variants = from("ProductAssoc").where(variantsFindContext).filterByDate().queryList()
+    List foundVariantValues = []
+    List foundValues = []
+    for (GenericValue newProduct : variants) {
+        Map productVariantContext = ["productId": newProduct.productIdTo]
+        // if requested, duplicate related data
+        if (UtilValidate.isNotEmpty(parameters.duplicatePrices)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductPrice").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductPrice").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateIDs)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("GoodIdentification").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("GoodIdentification").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+                }
+            
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateContent)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductContent").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductContent").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateCategoryMembers)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductCategoryMember").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductCategoryMember").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateAttributes)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductAttribute").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductAttribute").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateFacilities)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductFacility").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductFacility").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+        if (UtilValidate.isNotEmpty(parameters.duplicateLocations)) {
+            if (UtilValidate.isNotEmpty(parameters.removeBefore)) {
+                foundVariantValues = from("ProductFacilityLocation").where(productVariantContext).queryList()
+                for (GenericValue foundVariantValue : foundVariantValues) {
+                    foundVariantValue.remove()
+                }
+            }
+            foundValues = from("ProductFacilityLocation").where(productFindContext).queryList()
+            for (GenericValue foundValue : foundValues) {
+                GenericValue newTempValue = foundValue.clone()
+                newTempValue.productId = newProduct.productIdTo
+                newTempValue.create()
+            }
+        }
+    }
+    return success()
+}
+
+/**
+ * Check Product Related Permission
+ * a method to centralize product security code, meant to be called in-line with
+ * call-simple-method, and the checkAction and callingMethodName attributes should be in the method context
+ */
+def checkProductRelatedPermission (String callingMethodName, String checkAction){
+    if (UtilValidate.isEmpty(callingMethodName)) {
+        callingMethodName = UtilProperties.getMessage("CommonUiLabels", "CommonPermissionThisOperation", parameters.locale)
+    }
+    if (UtilValidate.isEmpty(checkAction)) {
+        checkAction = "UPDATE"
+    }
+    List roleCategories = []
+    // find all role-categories that this product is a member of
+    if (!security.hasEntityPermission("CATALOG", "_${checkAction}", parameters.userLogin)) {
+        Map lookupRoleCategoriesMap = ["productId": parameters.productId, "partyId": userLogin.partyId, "roleTypeId": "LTD_ADMIN"]
+        roleCategories = from("ProductCategoryMemberAndRole").where(lookupRoleCategoriesMap).filterByDate("roleFromDate", "roleThruDate").queryList()
+    }
+    
+    if (! ((security.hasEntityPermission("CATALOG", "_${checkAction}", parameters.userLogin))
+        || (security.hasEntityPermission("CATALOG_ROLE", "_${checkAction}", parameters.userLogin) && !UtilValidate.isEmpty(roleCategories))
+        || (!UtilValidate.isEmpty(parameters.alternatePermissionRoot) && security.hasEntityPermission(parameters.alternatePermissionRoot, checkAction, parameters.userLogin)))) {
+            String checkActionLabel = "ProductCatalog" + checkAction.charAt(0) + checkAction.substring(1).toLowerCase() + "PermissionError"
+            String resourceDescription = callingMethodName
+            
+            String errorMessage = UtilProperties.getMessage("ProductUiLabels", checkActionLabel, parameters.locale)
+            logError(errorMessage)
+            return error(errorMessage)
+        }
+    return success()
+}
+
+/**
+ * Main permission logic
+ */
+def productGenericPermission(){
+    String mainAction = parameters.mainAction
+    Map result = success()
+    if (UtilValidate.isEmpty(mainAction)) {
+        String errorMessage = UtilProperties.getMessage("ProductUiLabels", "ProductMissingMainActionInPermissionService", parameters.locale)
+        logError(errorMessage)
+        return error(errorMessage)
+    }
+    Map res = checkProductRelatedPermission(parameters.resourceDescription, parameters.mainAction)
+    if (!ServiceUtil.isSuccess(res)) {
+        String failMessage = UtilProperties.getMessage("ProductUiLabels", "ProductPermissionError", parameters.locale)
+        Boolean hasPermission = false
+        result = fail(failMessage)
+        result.hasPermission = hasPermission
+    } else {
+        Boolean hasPermission = true
+        result.hasPermission = hasPermission
+    }
+    return result
+}
+
+/**
+ * product price permission logic
+ */
+def productPriceGenericPermission(){
+    String mainAction = parameters.mainAction
+    if (UtilValidate.isEmpty(mainAction)) {
+        String errorMessage = UtilProperties.getMessage("ProductUiLabels", "ProductMissingMainActionInPermissionService", parameters.locale)
+        logError(errorMessage)
+        return error(errorMessage)
+    }
+    Map result = success()
+    if (!security.hasEntityPermission("CATALOG_PRICE_MAINT", null, parameters.userLogin)) {
+        String errorMessage = UtilProperties.getMessage("ProductUiLabels", "ProductPriceMaintPermissionError", parameters.locale)
+        logError(errorMessage)
+        result = error(errorMessage)
+    }
+    Map res = checkProductRelatedPermission(null, null)
+    if (ServiceUtil.isSuccess(result) && ServiceUtil.isSuccess(res)) {
+        result.hasPermission = true
+    } else {
+        String failMessage = UtilProperties.getMessage("ProductUiLabels", "ProductPermissionError", parameters.locale)
+        result = fail(failMessage)
+        result.hasPermission = false
+    }
+    return result
+}
+
+/**
+ * ================================================================
+ * ProductRole Services
+ * ================================================================
+ */
+
+
+/**
+ * Add Party to Product
+ */
+def addPartyToProduct(){
+    Map result = checkProductRelatedPermission("addPartyToProduct", "CREATE")
+    if (!ServiceUtil.isSuccess(result)) {
+        return result
+    }
+    GenericValue newEntity = makeValue("ProductRole", parameters)
+    
+    if (UtilValidate.isEmpty(newEntity.fromDate)) {
+        newEntity.fromDate = UtilDateTime.nowTimestamp()
+    }
+    newEntity.create()
+    return success()
+}
+
+/**
+ * Update Party to Product
+ */
+def updatePartyToProduct(){
+    Map result = checkProductRelatedPermission("updatePartyToProduct", "UPDATE")
+    if (!ServiceUtil.isSuccess(result)) {
+        return result
+    }
+    GenericValue lookupPKMap = makeValue("ProductRole")
+    lookupPKMap.setPKFields(parameters)
+    GenericValue lookedUpValue = findOne("ProductRole", lookupPKMap, false)
+    lookedUpValue.setNonPKFields(parameters)
+    lookedUpValue.store()
+    return success()
+}
+
+/**
+ * Remove Party From Product
+ */
+def removePartyFromProduct(){
+    Map res = checkProductRelatedPermission("removePartyFromProduct", "DELETE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    Map lookupPKMap = makeValue("ProductRole")
+    lookupPKMap.setPKFields(parameters)
+    GenericValue lookedUpValue = findOne("ProductRole", lookupPKMap, false)
+    lookedUpValue.remove()
+    
+    return success()
+}
+
+// ProductCategoryGlAccount methods
+ /**
+  * Create a ProductCategoryGlAccount
+  */
+def createProductCategoryGlAccount(){
+    Map res = checkProductRelatedPermission("createProductCategoryGlAccount", "CREATE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    GenericValue newEntity = makeValue("ProductCategoryGlAccount", parameters)
+    newEntity.create()
+    
+    return success()
+}
+
+/**
+ * Update a ProductCategoryGlAccount
+ */
+def updateProductCategoryGlAccount(){
+    Map res = checkProductRelatedPermission("updateProductCategoryGlAccount", "UPDATE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    
+    GenericValue lookedUpValue = findOne("ProductCategoryGlAccount", parameters, false)
+    lookedUpValue.setNonPKFields(parameters)
+    lookedUpValue.store()
+    
+    return success()
+}
+
+/**
+ * Delete a ProductCategoryGlAccount
+ */
+def deleteProductCategoryGlAccount(){
+    Map res = checkProductRelatedPermission("deleteProductCategorGLAccount", "DELETE")
+    if (!ServiceUtil.isSuccess(res)) {
+        return res
+    }
+    GenericValue lookedUpValue = findOne("ProductCategoryGlAccount", parameters, false)
+    lookedUpValue.remove()
+    
+    return success()
+}
+
+// Product GroupOrder Services -->
+
+/**
+ * Create ProductGroupOrder
+ */
+def createProductGroupOrder(){
+    GenericValue newEntity = makeValue("ProductGroupOrder")
+    delegator.setNextSubSeqId(newEntity, "groupOrderId", 5, 1)
+    Map result = success()
+    result.groupOrderId = newEntity.groupOrderId
+    newEntity.setNonPKFields(parameters)
+    newEntity.create()
+    
+    return result
+}
+
+/**
+ * Update ProductGroupOrder
+ */
+def updateProductGroupOrder(){
+    GenericValue productGroupOrder = findOne("ProductGroupOrder", ["groupOrderId": parameters.groupOrderId], false)
+    productGroupOrder.setNonPKFields(parameters)
+    productGroupOrder.store()
+    
+    if ("GO_CREATED".equals(productGroupOrder.statusId)) {
+        GenericValue jobSandbox = findOne("JobSandbox", ["jobId": productGroupOrder.jobId], false)
+        if (UtilValidate.isNotEmpty(jobSandbox)) {
+            jobSandbox.runTime = parameters.thruDate
+            jobSandbox.store()
+        }
+    }
+    return success()
+}
+
+/**
+ * Delete ProductGroupOrder
+ */
+def deleteProductGroupOrder(){
+    List orderItemGroupOrders = from("OrderItemGroupOrder").where("groupOrderId": parameters.groupOrderId).queryList()
+    for (GenericValue orderItemGroupOrder : orderItemGroupOrders) {
+        orderItemGroupOrder.remove()
+    }
+    GenericValue productGroupOrder = findOne("ProductGroupOrder", ["groupOrderId": parameters.groupOrderId], false)
+    if (UtilValidate.isEmpty(productGroupOrder)) {
+        return error("Entity value not found with name: " + productGroupOrder)
+    }
+    productGroupOrder.remove()
+    
+    GenericValue jobSandbox = findOne("JobSandbox", ["jobId": productGroupOrder.jobId], false)
+    if (UtilValidate.isEmpty(jobSandbox)) {
+        return error("Entity value not found with name: " + jobSandbox)
+    }
+    jobSandbox.remove()
+    
+    List jobSandboxList = from("JobSandbox").where("runtimeDataId": jobSandbox.runtimeDataId).queryList()
+    for (GenericValue jobSandboxRelatedRuntimeData : jobSandboxList) {
+        jobSandboxRelatedRuntimeData.remove()
+    }
+    
+    GenericValue runtimeData = findOne("RuntimeData", ["runtimeDataId": jobSandbox.runtimeDataId], false)
+    if (UtilValidate.isEmpty(runtimeData)) {
+        return error("Entity value not found with name: " + runtimeData)
+    }
+    runtimeData.remove()
+    
+    return success()
+}
+
+/**
+ * Create ProductGroupOrder
+ */
+def createJobForProductGroupOrder(){
+    GenericValue productGroupOrder = findOne("ProductGroupOrder", ["groupOrderId": parameters.groupOrderId], false)
+    if (UtilValidate.isEmpty(productGroupOrder.jobId)) {
+        // Create RuntimeData For ProductGroupOrder
+        Map runtimeDataMap = ["groupOrderId": parameters.groupOrderId]
+        XmlSerializer xmlSerializer = new XmlSerializer()
+        String runtimeInfo = xmlSerializer.serialize(runtimeDataMap)
+        
+        GenericValue runtimeData = makeValue("RuntimeData")
+        runtimeData.runtimeDataId = delegator.getNextSeqId("RuntimeData")
+        String runtimeDataId = runtimeData.runtimeDataId
+        runtimeData.runtimeInfo = runtimeInfo
+        runtimeData.create()
+        
+        // Create Job For ProductGroupOrder
+        // FIXME: Jobs should not be manually created
+        GenericValue jobSandbox = makeValue("JobSandbox")
+        jobSandbox.jobId = delegator.getNextSeqId("JobSandbox")
+        String jobId = jobSandbox.jobId
+        jobSandbox.jobName = "Check ProductGroupOrder Expired"
+        jobSandbox.runTime = parameters.thruDate
+        jobSandbox.poolId = "pool"
+        jobSandbox.statusId = "SERVICE_PENDING"
+        jobSandbox.serviceName = "checkProductGroupOrderExpired"
+        jobSandbox.runAsUser = "system"
+        jobSandbox.runtimeDataId = runtimeDataId
+        jobSandbox.maxRecurrenceCount = (Long) 1
+        jobSandbox.priority = (Long) 50
+        jobSandbox.create()
+        
+        productGroupOrder.jobId = jobId
+        productGroupOrder.store()
+    }
+    return success()
+}
+
+/**
+ * Check OrderItem For ProductGroupOrder
+ */
+def checkOrderItemForProductGroupOrder(){
+    List orderItems = from("OrderItem").where("orderId": parameters.orderId).queryList()
+    for (GenericValue orderItem : orderItems) {
+        String productId = orderItem.productId
+        GenericValue product = findOne("Product", ["productId": orderItem.productId], false)
+        if ("Y".equals(product.isVariant)) {
+            List variantProductAssocs = from("ProductAssoc").where("productIdTo": orderItem.productId, "productAssocTypeId": "PRODUCT_VARIANT").queryList()
+            variantProductAssocs = EntityUtil.filterByDate(variantProductAssocs)
+            GenericValue variantProductAssoc = variantProductAssocs.get(0)
+            productId = variantProductAssoc.productId
+        }
+        List productGroupOrders = from("ProductGroupOrder").where("productId": productId).queryList()
+        if (UtilValidate.isNotEmpty(productGroupOrders)) {
+            productGroupOrders = EntityUtil.filterByDate(productGroupOrders)
+            GenericValue productGroupOrder = productGroupOrders.get(0)
+            if (UtilValidate.isEmpty(productGroupOrder.soldOrderQty)) {
+                productGroupOrder.soldOrderQty = orderItem.quantity
+            } else {
+            productGroupOrder.soldOrderQty = productGroupOrder.soldOrderQty + orderItem.quantity
+            }
+            productGroupOrder.store()
+            
+            Map createOrderItemGroupOrderMap = ["orderId": orderItem.orderId, "orderItemSeqId": orderItem.orderItemSeqId, "groupOrderId": productGroupOrder.groupOrderId]
+            
+            run service: "createOrderItemGroupOrder", with: createOrderItemGroupOrderMap
+        }
+    }
+    return success()
+}
+
+/**
+ * Cancle OrderItemGroupOrder
+ */
+def cancleOrderItemGroupOrder(){
+    List orderItems = []
+    if (UtilValidate.isNotEmpty(parameters.orderItemSeqId)) {
+        orderItems = from("OrderItem")
+            .where("orderId", parameters.orderId, "orderItemSeqId", parameters.orderItemSeqId)
+            .queryList()
+    } else {
+        orderItems = from("OrderItem")
+            .where("orderId", parameters.orderId)
+            .queryList()
+    }
+    for(GenericValue orderItem : orderItems) {
+        List orderItemGroupOrders = from("OrderItemGroupOrder")
+            .where("orderId", orderItem.orderId, "orderItemSeqId", orderItem.orderItemSeqId)
+            .queryList()
+        if (UtilValidate.isNotEmpty(orderItemGroupOrders)) {
+            GenericValue orderItemGroupOrder = orderItemGroupOrders.get(0)
+            GenericValue productGroupOrder = findOne("ProductGroupOrder", [groupOrderId: orderItemGroupOrder.groupOrderId], false)
+            
+            if (UtilValidate.isNotEmpty(productGroupOrder)) {
+                if ("GO_CREATED".equals(productGroupOrder.statusId)) {
+                    if ("ITEM_CANCELLED".equals(orderItem.statusId)) {
+                        BigDecimal cancelQuantity
+                        if (UtilValidate.isNotEmpty(orderItem.cancelQuantity)) {
+                            cancelQuantity = orderItem.cancelQuantity
+                        } else {
+                            cancelQuantity = orderItem.quantity
+                        }
+                        productGroupOrder.soldOrderQty = productGroupOrder.soldOrderQty - cancelQuantity
+                    }
+                    productGroupOrder.store()
+                    orderItemGroupOrder.remove()
+                }
+            }
+        }
+    }
+    return success()
+}
+
+/**
+ * Check ProductGroupOrder Expired
+ */
+def checkProductGroupOrderExpired(){
+    GenericValue productGroupOrder = findOne("ProductGroupOrder", parameters, false)
+    if (UtilValidate.isNotEmpty(productGroupOrder)) {
+        String groupOrderStatusId
+        String newItemStatusId
+        if (productGroupOrder.soldOrderQty >= productGroupOrder.reqOrderQty) {
+            newItemStatusId = "ITEM_APPROVED"
+            groupOrderStatusId = "GO_SUCCESS"
+        } else {
+            newItemStatusId = "ITEM_CANCELLED"
+            groupOrderStatusId = "GO_CANCELLED"
+        }
+    Map updateProductGroupOrderMap = [:]
+    updateProductGroupOrderMap.groupOrderId = productGroupOrder.groupOrderId
+    updateProductGroupOrderMap.statusId = groupOrderStatusId
+    run service: "updateProductGroupOrder", with: updateProductGroupOrderMap
+    
+    List orderItemGroupOrders = from("OrderItemGroupOrder")
+        .where("groupOrderId", productGroupOrder.groupOrderId)
+        .queryList()
+    for(GenericValue orderItemGroupOrder : orderItemGroupOrders) {
+        Map changeOrderItemStatusMap = ["orderId": orderItemGroupOrder.orderId, "orderItemSeqId": orderItemGroupOrder.orderItemSeqId, "statusId": newItemStatusId]
+        run service: "changeOrderItemStatus", with: changeOrderItemStatusMap
+    }
+    return success()
+    }
+}
+
diff --git a/applications/product/minilang/product/product/ProductServices.xml b/applications/product/minilang/product/product/ProductServices.xml
deleted file mode 100644
index b331b32..0000000
--- a/applications/product/minilang/product/product/ProductServices.xml
+++ /dev/null
@@ -1,1051 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements.  See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership.  The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License.  You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied.  See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<simple-methods xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xmlns="http://ofbiz.apache.org/Simple-Method" xsi:schemaLocation="http://ofbiz.apache.org/Simple-Method http://ofbiz.apache.org/dtds/simple-methods.xsd">
-    <simple-method method-name="createProduct" short-description="Create a Product">
-        <check-permission permission="CATALOG" action="_CREATE">
-            <alt-permission permission="CATALOG_ROLE" action="_CREATE"/>
-            <fail-property resource="ProductUiLabels" property="ProductCatalogCreatePermissionError"/>
-        </check-permission>
-        <check-errors/>
-
-        <make-value entity-name="Product" value-field="newEntity"/>
-        <set-nonpk-fields map="parameters" value-field="newEntity"/>
-
-        <set from-field="parameters.productId" field="newEntity.productId"/>
-        <if-empty field="newEntity.productId">
-            <sequenced-id sequence-name="Product" field="newEntity.productId"/>
-        <else>
-            <check-id field="newEntity.productId"/>
-            <check-errors />
-            <entity-one entity-name="Product" value-field="dummyProduct"><field-map field-name="productId" from-field="parameters.productId"/></entity-one>
-            <if-not-empty field="dummyProduct">
-                <add-error ><fail-property resource="CommonErrorUiLabels" property="CommonErrorDuplicateKey" /></add-error>
-            </if-not-empty>
-            <check-errors />
-        </else>
-        </if-empty>
-        <field-to-result field="newEntity.productId" result-name="productId"/>
-
-        <now-timestamp field="nowTimestamp"/>
-        <set from-field="nowTimestamp" field="newEntity.createdDate"/>
-        <set from-field="nowTimestamp" field="newEntity.lastModifiedDate"/>
-        <set from-field="userLogin.userLoginId" field="newEntity.lastModifiedByUserLogin"/>
-        <set from-field="userLogin.userLoginId" field="newEntity.createdByUserLogin"/>
-        <if-empty field="newEntity.isVariant">
-            <set field="newEntity.isVariant" value="N"/>
-        </if-empty>
-        <if-empty field="newEntity.isVirtual">
-            <set field="newEntity.isVirtual" value="N"/>
-        </if-empty>
-        <if-empty field="newEntity.billOfMaterialLevel">
-            <set field="newEntity.billOfMaterialLevel" value="0" type="Long"/>
-        </if-empty>
-
-        <create-value value-field="newEntity"/>
-
-        <!-- if setting the primaryProductCategoryId create a member entity too -->
-        <!-- THIS IS REMOVED BECAUSE IT CAUSES PROBLEMS FOR WORKING ON PRODUCTION SITES
-        <if-not-empty field="newEntity.primaryProductCategoryId">
-            <make-value entity-name="ProductCategoryMember" value-field="newMember"/>
-            <set from-field="productId" map-name="newEntity" to-field-name="productId" to-map-name="newMember"/>
-            <set from-field="primaryProductCategoryId" map-name="newEntity" to-field-name="productCategoryId" to-map-name="newMember"/>
-            <now-timestamp field="nowStamp"/>
-            <set from-field="nowStamp" field="newMember.fromDate"/>
-            <create-value value-field="newMember"/>
-        </if-not-empty>
-        -->
-
-        <!-- if the user has the role limited position, add this product to the limit category/ies -->
-        <if-has-permission permission="CATALOG_ROLE" action="_CREATE">
-            <entity-and entity-name="ProductCategoryRole" list="productCategoryRoles" filter-by-date="true">
-                <field-map field-name="partyId" from-field="userLogin.partyId"/>
-                <field-map field-name="roleTypeId" value="LTD_ADMIN"/>
-            </entity-and>
-
-            <iterate list="productCategoryRoles" entry="productCategoryRole">
-                <!-- add this new product to the category -->
-                <make-value entity-name="ProductCategoryMember" value-field="newLimitMember"/>
-                <set from-field="newEntity.productId" field="newLimitMember.productId"/>
-                <set from-field="productCategoryRole.productCategoryId" field="newLimitMember.productCategoryId"/>
-                <set from-field="nowTimestamp" field="newLimitMember.fromDate"/>
-                <create-value value-field="newLimitMember"/>
-            </iterate>
-        </if-has-permission>
-    </simple-method>
-    <simple-method method-name="updateProduct" short-description="Update a Product">
-        <set value="updateProduct" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <entity-one entity-name="Product" value-field="lookedUpValue"/>
-        <!-- save this value before overwriting it so we can compare it later -->
-        <set from-field="lookedUpValue.primaryProductCategoryId" field="saveIdMap.primaryProductCategoryId"/>
-        <set-nonpk-fields map="parameters" value-field="lookedUpValue"/>
-
-        <now-timestamp field="lookedUpValue.lastModifiedDate"/>
-        <set from-field="userLogin.userLoginId" field="lookedUpValue.lastModifiedByUserLogin"/>
-
-        <store-value value-field="lookedUpValue"/>
-
-        <!-- if setting the primaryParentCategoryId, create a rollup entity too -->
-        <!-- THIS IS REMOVED BECAUSE IT CAUSES PROBLEMS FOR WORKING ON PRODUCTION SITES
-        <if-not-empty field="lookedUpValue.primaryProductCategoryId">
-            <if-compare-field to-field="saveIdMap.primaryProductCategoryId" field="lookedUpValue.primaryProductCategoryId" operator="equals">
-                <make-value entity-name="ProductCategoryMember" value-field="newMember"/>
-                <set from-field="productId" map-name="newEntity" to-field-name="productId" to-map-name="newMember"/>
-                <set from-field="primaryProductCategoryId" map-name="newEntity" to-field-name="productCategoryId" to-map-name="newMember"/>
-                <now-timestamp field="newMember.fromDate"/>
-                <create-value value-field="newMember"/>
-            </if-compare-field>
-        </if-not-empty>
-        -->
-    </simple-method>
-
-    <!-- update the name of a product - handles real , virtual and variant products -->
-    <simple-method method-name="updateProductQuickAdminName" short-description="Update a Product Name from quick admin">
-        <set value="updateProductQuickAdminName" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <entity-one entity-name="Product" value-field="lookedUpValue"/>
-        <set from-field="parameters.productName" field="lookedUpValue.productName"/>
-        <if-compare field="lookedUpValue.isVirtual" operator="equals" value="Y">
-            <set from-field="lookedUpValue.productName" field="lookedUpValue.internalName"/>
-        </if-compare>
-
-        <now-timestamp field="lookedUpValue.lastModifiedDate"/>
-        <set from-field="userLogin.userLoginId" field="lookedUpValue.lastModifiedByUserLogin"/>
-
-        <store-value value-field="lookedUpValue"/>
-
-        <if-compare field="lookedUpValue.isVirtual" operator="equals" value="Y">
-            <!-- get all variant products, to update their productNames -->
-            <set from-field="parameters.productId" field="variantProductAssocMap.productId"/>
-            <set value="PRODUCT_VARIANT" field="variantProductAssocMap.productAssocTypeId"/>
-
-            <!-- get all productAssocs, then get the actual product to update -->
-            <find-by-and entity-name="ProductAssoc" map="variantProductAssocMap" list="variantProductAssocs"/>
-            <filter-list-by-date list="variantProductAssocs"/>
-            <iterate list="variantProductAssocs" entry="variantProductAssoc">
-                <clear-field field="variantProduct"/>
-                <entity-one entity-name="Product" value-field="variantProduct" auto-field-map="false">
-                    <field-map field-name="productId" from-field="variantProductAssoc.productIdTo"/>
-                </entity-one>
-
-                <set from-field="parameters.productName" field="variantProduct.productName"/>
-                <now-timestamp field="variantProduct.lastModifiedDate"/>
-                <set from-field="userLogin.userLoginId" field="variantProduct.lastModifiedByUserLogin"/>
-                <store-value value-field="variantProduct"/>
-            </iterate>
-        </if-compare>
-    </simple-method>
-
-    <simple-method method-name="duplicateProduct" short-description="Duplicate a Product">
-        <set value="duplicateProduct" field="callingMethodName"/>
-        <set value="CREATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <set value="DELETE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <entity-one entity-name="Product" value-field="dummyProduct">
-            <field-map field-name="productId" from-field="parameters.productId"/>
-        </entity-one>
-        <if-not-empty field="dummyProduct">
-            <add-error ><fail-property resource="CommonErrorUiLabels" property="CommonErrorDuplicateKey" /></add-error>
-        </if-not-empty>
-        <check-errors/>
-
-        <!-- look up the old product and clone it -->
-        <entity-one entity-name="Product" value-field="oldProduct" auto-field-map="false">
-            <field-map field-name="productId" from-field="parameters.oldProductId"/>
-        </entity-one>
-        <clone-value value-field="oldProduct" new-value-field="newProduct"/>
-
-        <!-- set the productId, and write it to the datasource -->
-        <set from-field="parameters.productId" field="newProduct.productId"/>
-
-        <!-- if requested, set the new internalName field -->
-        <if-not-empty field="parameters.newInternalName">
-            <set from-field="parameters.newInternalName" field="newProduct.internalName"/>
-        </if-not-empty>
-
-        <!-- if requested, set the new productName field -->
-        <if-not-empty field="parameters.newProductName">
-            <set from-field="parameters.newProductName" field="newProduct.productName"/>
-        </if-not-empty>
-
-        <!-- if requested, set the new description field -->
-        <if-not-empty field="parameters.newDescription">
-            <set from-field="parameters.newDescription" field="newProduct.description"/>
-        </if-not-empty>
-
-        <!-- if requested, set the new longDescription field -->
-        <if-not-empty field="parameters.newLongDescription">
-            <set from-field="parameters.newLongDescription" field="newProduct.longDescription"/>
-        </if-not-empty>
-
-        <create-value value-field="newProduct"/>
-
-        <!-- set up entity filter -->
-        <set field="productFindContext.productId" from-field="parameters.oldProductId"/>
-        <set field="reverseProductFindContext.productIdTo" from-field="parameters.oldProductId"/>
-
-        <!-- if requested, duplicate related data as well -->
-        <if-not-empty field="parameters.duplicatePrices">
-            <find-by-and entity-name="ProductPrice" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateIDs">
-            <find-by-and entity-name="GoodIdentification" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateContent">
-            <find-by-and entity-name="ProductContent" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateCategoryMembers">
-            <find-by-and entity-name="ProductCategoryMember" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <!-- Clone Content -->
-                <sequenced-id sequence-name="Content" field="newContentId"/>
-                <entity-one entity-name="Content" value-field="oldContent" use-cache="false">
-                    <field-map field-name="contentId" value="${newTempValue.contentId}"/>
-                </entity-one>
-                <if-not-empty field="oldContent">
-                    <clone-value value-field="oldContent" new-value-field="clonedContent"/>
-                    <set from-field="newContentId" field="clonedContent.contentId"/>
-                    <set from-field="newContentId" field="newTempValue.contentId"/>
-                    <!-- Clone DataResource -->
-                    <entity-one entity-name="DataResource" value-field="oldDataResource" use-cache="false">
-                        <field-map field-name="dataResourceId" value="${clonedContent.dataResourceId}"/>
-                    </entity-one>
-                    <if-not-empty field="oldDataResource">
-                        <sequenced-id sequence-name="DataResource" field="newDataResourceId"/>
-                        <clone-value new-value-field="clonedDataresource" value-field="oldDataResource"/>
-                        <set from-field="newDataResourceId" field="clonedDataresource.dataResourceId"/>
-                        <!-- set new data resource id in cloned content -->
-                        <set from-field="newDataResourceId" field="clonedContent.dataResourceId"/>
-                        <create-value value-field="clonedDataresource"/>
-                        <!-- Clone Electronic Text if exists -->
-                        <get-related-one value-field="oldDataResource" relation-name="ElectronicText" to-value-field="oldElectronicText"/>
-                        <if-not-empty field="oldElectronicText">
-                            <clone-value value-field="oldElectronicText" new-value-field="clonedElectronicText"/>
-                            <set from-field="newDataResourceId" field="clonedElectronicText.dataResourceId"/>
-                            <create-value value-field="clonedElectronicText"/>
-                        </if-not-empty>
-                    </if-not-empty>
-                    <create-value value-field="clonedContent"/>
-                </if-not-empty>
-                <!-- End Clone Contet -->
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateAssocs">
-            <find-by-and entity-name="ProductAssoc" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-
-            <!-- small difference here, also do the reverse assocs... -->
-            <entity-and entity-name="ProductAssoc" list="foundValues">
-                <field-map field-name="productIdTo" from-field="parameters.oldProductId"/>
-            </entity-and>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productIdTo"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateAttributes">
-            <find-by-and entity-name="ProductAttribute" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateFeatureAppls">
-            <find-by-and entity-name="ProductFeatureAppl" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-        <if-not-empty field="parameters.duplicateInventoryItems">
-            <find-by-and entity-name="InventoryItem" map="productFindContext" list="foundValues"/>
-            <iterate list="foundValues" entry="foundValue">
-                <!--
-                    NOTE: new inventory items should always be created calling the
-                          createInventoryItem service because in this way we are sure
-                          that all the relevant fields are filled with default values.
-                          However, the code here should work fine because all the values
-                          for the new inventory item are inerited from the existing item.
-                    TODO: is this code correct? What is the meaning of duplicating inventory items?
-                          What about the InventoryItemDetail entries?
-                -->
-                <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                <set from-field="parameters.productId" field="newTempValue.productId"/>
-                <!-- this one is slightly different because it needs a new sequenced inventoryItemId -->
-                <sequenced-id sequence-name="InventoryItem" field="newTempValue.inventoryItemId"/>
-                <create-value value-field="newTempValue"/>
-            </iterate>
-        </if-not-empty>
-
-        <!-- if requested, remove related data as well -->
-        <if-not-empty field="parameters.removePrices">
-            <remove-by-and entity-name="ProductPrice" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeIDs">
-            <remove-by-and entity-name="GoodIdentification" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeContent">
-            <remove-by-and entity-name="ProductContent" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeCategoryMembers">
-            <remove-by-and entity-name="ProductCategoryMember" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeAssocs">
-            <remove-by-and entity-name="ProductAssoc" map="productFindContext"/>
-            <!-- small difference here, also do the reverse assocs... -->
-            <remove-by-and entity-name="ProductAssoc" map="reverseProductFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeAttributes">
-            <remove-by-and entity-name="ProductAttribute" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeFeatureAppls">
-            <remove-by-and entity-name="ProductFeatureAppl" map="productFindContext"/>
-        </if-not-empty>
-        <if-not-empty field="parameters.removeInventoryItems">
-            <remove-by-and entity-name="InventoryItem" map="productFindContext"/>
-        </if-not-empty>
-    </simple-method>
-
-    <!-- Product Keyword Services -->
-    <simple-method method-name="forceIndexProductKeywords" short-description="induce all the keywords of a product">
-        <entity-one entity-name="Product" value-field="product"/>
-        <call-class-method class-name="org.apache.ofbiz.product.product.KeywordIndex" method-name="forceIndexKeywords">
-            <field field="product" type="org.apache.ofbiz.entity.GenericValue"/>
-        </call-class-method>
-    </simple-method>
-    <simple-method method-name="deleteProductKeywords" short-description="delete all the keywords of a product">
-        <entity-one entity-name="Product" value-field="product"/>
-        <remove-related value-field="product" relation-name="ProductKeyword"/>
-    </simple-method>
-
-    <simple-method method-name="indexProductKeywords" short-description="Index the Keywords for a Product" login-required="false">
-        <!-- this service is meant to be called from an entity ECA for entities that include a productId -->
-        <!-- if it is the Product entity itself triggering this action, then a [productInstance] parameter
-            will be passed and we can save a few cycles looking that up -->
-        <set from-field="parameters.productInstance" field="productInstance"/>
-        <if-empty field="productInstance">
-            <set from-field="parameters.productId" field="findProductMap.productId"/>
-            <find-by-primary-key entity-name="Product" map="findProductMap" value-field="productInstance"/>
-        </if-empty>
-
-        <!-- induce keywords if autoCreateKeywords is emtpy or Y-->
-        <if>
-            <condition>
-                <or>
-                    <if-empty field="productInstance.autoCreateKeywords"/>
-                    <if-compare field="productInstance.autoCreateKeywords" operator="equals" value="Y"/>
-                </or>
-            </condition>
-            <then>
-                <call-class-method class-name="org.apache.ofbiz.product.product.KeywordIndex" method-name="indexKeywords">
-                    <field field="productInstance" type="org.apache.ofbiz.entity.GenericValue"/>
-                </call-class-method>
-            </then>
-        </if>
-    </simple-method>
-
-    <simple-method method-name="discontinueProductSales" short-description="Discontinue Product Sales" login-required="false">
-        <!-- set sales discontinuation date to now -->
-        <now-timestamp field="nowTimestamp"/>
-        <entity-one entity-name="Product" value-field="product"/>
-        <set from-field="nowTimestamp" field="product.salesDiscontinuationDate"/>
-        <store-value value-field="product"/>
-        <!-- expire product from all categories -->
-        <get-related value-field="product" relation-name="ProductCategoryMember" list="productCategoryMembers"/>
-        <iterate list="productCategoryMembers" entry="productCategoryMember">
-            <if-empty field="productCategoryMember.thruDate">
-                <set from-field="nowTimestamp" field="productCategoryMember.thruDate"/>
-                <store-value value-field="productCategoryMember"/>
-            </if-empty>
-        </iterate>
-        <!-- expire product from all associations going to it -->
-        <get-related value-field="product" relation-name="AssocProductAssoc" list="assocProductAssocs"/>
-        <iterate list="assocProductAssocs" entry="assocProductAssoc">
-            <if-empty field="assocProductAssoc.thruDate">
-                <set from-field="nowTimestamp" field="assocProductAssoc.thruDate"/>
-                <store-value value-field="assocProductAssoc"/>
-            </if-empty>
-        </iterate>
-    </simple-method>
-
-    <simple-method method-name="countProductView" short-description="Count Product View" login-required="false">
-        <if-empty field="parameters.weight">
-            <calculate field="parameters.weight" type="Long"><number value="1"/></calculate>
-        </if-empty>
-        <entity-one entity-name="ProductCalculatedInfo" value-field="productCalculatedInfo"/>
-        <if-empty field="productCalculatedInfo">
-            <!-- go ahead and create it -->
-            <make-value entity-name="ProductCalculatedInfo" value-field="productCalculatedInfo"/>
-            <set from-field="parameters.productId" field="productCalculatedInfo.productId"/>
-            <set from-field="parameters.weight" field="productCalculatedInfo.totalTimesViewed"/>
-            <create-value value-field="productCalculatedInfo"/>
-        <else>
-            <calculate field="productCalculatedInfo.totalTimesViewed" type="Long">
-                <calcop operator="add" field="productCalculatedInfo.totalTimesViewed">
-                    <calcop operator="get" field="parameters.weight"></calcop>
-                </calcop>
-            </calculate>
-            <store-value value-field="productCalculatedInfo"/>
-        </else>
-        </if-empty>
-
-        <!-- do the same for the virtual product... -->
-        <entity-one entity-name="Product" value-field="product" use-cache="true"/>
-        <call-class-method class-name="org.apache.ofbiz.product.product.ProductWorker" method-name="getVariantVirtualId" ret-field="virtualProductId">
-            <field field="product" type="GenericValue"/>
-        </call-class-method>
-        <if-not-empty field="virtualProductId">
-            <set from-field="virtualProductId" field="callSubMap.productId"/>
-            <set from-field="parameters.weight" field="callSubMap.weight"/>
-            <call-service service-name="countProductView" in-map-name="callSubMap"></call-service>
-        </if-not-empty>
-    </simple-method>
-    
-    <simple-method method-name="createProductReview" short-description="Create a ProductReview" login-required="false">
-        <make-value entity-name="ProductReview" value-field="newEntity"/>
-        <set-nonpk-fields map="parameters" value-field="newEntity"/>
-        <set from-field="userLogin.userLoginId" field="newEntity.userLoginId"/>
-        <set value="PRR_PENDING" field="newEntity.statusId"/>
-
-        <!-- code to check for auto-approved reviews (store setting) -->
-        <entity-one entity-name="ProductStore" value-field="productStore"/>
-
-        <if-not-empty field="productStore">
-            <if-compare field="productStore.autoApproveReviews" operator="equals" value="Y">
-                <set value="PRR_APPROVED" field="newEntity.statusId"/>
-            </if-compare>
-        </if-not-empty>
-
-        <!-- auto approve the review if it is just a rating and has no review text -->
-        <if-empty field="parameters.productReview">
-            <set value="PRR_APPROVED" field="newEntity.statusId"/>
-        </if-empty>
-
-        <!-- create the new ProductReview -->
-        <sequenced-id sequence-name="ProductReview" field="newEntity.productReviewId"/>
-        <field-to-result field="newEntity.productReviewId" result-name="productReviewId"/>
-
-        <if-empty field="newEntity.postedDateTime">
-            <now-timestamp field="newEntity.postedDateTime"/>
-        </if-empty>
-
-        <create-value value-field="newEntity"/>
-
-        <set from-field="newEntity.productId" field="productId"/>
-        <property-to-field resource="ProductUiLabels" property="ProductCreateProductReviewSuccess" field="successMessage"/>
-        <call-simple-method method-name="updateProductWithReviewRatingAvg"/>
-    </simple-method>
-    <simple-method method-name="updateProductReview" short-description="Update ProductReview">
-        <set value="updateProductReview" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <make-value entity-name="ProductReview" value-field="lookupPKMap"/>
-        <set-pk-fields map="parameters" value-field="lookupPKMap"/>
-        <find-by-primary-key map="lookupPKMap" value-field="lookedUpValue"/>
-        <set-nonpk-fields map="parameters" value-field="lookedUpValue"/>
-        <store-value value-field="lookedUpValue"/>
-
-        <set from-field="lookedUpValue.productId" field="productId"/>
-        <call-simple-method method-name="updateProductWithReviewRatingAvg"/>
-    </simple-method>
-    <simple-method method-name="updateProductWithReviewRatingAvg" short-description="Update Product with new Review Rating Avg" login-required="false">
-        <!-- this method is meant to be called in-line and depends in a productId parameter -->
-        <call-class-method class-name="org.apache.ofbiz.product.product.ProductWorker" method-name="getAverageProductRating" ret-field="averageCustomerRating">
-            <field field="delegator" type="org.apache.ofbiz.entity.Delegator"/>
-            <field field="productId" type="java.lang.String"/>
-        </call-class-method>
-        <log level="info" message="Got new average customer rating ${averageCustomerRating}"/>
-        <if-compare field="averageCustomerRating" operator="equals" value="0" type="BigDecimal">
-            <return/>
-        </if-compare>
-        <!-- update the review average on the ProductCalculatedInfo entity -->
-        <entity-one entity-name="ProductCalculatedInfo" value-field="productCalculatedInfo"/>
-        <if-empty field="productCalculatedInfo">
-            <!-- go ahead and create it -->
-            <make-value entity-name="ProductCalculatedInfo" value-field="productCalculatedInfo"/>
-            <set from-field="productId" field="productCalculatedInfo.productId"/>
-            <set from-field="averageCustomerRating" field="productCalculatedInfo.averageCustomerRating"/>
-            <create-value value-field="productCalculatedInfo"/>
-        <else>
-            <set from-field="averageCustomerRating" field="productCalculatedInfo.averageCustomerRating"/>
-            <store-value value-field="productCalculatedInfo"/>
-        </else>
-        </if-empty>
-    </simple-method>
-    <simple-method method-name="copyToProductVariants" short-description="Updates the Product's Variants">
-        <set value="copyToProductVariants" field="callingMethodName"/>
-        <set value="CREATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <set value="DELETE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <set from-field="parameters.virtualProductId" field="productFindContext.productId"/>
-        <find-by-primary-key entity-name="Product" map="productFindContext" value-field="oldProduct"/>
-
-        <set from-field="parameters.virtualProductId" field="variantsFindContext.productId"/>
-        <set value="PRODUCT_VARIANT" field="variantsFindContext.productAssocTypeId"/>
-        <find-by-and entity-name="ProductAssoc" map="variantsFindContext" list="variants"/>
-        <filter-list-by-date list="variants"/>
-        <iterate list="variants" entry="newProduct">
-            <set from-field="newProduct.productIdTo" field="productVariantContext.productId"/>
-            <!-- if requested, duplicate related data -->
-            <if-not-empty field="parameters.duplicatePrices">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductPrice" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductPrice" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateIDs">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="GoodIdentification" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="GoodIdentification" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateContent">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductContent" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductContent" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateCategoryMembers">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductCategoryMember" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductCategoryMember" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateAttributes">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductAttribute" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductAttribute" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateFacilities">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductFacility" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductFacility" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-            <if-not-empty field="parameters.duplicateLocations">
-                <if-not-empty field="parameters.removeBefore">
-                    <find-by-and entity-name="ProductFacilityLocation" map="productVariantContext" list="foundVariantValues"/>
-                    <iterate list="foundVariantValues" entry="foundVariantValue">
-                        <remove-value value-field="foundVariantValue"/>
-                    </iterate>
-                </if-not-empty>
-                <find-by-and entity-name="ProductFacilityLocation" map="productFindContext" list="foundValues"/>
-                <iterate list="foundValues" entry="foundValue">
-                    <clone-value value-field="foundValue" new-value-field="newTempValue"/>
-                    <set from-field="newProduct.productIdTo" field="newTempValue.productId"/>
-                    <create-value value-field="newTempValue"/>
-                </iterate>
-            </if-not-empty>
-        </iterate>
-    </simple-method>
-
-    <!-- a method to centralize product security code, meant to be called in-line with
-        call-simple-method, and the checkAction and callingMethodName attributes should be in the method context -->
-    <simple-method method-name="checkProductRelatedPermission" short-description="Check Product Related Permission">
-        <if-empty field="callingMethodName">
-            <property-to-field resource="CommonUiLabels" property="CommonPermissionThisOperation" field="callingMethodName"/>
-        </if-empty>
-        <if-empty field="checkAction">
-            <set value="UPDATE" field="checkAction"/>
-        </if-empty>
-
-        <!-- find all role-categories that this product is a member of -->
-        <if>
-            <condition>
-                <not><if-has-permission permission="CATALOG" action="_${checkAction}"/></not>
-            </condition>
-            <then>
-                <set from-field="parameters.productId" field="lookupRoleCategoriesMap.productId"/>
-                <set from-field="userLogin.partyId" field="lookupRoleCategoriesMap.partyId"/>
-                <set value="LTD_ADMIN" field="lookupRoleCategoriesMap.roleTypeId"/>
-                <find-by-and entity-name="ProductCategoryMemberAndRole" map="lookupRoleCategoriesMap" list="roleCategories"/>
-                <filter-list-by-date list="roleCategories"/>
-                <filter-list-by-date list="roleCategories" from-field-name="roleFromDate" thru-field-name="roleThruDate"/>
-            </then>
-        </if>
-        <if>
-            <condition>
-                <not>
-                    <or>
-                        <if-has-permission permission="CATALOG" action="_${checkAction}"/>
-                        <and>
-                            <if-has-permission permission="CATALOG_ROLE" action="_${checkAction}"/>
-                            <not><if-empty field="roleCategories"/></not>
-                        </and>
-                        <and>
-                            <not><if-empty field="alternatePermissionRoot"/></not>
-                            <if-has-permission permission="${alternatePermissionRoot}" action="_${checkAction}"/>
-                        </and>
-                    </or>
-                </not>
-            </condition>
-            <then>
-                <set field="checkActionLabel" value="${groovy: 'ProductCatalog' + checkAction.charAt(0) + checkAction.substring(1).toLowerCase() + 'PermissionError'}"/>
-                <set field="resourceDescription" from-field="callingMethodName"/>
-                <add-error>
-                    <fail-property resource="ProductUiLabels" property="${checkActionLabel}"/>
-                </add-error>
-            </then>
-        </if>
-    </simple-method>
-    <simple-method method-name="productGenericPermission" short-description="Main permission logic">
-        <set field="mainAction" from-field="parameters.mainAction"/>
-        <if-empty field="mainAction">
-            <add-error>
-                <fail-property resource="ProductUiLabels" property="ProductMissingMainActionInPermissionService"/>
-            </add-error>
-            <check-errors/>
-        </if-empty>
-
-        <set field="callingMethodName" from-field="parameters.resourceDescription"/>
-        <set field="checkAction" from-field="parameters.mainAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-
-        <if-empty field="error_list">
-            <set field="hasPermission" type="Boolean" value="true"/>
-            <field-to-result field="hasPermission"/>
-
-            <else>
-                <property-to-field resource="ProductUiLabels" property="ProductPermissionError" field="failMessage"/>
-                <set field="hasPermission" type="Boolean" value="false"/>
-                <field-to-result field="hasPermission"/>
-                <field-to-result field="failMessage"/>
-            </else>
-        </if-empty>
-    </simple-method>
-    <simple-method method-name="productPriceGenericPermission" short-description="product price permission logic">
-        <set field="mainAction" from-field="parameters.mainAction"/>
-        <if-empty field="mainAction">
-            <add-error>
-                <fail-property resource="ProductUiLabels" property="ProductMissingMainActionInPermissionService"/>
-            </add-error>
-            <check-errors/>
-        </if-empty>
-        <check-permission permission="CATALOG_PRICE_MAINT">
-            <fail-property resource="ProductUiLabels" property="ProductPriceMaintPermissionError"/>
-        </check-permission>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <if-empty field="error_list">
-            <set field="hasPermission" type="Boolean" value="true"/>
-            <field-to-result field="hasPermission"/>
-            <else>
-                <property-to-field resource="ProductUiLabels" property="ProductPermissionError" field="failMessage"/>
-                <set field="hasPermission" type="Boolean" value="false"/>
-                <field-to-result field="hasPermission"/>
-                <field-to-result field="failMessage"/>
-            </else>
-        </if-empty>
-    </simple-method>
-
-    <!-- ================================================================ -->
-    <!-- ProductRole Services -->
-    <!-- ================================================================ -->
-
-    <simple-method method-name="addPartyToProduct" short-description="Add Party to Product">
-        <set value="addPartyToProduct" field="callingMethodName"/>
-        <set value="CREATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <make-value entity-name="ProductRole" value-field="newEntity"/>
-        <set-pk-fields map="parameters" value-field="newEntity"/>
-        <set-nonpk-fields map="parameters" value-field="newEntity"/>
-
-        <if-empty field="newEntity.fromDate">
-            <now-timestamp field="newEntity.fromDate"/>
-        </if-empty>
-
-        <create-value value-field="newEntity"/>
-    </simple-method>
-    <simple-method method-name="updatePartyToProduct" short-description="Update Party to Product">
-        <set value="updatePartyToProduct" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <make-value entity-name="ProductRole" value-field="lookupPKMap"/>
-        <set-pk-fields map="parameters" value-field="lookupPKMap"/>
-        <find-by-primary-key entity-name="ProductRole" map="lookupPKMap" value-field="lookedUpValue"/>
-        <set-nonpk-fields map="parameters" value-field="lookedUpValue"/>
-        <store-value value-field="lookedUpValue"/>
-    </simple-method>
-    <simple-method method-name="removePartyFromProduct" short-description="Remove Party From Product">
-        <set value="removePartyFromProduct" field="callingMethodName"/>
-        <set value="DELETE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <make-value entity-name="ProductRole" value-field="lookupPKMap"/>
-        <set-pk-fields map="parameters" value-field="lookupPKMap"/>
-        <find-by-primary-key entity-name="ProductRole" map="lookupPKMap" value-field="lookedUpValue"/>
-        <remove-value value-field="lookedUpValue"/>
-    </simple-method>
-
-    <!-- ProductCategoryGlAccount methods -->
-    <simple-method method-name="createProductCategoryGlAccount" short-description="Create a ProductCategoryGlAccount">
-        <set value="createProductCategoryGlAccount" field="callingMethodName"/>
-        <set value="CREATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <make-value entity-name="ProductCategoryGlAccount" value-field="newEntity"/>
-        <set-nonpk-fields map="parameters" value-field="newEntity"/>
-        <set-pk-fields map="parameters" value-field="newEntity"/>
-        <create-value value-field="newEntity"/>
-    </simple-method>
-
-    <simple-method method-name="updateProductCategoryGlAccount" short-description="Update a ProductCategoryGlAccount">
-        <set value="updateProductCategoryGlAccount" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <entity-one entity-name="ProductCategoryGlAccount" value-field="lookedUpValue"/>
-        <set-nonpk-fields map="parameters" value-field="lookedUpValue"/>
-        <store-value value-field="lookedUpValue"/>
-    </simple-method>
-
-    <simple-method method-name="deleteProductCategoryGlAccount" short-description="Delete a ProductCategoryGlAccount">
-        <set value="deleteProductCategoryGlAccount" field="callingMethodName"/>
-        <set value="DELETE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-
-        <entity-one entity-name="ProductCategoryGlAccount" value-field="lookedUpValue"/>
-        <remove-value value-field="lookedUpValue"/>
-    </simple-method>
-
-    <!-- Product GroupOrder Services -->
-    <simple-method method-name="createProductGroupOrder" short-description="Create ProductGroupOrder">
-        <make-value entity-name="ProductGroupOrder" value-field="newEntity"/>
-        <make-next-seq-id value-field="newEntity" seq-field-name="groupOrderId"/>
-        <field-to-result field="newEntity.groupOrderId" result-name="groupOrderId"/>
-        <set-nonpk-fields map="parameters" value-field="newEntity"/>
-        <create-value value-field="newEntity"/>
-    </simple-method>
-
-    <simple-method method-name="updateProductGroupOrder" short-description="Update ProductGroupOrder">
-        <entity-one entity-name="ProductGroupOrder" value-field="productGroupOrder"/>
-        <set-nonpk-fields map="parameters" value-field="productGroupOrder"/>
-        <store-value value-field="productGroupOrder"/>
-        
-        <if-compare field="productGroupOrder.statusId" operator="equals" value="GO_CREATED">
-            <entity-one entity-name="JobSandbox" value-field="jobSandbox">
-                <field-map field-name="jobId" from-field="productGroupOrder.jobId"/>
-            </entity-one>
-            <if-not-empty field="jobSandbox">
-                <set field="jobSandbox.runTime" from-field="parameters.thruDate"/>
-                <store-value value-field="jobSandbox"/>
-            </if-not-empty>
-        </if-compare>
-    </simple-method>
-
-    <simple-method method-name="deleteProductGroupOrder" short-description="Delete ProductGroupOrder">
-        <entity-and entity-name="OrderItemGroupOrder" list="orderItemGroupOrders">
-            <field-map field-name="groupOrderId" from-field="parameters.groupOrderId"/>
-        </entity-and>
-        <iterate list="orderItemGroupOrders" entry="orderItemGroupOrder">
-            <remove-value value-field="orderItemGroupOrder"/>
-        </iterate>
-        
-        <entity-one entity-name="ProductGroupOrder" value-field="productGroupOrder"/>
-        <remove-value value-field="productGroupOrder"/>
-        
-        <entity-one entity-name="JobSandbox" value-field="jobSandbox">
-            <field-map field-name="jobId" from-field="productGroupOrder.jobId"/>
-        </entity-one>
-        <remove-value value-field="jobSandbox"/>
-        
-        <entity-and entity-name="JobSandbox" list="jobSandboxList">
-            <field-map field-name="runtimeDataId" from-field="jobSandbox.runtimeDataId"/>
-        </entity-and>
-        <iterate list="jobSandboxList" entry="jobSandboxRelatedRuntimeData">
-            <remove-value value-field="jobSandboxRelatedRuntimeData"/>
-        </iterate>
-        
-        <entity-one entity-name="RuntimeData" value-field="runtimeData">
-            <field-map field-name="runtimeDataId" from-field="jobSandbox.runtimeDataId"/>
-        </entity-one>
-        <remove-value value-field="runtimeData"/>
-    </simple-method>
-
-    <simple-method method-name="createJobForProductGroupOrder" short-description="Create ProductGroupOrder">
-        <entity-one entity-name="ProductGroupOrder" value-field="productGroupOrder"/>
-        <if-empty field="productGroupOrder.jobId">
-            <!-- Create RuntimeData For ProductGroupOrder -->
-            <set field="runtimeDataMap.groupOrderId" from-field="parameters.groupOrderId"/>
-            <call-class-method class-name="org.apache.ofbiz.entity.serialize.XmlSerializer" method-name="serialize"  ret-field="runtimeInfo">
-                <field field="runtimeDataMap" type="Object"/>
-            </call-class-method>
-            <make-value entity-name="RuntimeData" value-field="runtimeData"/>
-            <sequenced-id sequence-name="RuntimeData" field="runtimeData.runtimeDataId"/>
-            <set field="runtimeDataId" from-field="runtimeData.runtimeDataId"/>
-            <set field="runtimeData.runtimeInfo" from-field="runtimeInfo"/>
-            <create-value value-field="runtimeData"/>
-
-             <!-- Create Job For ProductGroupOrder -->
-             <!-- FIXME: Jobs should not be manually created -->
-            <make-value entity-name="JobSandbox" value-field="jobSandbox"/>
-            <sequenced-id sequence-name="JobSandbox" field="jobSandbox.jobId"/>
-            <set field="jobId" from-field="jobSandbox.jobId"/>
-            <set field="jobSandbox.jobName" value="Check ProductGroupOrder Expired"/>
-            <set field="jobSandbox.runTime" from-field="parameters.thruDate"/>
-            <set field="jobSandbox.poolId" value="pool"/>
-            <set field="jobSandbox.statusId" value="SERVICE_PENDING"/>
-            <set field="jobSandbox.serviceName" value="checkProductGroupOrderExpired"/>
-            <set field="jobSandbox.runAsUser" value="system"/>
-            <set field="jobSandbox.runtimeDataId" from-field="runtimeDataId"/>
-            <set field="jobSandbox.maxRecurrenceCount" value="1" type="Long"/>
-            <set field="jobSandbox.priority" value="50" type="Long"/>
-            <create-value value-field="jobSandbox"/>
-
-            <set field="productGroupOrder.jobId" from-field="jobId"/>
-            <store-value value-field="productGroupOrder"/>
-        </if-empty>
-    </simple-method>
-
-    <simple-method method-name="checkOrderItemForProductGroupOrder" short-description="Check OrderItem For ProductGroupOrder">
-        <entity-and entity-name="OrderItem" list="orderItems">
-            <field-map field-name="orderId" from-field="parameters.orderId"/>
-        </entity-and>
-        <iterate list="orderItems" entry="orderItem">
-            <set field="productId" from-field="orderItem.productId"/>
-            <entity-one entity-name="Product" value-field="product">
-                <field-map field-name="productId" from-field="orderItem.productId"/>
-            </entity-one>
-            <if-compare field="product.isVariant" operator="equals" value="Y">
-                <entity-and entity-name="ProductAssoc" list="variantProductAssocs" filter-by-date="true">
-                    <field-map field-name="productIdTo" from-field="orderItem.productId"/>
-                    <field-map field-name="productAssocTypeId" value="PRODUCT_VARIANT"/>
-                </entity-and>
-                <first-from-list list="variantProductAssocs" entry="variantProductAssoc"/>
-                <set field="productId" from-field="variantProductAssoc.productId"/>
-            </if-compare>
-
-            <entity-and entity-name="ProductGroupOrder" list="productGroupOrders" filter-by-date="true">
-                <field-map field-name="productId" from-field="productId"/>
-            </entity-and>
-            <if-not-empty field="productGroupOrders">
-                <first-from-list list="productGroupOrders" entry="productGroupOrder"/>
-                <calculate field="productGroupOrder.soldOrderQty">
-                    <calcop operator="add" field="productGroupOrder.soldOrderQty">
-                        <calcop operator="get" field="orderItem.quantity"/>
-                    </calcop>
-                </calculate>
-                <store-value value-field="productGroupOrder"/>
-                
-                <set field="createOrderItemGroupOrderMap.orderId" from-field="orderItem.orderId"/>
-                <set field="createOrderItemGroupOrderMap.orderItemSeqId" from-field="orderItem.orderItemSeqId"/>
-                <set field="createOrderItemGroupOrderMap.groupOrderId" from-field="productGroupOrder.groupOrderId"/>
-                <call-service service-name="createOrderItemGroupOrder" in-map-name="createOrderItemGroupOrderMap"/>
-            </if-not-empty>
-        </iterate>
-    </simple-method>
-    
-    <simple-method method-name="cancleOrderItemGroupOrder" short-description="Cancle OrderItemGroupOrder">
-        <if-not-empty field="parameters.orderItemSeqId">
-            <entity-and entity-name="OrderItem" list="orderItems">
-                <field-map field-name="orderId" from-field="parameters.orderId"/>
-                <field-map field-name="orderItemSeqId" from-field="parameters.orderItemSeqId" />
-            </entity-and>
-        <else>
-            <entity-and entity-name="OrderItem" list="orderItems">
-                <field-map field-name="orderId" from-field="parameters.orderId"/>
-            </entity-and>
-        </else>
-        </if-not-empty>
-        <iterate list="orderItems" entry="orderItem">
-            <entity-and entity-name="OrderItemGroupOrder" list="orderItemGroupOrders">
-                <field-map field-name="orderId" from-field="orderItem.orderId"/>
-                <field-map field-name="orderItemSeqId" from-field="orderItem.orderItemSeqId"/>
-            </entity-and>
-            <if-not-empty field="orderItemGroupOrders">
-                <first-from-list list="orderItemGroupOrders" entry="orderItemGroupOrder"/>
-                <entity-one entity-name="ProductGroupOrder" value-field="productGroupOrder">
-                    <field-map field-name="groupOrderId" from-field="orderItemGroupOrder.groupOrderId"/>
-                </entity-one>
-                <if-not-empty field="productGroupOrder">
-                    <if-compare field="productGroupOrder.statusId" operator="equals" value="GO_CREATED">
-                        <if-compare field="orderItem.statusId" operator="equals" value="ITEM_CANCELLED">
-                            <if-not-empty field="orderItem.cancelQuantity">
-                                <set field="cancelQuantity" from-field="orderItem.cancelQuantity"/>
-                            <else>
-                                <set field="cancelQuantity" from-field="orderItem.quantity"/>
-                            </else>
-                            </if-not-empty>
-                            <calculate field="productGroupOrder.soldOrderQty">
-                                <calcop operator="subtract" field="productGroupOrder.soldOrderQty">
-                                    <calcop operator="get" field="cancelQuantity"/>
-                                </calcop>
-                            </calculate>
-                        </if-compare>
-                        <store-value value-field="productGroupOrder"/>
-                        <remove-value value-field="orderItemGroupOrder"/>
-                    </if-compare>
-                </if-not-empty>
-            </if-not-empty>
-        </iterate>
-    </simple-method>
-    
-    <simple-method method-name="checkProductGroupOrderExpired" short-description="Check ProductGroupOrder Expired">
-        <entity-one entity-name="ProductGroupOrder" value-field="productGroupOrder"/>
-        <if-not-empty field="productGroupOrder">
-            <if-compare field="productGroupOrder.soldOrderQty" operator="greater-equals" value="${productGroupOrder.reqOrderQty}">
-                <set field="newItemStatusId" value="ITEM_APPROVED"/>
-                <set field="groupOrderStatusId" value="GO_SUCCESS"/>
-            <else>
-                <set field="newItemStatusId" value="ITEM_CANCELLED"/>
-                <set field="groupOrderStatusId" value="GO_CANCELLED"/>
-            </else>
-            </if-compare>
-            
-            <set field="updateProductGroupOrderMap.groupOrderId" from-field="productGroupOrder.groupOrderId"/>
-            <set field="updateProductGroupOrderMap.statusId" from-field="groupOrderStatusId"/>
-            <call-service service-name="updateProductGroupOrder" in-map-name="updateProductGroupOrderMap"/>
-            
-            <entity-and entity-name="OrderItemGroupOrder" list="orderItemGroupOrders">
-                <field-map field-name="groupOrderId" from-field="productGroupOrder.groupOrderId"/>
-            </entity-and>
-            <iterate list="orderItemGroupOrders" entry="orderItemGroupOrder">
-                <set field="changeOrderItemStatusMap.orderId" from-field="orderItemGroupOrder.orderId"/>
-                <set field="changeOrderItemStatusMap.orderItemSeqId" from-field="orderItemGroupOrder.orderItemSeqId"/>
-                <set field="changeOrderItemStatusMap.statusId" from-field="newItemStatusId"/>
-                <call-service service-name="changeOrderItemStatus" in-map-name="changeOrderItemStatusMap"/>
-            </iterate>
-        </if-not-empty>
-    </simple-method>
-    
-    <simple-method method-name="setProductReviewStatus" short-description="change the product review Status">
-        <set value="setProductReviewStatus" field="callingMethodName"/>
-        <set value="UPDATE" field="checkAction"/>
-        <call-simple-method method-name="checkProductRelatedPermission"/>
-        <check-errors/>
-        
-        <entity-one entity-name="ProductReview" value-field="productReview"/>
-        <if-not-empty field="productReview">
-            <if-compare-field field="productReview.statusId" to-field="parameters.statusId" operator="not-equals">
-                <entity-one entity-name="StatusValidChange" value-field="statusChange">
-                    <field-map field-name="statusId" from-field="productReview.statusId"/>
-                    <field-map field-name="statusIdTo" from-field="parameters.statusId"/>
-                </entity-one>
-                <if-empty field="statusChange">
-                    <set field="msg" value="Status is not a valid change: from ${productReview.statusId} to ${parameters.statusId}"/>
-                    <log level="error" message="${msg}"/>
-                    <add-error>
-                        <fail-property resource="ProductErrorUiLabels" property="ProductReviewErrorCouldNotChangeOrderStatusFromTo"/>
-                    </add-error>
-                </if-empty>
-            </if-compare-field>
-        </if-not-empty>
-        <check-errors/>
-        
-        <set field="productReview.statusId" from-field="parameters.statusId"/>
-        <store-value value-field="productReview"/>
-        <field-to-result field="productReview.productReviewId" result-name="productReviewId"/>
-    </simple-method>
-</simple-methods>
diff --git a/applications/product/servicedef/services.xml b/applications/product/servicedef/services.xml
index 4e821b4..b44cba7 100644
--- a/applications/product/servicedef/services.xml
+++ b/applications/product/servicedef/services.xml
@@ -37,22 +37,22 @@ under the License.
         <override name="description" allow-html="safe"/>
         <override name="longDescription" allow-html="safe"/>
     </service>
-    <service name="createProduct" default-entity-name="Product" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="createProduct" auth="true">
+    <service name="createProduct" default-entity-name="Product" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="createProduct" auth="true">
         <description>Create a Product</description>
         <implements service="interfaceProduct"/>
         <auto-attributes include="pk" mode="INOUT" optional="true"/>
         <override name="productTypeId" optional="false"/>
         <override name="internalName" optional="false"/>
     </service>
-    <service name="updateProduct" default-entity-name="Product" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="updateProduct" auth="true">
+    <service name="updateProduct" default-entity-name="Product" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updateProduct" auth="true">
         <description>Update a Product</description>
         <implements service="interfaceProduct"/>
         <auto-attributes include="pk" mode="IN" optional="false"/>
     </service>
-    <service name="updateProductQuickAdminName" default-entity-name="Product" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="updateProductQuickAdminName" auth="true">
+    <service name="updateProductQuickAdminName" default-entity-name="Product" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updateProductQuickAdminName" auth="true">
         <description>Update a Product from Quick Admin</description>
         <implements service="interfaceProduct"/>
         <auto-attributes include="pk" mode="IN" optional="false"/>
@@ -63,8 +63,8 @@ under the License.
         <implements service="interfaceProduct"/>
         <auto-attributes include="pk" mode="IN" optional="false"/>
     </service>
-    <service name="duplicateProduct" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="duplicateProduct" auth="true">
+    <service name="duplicateProduct" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="duplicateProduct" auth="true">
         <description>Duplicate a Product using a new productId</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="oldProductId" type="String" mode="IN" optional="false"/>
@@ -89,8 +89,8 @@ under the License.
         <attribute name="removeFeatureAppls" type="String" mode="IN" optional="true"/>
         <attribute name="removeInventoryItems" type="String" mode="IN" optional="true"/>
     </service>
-    <service name="copyToProductVariants" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="copyToProductVariants" auth="true">
+    <service name="copyToProductVariants" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="copyToProductVariants" auth="true">
         <description>Copy Virtual Product's data to the Variant Products</description>
         <attribute name="virtualProductId" type="String" mode="IN" optional="false"/>
         <attribute name="removeBefore" type="String" mode="IN" optional="true"/>
@@ -151,40 +151,40 @@ under the License.
         <permission-service service-name="productGenericPermission" main-action="DELETE"/>
         <auto-attributes include="pk" mode="IN" optional="false"/>
     </service>
-    <service name="deleteProductKeywords" engine="simple"
-            location="component://product/minilang/product/product/ProductServices.xml" invoke="deleteProductKeywords" auth="true">
+    <service name="deleteProductKeywords" engine="groovy"
+            location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="deleteProductKeywords" auth="true">
         <description>Delete all the keywords of a product</description>
         <permission-service service-name="productGenericPermission" main-action="DELETE"/>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
     </service>
-    <service name="indexProductKeywords" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="indexProductKeywords" auth="false">
+    <service name="indexProductKeywords" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="indexProductKeywords" auth="false">
         <description>Index the Keywords for a Product</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="productInstance" type="org.apache.ofbiz.entity.GenericValue" mode="IN" optional="true"/>
     </service>
-    <service name="forceIndexProductKeywords" engine="simple"
-            location="component://product/minilang/product/product/ProductServices.xml" invoke="forceIndexProductKeywords" auth="true">
+    <service name="forceIndexProductKeywords" engine="groovy"
+            location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="forceIndexProductKeywords" auth="true">
         <description>Induce all the keywords of a product, ignoring the flag in the Product.autoCreateKeywords flag</description>
         <permission-service service-name="productGenericPermission" main-action="CREATE"/>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
     </service>
 
-    <service name="discontinueProductSales" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="discontinueProductSales" auth="false">
+    <service name="discontinueProductSales" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="discontinueProductSales" auth="false">
         <description>Discontinue Product Sales</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
     </service>
 
-    <service name="countProductView" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="countProductView" auth="false">
+    <service name="countProductView" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="countProductView" auth="false">
         <description>count Product View</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="weight" type="Long" mode="IN" optional="true"/>
     </service>
 
-    <service name="createProductReview" engine="simple"
-            location="component://product/minilang/product/product/ProductServices.xml" invoke="createProductReview" auth="true">
+    <service name="createProductReview" engine="groovy"
+            location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="createProductReview" auth="true">
         <description>Create a product review entity</description>
         <auto-attributes entity-name="ProductReview" mode="IN" include="nonpk" optional="true"/>
         <attribute name="productReviewId" type="String" mode="OUT" optional="false"/>
@@ -192,8 +192,8 @@ under the License.
         <override name="productId" optional="false"/>
         <override name="productRating" optional="false"/>
     </service>
-    <service name="updateProductReview" engine="simple" default-entity-name="ProductReview"
-            location="component://product/minilang/product/product/ProductServices.xml" invoke="updateProductReview" auth="true">
+    <service name="updateProductReview" engine="groovy" default-entity-name="ProductReview"
+            location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updateProductReview" auth="true">
         <description>Updates a product review record</description>
         <required-permissions join-type="OR">
             <check-permission permission="CATALOG_UPDATE"/>
@@ -202,8 +202,8 @@ under the License.
         <auto-attributes mode="IN" include="pk" optional="false"/>
         <auto-attributes mode="IN" include="nonpk" optional="true"/>
     </service>
-    <service name="setProductReviewStatus" engine="simple"
-            location="component://product/minilang/product/product/ProductServices.xml" invoke="setProductReviewStatus" auth="true">
+    <service name="setProductReviewStatus" engine="groovy"
+            location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="setProductReviewStatus" auth="true">
         <description>Updates a product review record</description>
         <required-permissions join-type="OR">
             <check-permission permission="CATALOG_UPDATE"/>
@@ -784,8 +784,8 @@ under the License.
         <attribute name="fromDate" type="Timestamp" mode="IN" optional="false"/>
     </service>
 
-    <service name="addPartyToProduct" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="addPartyToProduct" auth="true">
+    <service name="addPartyToProduct" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="addPartyToProduct" auth="true">
         <description>Add Party To Product</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="partyId" type="String" mode="IN" optional="false"/>
@@ -795,8 +795,8 @@ under the License.
         <attribute name="sequenceNum" type="Long" mode="IN" optional="true"/>
         <attribute name="comments" type="String" mode="IN" optional="true"/>
     </service>
-    <service name="updatePartyToProduct" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="updatePartyToProduct" auth="true">
+    <service name="updatePartyToProduct" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updatePartyToProduct" auth="true">
         <description>Update Party To Product</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="partyId" type="String" mode="IN" optional="false"/>
@@ -806,8 +806,8 @@ under the License.
         <attribute name="sequenceNum" type="Long" mode="IN" optional="true"/>
         <attribute name="comments" type="String" mode="IN" optional="true"/>
     </service>
-    <service name="removePartyFromProduct" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="removePartyFromProduct" auth="true">
+    <service name="removePartyFromProduct" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="removePartyFromProduct" auth="true">
         <description>Remove Party From Product</description>
         <attribute name="productId" type="String" mode="IN" optional="false"/>
         <attribute name="partyId" type="String" mode="IN" optional="false"/>
@@ -1240,16 +1240,16 @@ under the License.
     </service>
 
     <!-- Permission Services -->
-    <service name="productGenericPermission" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="productGenericPermission">
+    <service name="productGenericPermission" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="productGenericPermission">
         <implements service="permissionInterface"/>
     </service>
     <service name="productCategoryGenericPermission" engine="groovy"
         location="component://product/groovyScripts/product/category/CategoryServices.groovy" invoke="productCategoryGenericPermission">
         <implements service="permissionInterface"/>
     </service>
-    <service name="productPriceGenericPermission" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="productPriceGenericPermission">
+    <service name="productPriceGenericPermission" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="productPriceGenericPermission">
         <implements service="permissionInterface"/>
     </service>
     <service name="checkCategoryPermissionWithViewPurchaseAllow" engine="groovy"
@@ -1285,20 +1285,20 @@ under the License.
     </service>
 
     <!-- ProductCategoryGlAccount Services -->
-    <service name="createProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="createProductCategoryGlAccount" auth="true">
+    <service name="createProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="createProductCategoryGlAccount" auth="true">
         <description>Create a ProductCategoryGlAccount</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="false"/>
     </service>
-    <service name="updateProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="updateProductCategoryGlAccount" auth="true">
+    <service name="updateProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updateProductCategoryGlAccount" auth="true">
         <description>Update a ProductCategoryGlAccount</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="false"/>
     </service>
-    <service name="deleteProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="simple"
-                location="component://product/minilang/product/product/ProductServices.xml" invoke="deleteProductCategoryGlAccount" auth="true">
+    <service name="deleteProductCategoryGlAccount" default-entity-name="ProductCategoryGlAccount" engine="groovy"
+                location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="deleteProductCategoryGlAccount" auth="true">
         <description>Delete a ProductCategoryGlAccount</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
     </service>
@@ -1608,48 +1608,48 @@ under the License.
     </service>
 
     <!-- Product GroupOrder Services -->
-    <service name="createProductGroupOrder" default-entity-name="ProductGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="createProductGroupOrder" auth="true">
+    <service name="createProductGroupOrder" default-entity-name="ProductGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="createProductGroupOrder" auth="true">
         <description>Create ProductGroupOrder</description>
         <auto-attributes include="pk" mode="OUT" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
     </service>
 
-    <service name="updateProductGroupOrder" default-entity-name="ProductGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="updateProductGroupOrder" auth="true">
+    <service name="updateProductGroupOrder" default-entity-name="ProductGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="updateProductGroupOrder" auth="true">
         <description>Update ProductGroupOrder</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
     </service>
 
-    <service name="deleteProductGroupOrder" default-entity-name="ProductGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="deleteProductGroupOrder" auth="true">
+    <service name="deleteProductGroupOrder" default-entity-name="ProductGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="deleteProductGroupOrder" auth="true">
         <description>Delete ProductGroupOrder</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
     </service>
 
-    <service name="createJobForProductGroupOrder" default-entity-name="ProductGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="createJobForProductGroupOrder" auth="true">
+    <service name="createJobForProductGroupOrder" default-entity-name="ProductGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="createJobForProductGroupOrder" auth="true">
         <description>Create Job For ProductGroupOrder</description>
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
     </service>
 
-    <service name="checkOrderItemForProductGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="checkOrderItemForProductGroupOrder" auth="true">
+    <service name="checkOrderItemForProductGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="checkOrderItemForProductGroupOrder" auth="true">
         <description>Check OrderItem For ProductGroupOrder</description>
         <attribute name="orderId" mode="IN" type="String" optional="false"/>
     </service>
 
-    <service name="cancleOrderItemGroupOrder" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="cancleOrderItemGroupOrder" auth="true">
+    <service name="cancleOrderItemGroupOrder" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="cancleOrderItemGroupOrder" auth="true">
         <description>Cancle OrderItemGroupOrder</description>
         <attribute name="orderId" mode="IN" type="String" optional="false"/>
         <attribute name="orderItemSeqId" type="String" mode="IN" optional="true"/>
     </service>
 
-    <service name="checkProductGroupOrderExpired" engine="simple"
-        location="component://product/minilang/product/product/ProductServices.xml" invoke="checkProductGroupOrderExpired" auth="true">
+    <service name="checkProductGroupOrderExpired" engine="groovy"
+        location="component://product/groovyScripts/product/product/ProductServices.groovy" invoke="checkProductGroupOrderExpired" auth="true">
         <description>Check ProductGroupOrder Expired</description>
         <attribute name="groupOrderId" mode="IN" type="String" optional="false"/>
     </service>