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

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

commit c8170e9fff55bff50eec5b55e35a3890fdbf195a
Author: diveshdut <[email protected]>
AuthorDate: Fri Jul 31 14:24:39 2026 +0530

    OFBIZ-13468 Improved: add BOM management service APIs
---
 .../manufacturing/config/ManufacturingUiLabels.xml |  21 +
 .../manufacturing/servicedef/services_bom.xml      | 143 ++++++
 .../manufacturing/api/BomManagementServices.groovy | 497 +++++++++++++++++++++
 .../api/ManufacturingLookupServices.groovy         |  93 ++++
 .../api/ManufacturingServiceUtil.groovy            |  31 ++
 .../ofbiz/manufacturing/bom/BOMServices.java       |  29 +-
 6 files changed, 798 insertions(+), 16 deletions(-)

diff --git a/applications/manufacturing/config/ManufacturingUiLabels.xml 
b/applications/manufacturing/config/ManufacturingUiLabels.xml
index 0ab553324c..8a87582410 100644
--- a/applications/manufacturing/config/ManufacturingUiLabels.xml
+++ b/applications/manufacturing/config/ManufacturingUiLabels.xml
@@ -994,6 +994,9 @@
         <value xml:lang="zh">物料清单</value>
         <value xml:lang="zh-TW">物料清單</value>
     </property>
+    <property key="ManufacturingBomAssociationNotFound">
+        <value xml:lang="en">BOM association was not found.</value>
+    </property>
     <property key="ManufacturingBomErrorCreatingBillOfMaterialsTree">
         <value xml:lang="en">Error creating bill of materials tree: 
${errorString}</value>
         <value xml:lang="it">Errore durante la creazione dell'albero dei 
materiali: ${errorString}</value>
@@ -1079,6 +1082,18 @@
         <value xml:lang="zh">物料清单仿真</value>
         <value xml:lang="zh-TW">物料清單模擬</value>
     </property>
+    <property key="ManufacturingBomSimulationAvailabilityCheckFailed">
+        <value xml:lang="en">Availability check failed for product 
${productId}: ${errorString}</value>
+    </property>
+    <property key="ManufacturingBomSimulationAvailabilityNotChecked">
+        <value xml:lang="en">Availability not checked because no facility was 
selected.</value>
+    </property>
+    <property key="ManufacturingBomSimulationCostCheckFailed">
+        <value xml:lang="en">Cost check failed for product ${productId}: 
${errorString}</value>
+    </property>
+    <property key="ManufacturingBomSimulationCostNotChecked">
+        <value xml:lang="en">Cost not checked because no currency was 
selected.</value>
+    </property>
     <property key="ManufacturingBomType">
         <value xml:lang="de">Stücklistenart</value>
         <value xml:lang="en">Bom Type</value>
@@ -3598,6 +3613,9 @@
         <value xml:lang="zh">产品标识</value>
         <value xml:lang="zh-TW">產品識別</value>
     </property>
+    <property key="ManufacturingProductIdRequired">
+        <value xml:lang="en">Product ID is required.</value>
+    </property>
     <property key="ManufacturingProductIdTo">
         <value xml:lang="de">Produkt-Nr. an</value>
         <value xml:lang="en">Product ID To</value>
@@ -3653,6 +3671,9 @@
         <value xml:lang="zh">产品不存在</value>
         <value xml:lang="zh-TW">產品不存在</value>
     </property>
+    <property key="ManufacturingProductNotFound">
+        <value xml:lang="en">Product not found: ${productId}</value>
+    </property>
     <property key="ManufacturingProductRoutingNotExist">
         <value xml:lang="de">Arbeitsplan-Verbindung nicht vorhanden</value>
         <value xml:lang="en">Product Routing link doesn't exist</value>
diff --git a/applications/manufacturing/servicedef/services_bom.xml 
b/applications/manufacturing/servicedef/services_bom.xml
index a7a78299ed..9be7f1fb72 100644
--- a/applications/manufacturing/servicedef/services_bom.xml
+++ b/applications/manufacturing/servicedef/services_bom.xml
@@ -146,4 +146,147 @@ under the License.
         <attribute mode="IN" name="fromDate" optional="true" type="String"/>
         <attribute mode="OUT" name="notAssembledComponents" 
type="java.util.List"/>
     </service>
+    <!-- API wrappers for BOM Management -->
+    <service name="findBomProducts" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="findBomProducts">
+        <description>Find BOM-managed products that have component 
associations and return BOM summary metadata for list views.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="query" optional="true" type="String"/>
+        <attribute mode="IN" name="productId" optional="true" type="String"/>
+        <attribute mode="IN" name="componentProductId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="productIdTo" optional="true" type="String"/>
+        <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
+        <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
+        <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
+        <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
+        <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="products" optional="false" 
type="java.util.List"/>
+    </service>
+    <service name="findProductLookupOptions" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy"
 invoke="findProductLookupOptions">
+        <description>Find any product by id, name, or internal name for BOM 
picker and autocomplete workflows.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="query" optional="true" type="String"/>
+        <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
+        <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
+        <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
+        <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
+        <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="products" optional="false" 
type="java.util.List"/>
+    </service>
+    <service name="getBomSnapshot" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="getBomSnapshot">
+        <description>Get a frontend-friendly BOM snapshot for one 
product.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="productId" optional="false" type="String"/>
+        <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="fromDate" optional="true" type="Timestamp"/>
+        <attribute mode="IN" name="asOfDate" optional="true" type="Timestamp"/>
+        <attribute mode="IN" name="quantity" optional="true" 
type="BigDecimal"/>
+        <attribute mode="IN" name="amount" optional="true" type="BigDecimal"/>
+        <attribute mode="IN" name="type" optional="true" type="Integer"/>
+        <attribute mode="IN" name="facilityId" optional="true" type="String"/>
+        <attribute mode="IN" name="currencyUomId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productId" optional="false" type="String"/>
+        <attribute mode="OUT" name="productName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="internalName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productAssocTypeId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="bomType" optional="false" type="String"/>
+        <attribute mode="OUT" name="fromDate" optional="true" 
type="Timestamp"/>
+        <attribute mode="OUT" name="quantity" optional="true" 
type="BigDecimal"/>
+        <attribute mode="OUT" name="amount" optional="true" type="BigDecimal"/>
+        <attribute mode="OUT" name="type" optional="true" type="Integer"/>
+        <attribute mode="OUT" name="facilityId" optional="true" type="String"/>
+        <attribute mode="OUT" name="currencyUomId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="componentCount" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="usedInCount" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="usedInParents" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="bomLevels" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="components" optional="false" 
type="java.util.List"/>
+    </service>
+    <service name="removeBomAssociation" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="removeBomAssociation">
+        <description>Expire a BOM component association by setting 
ProductAssoc.thruDate instead of deleting either product. This REST-facing 
wrapper validates the association, defaults the BOM association type when 
omitted, delegates persistence to updateProductAssoc, and returns a BOM-shaped 
removal response.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="UPDATE"/>
+        <attribute mode="IN" name="productId" optional="false" type="String"/>
+        <attribute mode="IN" name="productIdTo" optional="false" 
type="String"/>
+        <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="fromDate" optional="false" 
type="Timestamp"/>
+        <attribute mode="OUT" name="removed" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="productId" optional="false" type="String"/>
+        <attribute mode="OUT" name="parentProductId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productIdTo" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="componentProductId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="fromDate" optional="true" 
type="Timestamp"/>
+        <attribute mode="OUT" name="thruDate" optional="true" 
type="Timestamp"/>
+    </service>
+    <service name="removeBomParentAssociation" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="removeBomParentAssociation">
+        <description>Expire a used-in parent association for a component 
product by setting ProductAssoc.thruDate. This REST-facing wrapper keeps the 
parent route contract explicit, maps componentProductId to the native 
ProductAssoc key fields, delegates persistence to updateProductAssoc, and 
returns the same BOM-shaped removal response as component removal.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="UPDATE"/>
+        <attribute mode="IN" name="componentProductId" optional="false" 
type="String"/>
+        <attribute mode="IN" name="productId" optional="false" type="String"/>
+        <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="fromDate" optional="false" 
type="Timestamp"/>
+        <attribute mode="OUT" name="removed" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="productId" optional="false" type="String"/>
+        <attribute mode="OUT" name="parentProductId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productIdTo" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="componentProductId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="fromDate" optional="true" 
type="Timestamp"/>
+        <attribute mode="OUT" name="thruDate" optional="true" 
type="Timestamp"/>
+    </service>
+    <service name="runBomSimulation" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="runBomSimulation">
+        <description>Run a BOM explosion simulation for REST/API clients. 
Delegates BOM traversal to getBOMTree and existing cost/availability services, 
then converts the internal BOMTree result into JSON-ready parameters, summary, 
availability rows, cost rows, and warnings.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="productId" optional="false" type="String"/>
+        <attribute mode="IN" name="productAssocTypeId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="effectiveDate" optional="true" 
type="Timestamp"/>
+        <attribute mode="IN" name="fromDate" optional="true" type="Timestamp"/>
+        <attribute mode="IN" name="asOfDate" optional="true" type="Timestamp"/>
+        <attribute mode="IN" name="quantity" optional="true" 
type="BigDecimal"/>
+        <attribute mode="IN" name="simulationMode" optional="true" 
type="String"/>
+        <attribute mode="IN" name="type" optional="true" type="Integer"/>
+        <attribute mode="IN" name="facilityId" optional="true" type="String"/>
+        <attribute mode="IN" name="currencyUomId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="parameters" optional="false" type="Map"/>
+        <attribute mode="OUT" name="summary" optional="false" type="Map"/>
+        <attribute mode="OUT" name="availabilityRows" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="costRows" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="warnings" optional="false" 
type="java.util.List"/>
+    </service>
+    <service name="findWarehouses" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy"
 invoke="findWarehouses">
+        <description>Find warehouse facilities available for manufacturing 
application workflows.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
+        <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
+        <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
+        <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
+        <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="facilities" optional="false" 
type="java.util.List"/>
+    </service>
+    <service name="findBomTypes" engine="groovy" auth="true"
+            
location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy"
 invoke="findBomTypes">
+        <description>Find product association types available for BOM 
workflows.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="VIEW"/>
+        <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/>
+        <attribute mode="IN" name="pageSize" optional="true" type="Integer"/>
+        <attribute mode="OUT" name="pageIndex" optional="false" 
type="Integer"/>
+        <attribute mode="OUT" name="pageSize" optional="false" type="Integer"/>
+        <attribute mode="OUT" name="totalCount" optional="false" type="Long"/>
+        <attribute mode="OUT" name="totalPages" optional="false" type="Long"/>
+        <attribute mode="OUT" name="hasNext" optional="false" type="Boolean"/>
+        <attribute mode="OUT" name="bomTypes" optional="false" 
type="java.util.List"/>
+    </service>
 </services>
diff --git 
a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
new file mode 100644
index 0000000000..7f60a52bfa
--- /dev/null
+++ 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/BomManagementServices.groovy
@@ -0,0 +1,497 @@
+/*
+ * 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.
+ */
+package org.apache.ofbiz.manufacturing.api
+
+import java.sql.Timestamp
+
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.base.util.UtilMisc
+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.condition.EntityCondition
+import org.apache.ofbiz.entity.condition.EntityOperator
+import org.apache.ofbiz.entity.util.EntityQuery
+import org.apache.ofbiz.entity.util.EntityUtil
+import org.apache.ofbiz.manufacturing.bom.BOMNode
+import org.apache.ofbiz.manufacturing.bom.BOMTree
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.ws.rs.util.RestApiUtil
+import org.apache.ofbiz.ws.rs.util.RestQueryOptions
+
+Map findBomProducts() {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    List bomTypeIds = bomAssocTypeIds()
+    if (!bomTypeIds) {
+        return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L, null))
+    }
+
+    List selectedBomTypeIds = 
UtilValidate.isNotEmpty(parameters.productAssocTypeId)
+            ? [parameters.productAssocTypeId]
+            : bomTypeIds
+    List conditions = [EntityCondition.makeCondition('productAssocTypeId', 
EntityOperator.IN, selectedBomTypeIds)]
+    if (UtilValidate.isNotEmpty(parameters.productId)) {
+        conditions.add(EntityCondition.makeCondition('productId', 
parameters.productId))
+    }
+    if (UtilValidate.isNotEmpty(parameters.componentProductId ?: 
parameters.productIdTo)) {
+        conditions.add(EntityCondition.makeCondition('productIdTo', 
parameters.componentProductId ?: parameters.productIdTo))
+    }
+    if (UtilValidate.isNotEmpty(parameters.query)) {
+        Set matchingProductIds = EntityUtil.searchIds(delegator, 'Product', 
'productId',
+                ['productId', 'productName', 'internalName'], 
parameters.query, 500)
+        if (!matchingProductIds) {
+            return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L, null))
+        }
+        conditions.add(EntityCondition.makeCondition('productId', 
EntityOperator.IN, matchingProductIds as List))
+    }
+
+    EntityCondition whereCondition = EntityCondition.makeCondition(conditions, 
EntityOperator.AND)
+    EntityQuery query = select('productId', 
'productAssocTypeId').from('ProductAssoc')
+            .where(whereCondition).filterByDate().distinct()
+    long totalCount = query.queryCount()
+    List rows = query.orderBy('productId', 'productAssocTypeId')
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+    Set productIds = rows*.productId.findAll { it } as Set
+    Map productsById = EntityUtil.lookupById(delegator, 'Product', 
'productId', productIds, false)
+    Map componentSummaries = componentSummaryByProduct(productIds, 
selectedBomTypeIds)
+    Map usedInCounts = EntityUtil.countByField(delegator, 'ProductAssoc', 
'productIdTo', productIds,
+            [EntityCondition.makeCondition('productAssocTypeId', 
EntityOperator.IN, selectedBomTypeIds)], true)
+
+    List products = rows.collect { GenericValue row ->
+        GenericValue product = productsById[row.productId]
+        Map componentSummary = componentSummaries[row.productId] ?: [:]
+        [
+                productId: row.productId,
+                productName: 
ManufacturingServiceUtil.displayProductName(product),
+                internalName: product?.internalName,
+                productAssocTypeId: row.productAssocTypeId,
+                bomType: row.productAssocTypeId,
+                componentCount: componentSummary.count ?: 0,
+                usedInCount: usedInCounts[row.productId] ?: 0,
+                activeFromDate: componentSummary.activeFromDate
+        ]
+    }
+    return success(RestApiUtil.getPagedResult('products', products, 
queryOptions, totalCount, null))
+}
+
+Map getBomSnapshot() {
+    String productId = parameters.productId
+    if (!productId) {
+        return 
ServiceUtil.returnError(manufacturingMessage('ManufacturingProductIdRequired'))
+    }
+    GenericValue product = from('Product').where(productId: 
productId).queryOne()
+    if (!product) {
+        return 
ServiceUtil.returnError(manufacturingMessage('ManufacturingProductNotFound',
+                [productId: productId]))
+    }
+
+    String productAssocTypeId = parameters.productAssocTypeId ?: 
'MANUF_COMPONENT'
+    Timestamp fromDate = parameters.fromDate
+            ?: parameters.asOfDate
+            ?: UtilDateTime.nowTimestamp()
+    BigDecimal quantity = parameters.quantity ?: BigDecimal.ONE
+    BigDecimal amount = parameters.amount ?: BigDecimal.ZERO
+    Integer type = UtilMisc.toIntegerObject(parameters.type) ?: 0
+
+    List componentRows = datedProductAssocRows([productId: productId, 
productAssocTypeId: productAssocTypeId],
+            fromDate, 'sequenceNum', 'productIdTo')
+    List components = snapshotComponentRows(componentRows, quantity)
+    List bomLevels = bomLevelRows(productId, productAssocTypeId, fromDate, 
quantity, type)
+    List parentRows = datedProductAssocRows([productIdTo: productId, 
productAssocTypeId: productAssocTypeId],
+            fromDate, 'sequenceNum', 'productId')
+    List usedInParents = snapshotParentRows(parentRows, product.quantityUomId 
as String)
+
+    return success(productId: productId, productName: 
ManufacturingServiceUtil.displayProductName(product), internalName: 
product.internalName,
+            productAssocTypeId: productAssocTypeId, bomType: 
productAssocTypeId, fromDate: fromDate, quantity: quantity,
+            amount: amount, type: type, facilityId: parameters.facilityId, 
currencyUomId: parameters.currencyUomId,
+            componentCount: components.size(), usedInCount: 
usedInParents.size(), usedInParents: usedInParents,
+            bomLevels: bomLevels, components: components)
+}
+
+List bomLevelRows(String productId, String productAssocTypeId, Timestamp 
fromDate, BigDecimal quantity, Integer type) {
+    Map treeResult = runService('getBOMTree', [
+            productId: productId,
+            bomType: productAssocTypeId,
+            fromDate: fromDate,
+            quantity: quantity,
+            type: type,
+            userLogin: userLogin
+    ].findAll { it.value != null })
+    if (ServiceUtil.isError(treeResult)) {
+        return []
+    }
+
+    BOMTree tree = treeResult.tree as BOMTree
+    List nodes = []
+    tree?.print(nodes)
+    return nodes.collect { BOMNode node ->
+        GenericValue nodeProduct = node.getProduct()
+        GenericValue productAssoc = node.getProductAssoc()
+        [
+                level: node.getDepth(),
+                productId: nodeProduct?.productId,
+                productName: 
ManufacturingServiceUtil.displayProductName(nodeProduct),
+                parentProductId: productAssoc?.productId,
+                componentProductId: node.getDepth() == 0 ? null : 
nodeProduct?.productId,
+                componentName: node.getDepth() == 0 ? null : 
ManufacturingServiceUtil.displayProductName(nodeProduct),
+                quantity: node.getQuantity(),
+                quantityUomId: nodeProduct?.quantityUomId,
+                fromDate: productAssoc?.fromDate,
+                thruDate: productAssoc?.thruDate,
+                productAssocTypeId: productAssoc?.productAssocTypeId ?: 
productAssocTypeId
+        ]
+    }
+}
+
+List datedProductAssocRows(Map fields, Timestamp fromDate, String... orderBy) {
+    return from('ProductAssoc')
+            .where(fields)
+            .filterByDate(fromDate)
+            .orderBy(orderBy as List)
+            .queryList()
+}
+
+List snapshotComponentRows(List componentRows, BigDecimal bomQuantity) {
+    Set componentProductIds = componentRows*.productIdTo.findAll { it } as Set
+    Set routingIds = componentRows*.routingWorkEffortId.findAll { it } as Set
+    Map componentProducts = EntityUtil.lookupById(delegator, 'Product', 
'productId',
+            componentProductIds, false)
+    Map routings = EntityUtil.lookupById(delegator, 'WorkEffort', 
'workEffortId', routingIds)
+    return componentRows.collect { GenericValue component ->
+        BigDecimal componentQuantity = component.getBigDecimal('quantity') ?: 
BigDecimal.ZERO
+        GenericValue componentProduct = 
componentProducts[component.productIdTo]
+        GenericValue routing = routings[component.routingWorkEffortId]
+        [
+                level: 1,
+                productId: component.productId,
+                componentProductId: component.productIdTo,
+                componentName: 
ManufacturingServiceUtil.displayProductName(componentProduct),
+                componentProductName: 
ManufacturingServiceUtil.displayProductName(componentProduct),
+                quantityUomId: componentProduct?.quantityUomId,
+                productAssocTypeId: component.productAssocTypeId,
+                fromDate: component.fromDate,
+                thruDate: component.thruDate,
+                sequenceNum: component.sequenceNum,
+                quantity: componentQuantity,
+                extendedQuantity: componentQuantity * bomQuantity,
+                scrapFactor: component.scrapFactor,
+                instruction: component.instruction,
+                routingWorkEffortId: component.routingWorkEffortId,
+                routingName: routing?.workEffortName,
+                estimateCalcMethod: component.estimateCalcMethod,
+                recurrenceInfoId: component.recurrenceInfoId
+        ]
+    }
+}
+
+List snapshotParentRows(List parentRows, String quantityUomId) {
+    Set parentProductIds = parentRows*.productId.findAll { it } as Set
+    Map parentProducts = EntityUtil.lookupById(delegator, 'Product', 
'productId', parentProductIds,
+            false)
+    return parentRows.collect { GenericValue association ->
+        GenericValue parentProduct = parentProducts[association.productId]
+        [
+                parentProductId: association.productId,
+                parentProductName: 
ManufacturingServiceUtil.displayProductName(parentProduct),
+                componentProductId: association.productIdTo,
+                quantity: association.getBigDecimal('quantity'),
+                quantityUomId: quantityUomId,
+                sequenceNum: association.sequenceNum,
+                fromDate: association.fromDate,
+                thruDate: association.thruDate,
+                productAssocTypeId: association.productAssocTypeId
+        ]
+    }
+}
+
+Map removeBomAssociation() {
+    return expireBomAssociation(parameters.productId, parameters.productIdTo, 
parameters.productAssocTypeId,
+            parameters.fromDate)
+}
+
+Map removeBomParentAssociation() {
+    return expireBomAssociation(parameters.productId, 
parameters.componentProductId, parameters.productAssocTypeId,
+            parameters.fromDate)
+}
+
+Map expireBomAssociation(String productId, String productIdTo, String 
assocTypeId, Timestamp fromDate) {
+    String productAssocTypeId = assocTypeId ?: 'MANUF_COMPONENT'
+    GenericValue association = from('ProductAssoc')
+            .where(productId: productId, productIdTo: productIdTo,
+                    productAssocTypeId: productAssocTypeId, fromDate: fromDate)
+            .queryOne()
+    if (!association) {
+        return 
ServiceUtil.returnError(manufacturingMessage('ManufacturingBomAssociationNotFound'))
+    }
+    Timestamp thruDate = UtilDateTime.nowTimestamp()
+    Map updateResult = runService('updateProductAssoc', [
+            productId: association.productId,
+            productIdTo: association.productIdTo,
+            productAssocTypeId: association.productAssocTypeId,
+            fromDate: association.fromDate,
+            thruDate: thruDate,
+            userLogin: userLogin
+    ])
+    if (ServiceUtil.isError(updateResult)) {
+        return updateResult
+    }
+    if (UtilValidate.isNotEmpty(updateResult.errorMessage)) {
+        return ServiceUtil.returnError(updateResult.errorMessage)
+    }
+    return success([
+            removed: true,
+            productId: association.productId,
+            parentProductId: association.productId,
+            productIdTo: association.productIdTo,
+            componentProductId: association.productIdTo,
+            productAssocTypeId: association.productAssocTypeId,
+            fromDate: association.fromDate,
+            thruDate: thruDate,
+            quantity: association.quantity,
+            sequenceNum: association.sequenceNum
+    ])
+}
+
+Map runBomSimulation() {
+    String productId = parameters.productId
+    if (!productId) {
+        return 
ServiceUtil.returnError(manufacturingMessage('ManufacturingProductIdRequired'))
+    }
+    String productAssocTypeId = parameters.productAssocTypeId ?: 
'MANUF_COMPONENT'
+    Timestamp effectiveDate = parameters.effectiveDate
+            ?: parameters.fromDate
+            ?: parameters.asOfDate
+    BigDecimal quantity = parameters.quantity ?: BigDecimal.ONE
+    String simulationMode = parameters.simulationMode ?: 'EXPLOSION'
+    String currencyUomId = parameters.currencyUomId
+    String facilityId = parameters.facilityId
+    List warnings = []
+
+    Integer type = UtilMisc.toIntegerObject(parameters.type)
+    if (type == null) {
+        type = simulationMode == 'IMPLOSION' ? BOMTree.IMPLOSION : 
BOMTree.EXPLOSION
+    }
+    Map treeResult = runService('getBOMTree', [
+            productId: productId,
+            bomType: productAssocTypeId,
+            fromDate: effectiveDate,
+            quantity: quantity,
+            type: type,
+            userLogin: userLogin
+    ].findAll { it.value != null })
+    if (ServiceUtil.isError(treeResult)) {
+        return treeResult
+    }
+
+    GenericValue rootProduct = from('Product').where(productId: 
productId).cache().queryOne()
+    BOMTree tree = treeResult.tree as BOMTree
+    List nodes = []
+    tree?.print(nodes)
+    List componentNodes = nodes.findAll { BOMNode node -> node && 
node.getDepth() > 0 }
+    Set productsWithCostSources = costSourceProductIds(componentNodes.collect 
{ BOMNode node -> node.getProduct()?.productId },
+            currencyUomId)
+    List availabilityRows = componentNodes.collect { BOMNode node ->
+        availabilityRow(node, facilityId, warnings)
+    }
+    List costRows = componentNodes.collect { BOMNode node ->
+        costRow(node, currencyUomId, warnings, productsWithCostSources)
+    }
+    BigDecimal knownMaterialCost = costRows.inject(BigDecimal.ZERO) { 
BigDecimal total, Map row ->
+        row.totalCost != null ? total.add(row.totalCost as BigDecimal) : total
+    }
+    int missingCostCount = costRows.count { Map row -> row.costStatus == 
'MISSING_COST' }
+    if (!facilityId) {
+        
warnings.add(manufacturingMessage('ManufacturingBomSimulationAvailabilityNotChecked'))
+    }
+    if (!currencyUomId) {
+        
warnings.add(manufacturingMessage('ManufacturingBomSimulationCostNotChecked'))
+    }
+
+    Map requestParameters = [
+            productId: productId,
+            productName: 
ManufacturingServiceUtil.displayProductName(rootProduct),
+            quantity: quantity,
+            productAssocTypeId: productAssocTypeId,
+            facilityId: facilityId,
+            currencyUomId: currencyUomId,
+            effectiveDate: effectiveDate,
+            simulationMode: simulationMode
+    ]
+    Map summary = [
+            requiredComponentCount: componentNodes.size(),
+            knownMaterialCost: knownMaterialCost,
+            missingCostCount: missingCostCount,
+            availabilityStatus: facilityId ? 'CHECKED' : 'NOT_CHECKED'
+    ]
+    return success(parameters: requestParameters, summary: summary, 
availabilityRows: availabilityRows,
+            costRows: costRows, warnings: warnings.unique())
+}
+
+Map findBomTypes() {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    EntityQuery query = from('ProductAssocType')
+            .select('productAssocTypeId', 'description')
+            .where(parentTypeId: 'PRODUCT_COMPONENT')
+            .cache(true)
+    long totalCount = query.queryCount()
+    List bomTypes = query.orderBy('description', 'productAssocTypeId')
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+            .collect { GenericValue productAssocType ->
+                String productAssocTypeId = productAssocType.productAssocTypeId
+                String description = productAssocType.description ?: 
productAssocTypeId
+                [
+                        productAssocTypeId: productAssocTypeId,
+                        description: description,
+                        label: description
+                ]
+            }
+    return success(RestApiUtil.getPagedResult('bomTypes', bomTypes, 
queryOptions, totalCount, null))
+}
+
+List bomAssocTypeIds() {
+    return from('ProductAssocType').where(parentTypeId: 'PRODUCT_COMPONENT')
+            .cache(true).getFieldList('productAssocTypeId') ?: []
+}
+
+Map componentSummaryByProduct(Collection productIds, List bomTypeIds) {
+    if (!productIds || !bomTypeIds) {
+        return [:]
+    }
+    List components = from('ProductAssoc')
+            .select('productId', 'fromDate')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('productId', 
EntityOperator.IN, productIds as List),
+                    EntityCondition.makeCondition('productAssocTypeId', 
EntityOperator.IN, bomTypeIds)
+            ], EntityOperator.AND))
+            .filterByDate()
+            .queryList()
+    return components.groupBy { it.productId }.collectEntries { String id, 
List rows ->
+        [(id): [count: rows.size(), activeFromDate: rows*.fromDate.min()]]
+    }
+}
+
+Map availabilityRow(BOMNode node, String facilityId, List warnings) {
+    GenericValue product = node.getProduct()
+    BigDecimal requiredQuantity = node.getQuantity()
+    BigDecimal quantityOnHand = null
+    BigDecimal shortageQuantity = null
+    if (facilityId) {
+        Map inventoryResult = runService('getInventoryAvailableByFacility', [
+                productId: product.productId,
+                facilityId: facilityId,
+                userLogin: userLogin
+        ])
+        if (ServiceUtil.isError(inventoryResult)) {
+            
warnings.add(manufacturingMessage('ManufacturingBomSimulationAvailabilityCheckFailed',
+                    [productId: product.productId, errorString: 
ServiceUtil.getErrorMessage(inventoryResult)]))
+        } else {
+            quantityOnHand = inventoryResult.quantityOnHandTotal as BigDecimal
+            if (quantityOnHand != null && requiredQuantity != null) {
+                shortageQuantity = 
requiredQuantity.subtract(quantityOnHand).max(BigDecimal.ZERO)
+            }
+        }
+    }
+    return [
+            level: node.getDepth(),
+            productId: product.productId,
+            productName: ManufacturingServiceUtil.displayProductName(product),
+            requiredQuantity: requiredQuantity,
+            quantityUomId: product.quantityUomId,
+            quantityOnHand: quantityOnHand,
+            shortageQuantity: shortageQuantity,
+            availabilityStatus: facilityId ? 'CHECKED' : 'NOT_CHECKED'
+    ]
+}
+
+Map costRow(BOMNode node, String currencyUomId, List warnings, Set 
costSourceProductIds) {
+    GenericValue product = node.getProduct()
+    BigDecimal requiredQuantity = node.getQuantity()
+    BigDecimal unitCost = null
+    BigDecimal totalCost = null
+    String costStatus = 'MISSING_COST'
+    if (currencyUomId) {
+        if (costSourceProductIds.contains(product.productId)) {
+            Map costResult = runService('getProductCost', [
+                    productId: product.productId,
+                    currencyUomId: currencyUomId,
+                    costComponentTypePrefix: 'EST_STD',
+                    userLogin: userLogin
+            ])
+            if (ServiceUtil.isError(costResult)) {
+                
warnings.add(manufacturingMessage('ManufacturingBomSimulationCostCheckFailed',
+                        [productId: product.productId, errorString: 
ServiceUtil.getErrorMessage(costResult)]))
+            } else {
+                unitCost = costResult.productCost as BigDecimal
+                totalCost = unitCost != null && requiredQuantity != null ? 
unitCost * requiredQuantity : null
+                costStatus = 'KNOWN'
+            }
+        }
+    }
+    return [
+            level: node.getDepth(),
+            productId: product.productId,
+            productName: ManufacturingServiceUtil.displayProductName(product),
+            requiredQuantity: requiredQuantity,
+            unitCost: unitCost,
+            totalCost: totalCost,
+            costStatus: costStatus
+    ]
+}
+
+Set costSourceProductIds(Collection productIds, String currencyUomId) {
+    Set selectedProductIds = productIds.findAll { it } as Set
+    if (!currencyUomId || !selectedProductIds) {
+        return [] as Set
+    }
+    EntityCondition costCondition = EntityCondition.makeCondition([
+            EntityCondition.makeCondition('productId', EntityOperator.IN, 
selectedProductIds as List),
+            EntityCondition.makeCondition('costUomId', currencyUomId),
+            EntityCondition.makeCondition('costComponentTypeId', 
EntityOperator.LIKE, 'EST_STD_%')
+    ], EntityOperator.AND)
+    Set productIdsWithCosts = from('CostComponent')
+            .select('productId')
+            .where(costCondition)
+            .filterByDate()
+            .distinct()
+            .queryList()*.productId.findAll { it } as Set
+    Set supplierProductIds = from('SupplierProduct')
+            .select('productId')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('productId', 
EntityOperator.IN, selectedProductIds as List),
+                    EntityCondition.makeCondition('currencyUomId', 
currencyUomId)
+            ], EntityOperator.AND))
+            .filterByDate('availableFromDate', 'availableThruDate')
+            .distinct()
+            .queryList()*.productId.findAll { it } as Set
+    return productIdsWithCosts + supplierProductIds
+}
+
+String manufacturingMessage(String property, Map context = [:]) {
+    return UtilProperties.getMessage('ManufacturingUiLabels', property, 
context, locale)
+}
diff --git 
a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy
 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy
new file mode 100644
index 0000000000..19ae356b8c
--- /dev/null
+++ 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.ofbiz.manufacturing.api
+
+import org.apache.ofbiz.base.util.UtilValidate
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.entity.condition.EntityCondition
+import org.apache.ofbiz.entity.condition.EntityOperator
+import org.apache.ofbiz.entity.util.EntityQuery
+import org.apache.ofbiz.entity.util.EntityUtil
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.ws.rs.util.RestApiUtil
+import org.apache.ofbiz.ws.rs.util.RestQueryOptions
+
+Map findProductLookupOptions() {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    String queryText = parameters.query?.trim()
+    if (UtilValidate.isEmpty(queryText)) {
+        return success(RestApiUtil.getPagedResult('products', [], 
queryOptions, 0L, null))
+    }
+
+    EntityCondition searchCondition = EntityUtil.upperLikeAny(['productId', 
'productName', 'internalName'], queryText)
+    EntityQuery productQuery = from('Product')
+            .select('productId', 'productName', 'internalName', 
'productTypeId', 'quantityUomId')
+            .where(searchCondition)
+    long totalCount = productQuery.queryCount()
+    List productRows = productQuery.orderBy('productId')
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+    List products = productRows.collect { GenericValue product ->
+        String productName = 
ManufacturingServiceUtil.displayProductName(product)
+        [
+                productId: product.productId,
+                productName: productName,
+                internalName: product.internalName,
+                productTypeId: product.productTypeId,
+                quantityUomId: product.quantityUomId,
+                label: productName
+        ]
+    }
+    return success(RestApiUtil.getPagedResult('products', products, 
queryOptions, totalCount, null))
+}
+
+Map findWarehouses() {
+    return findFacilitiesByTypes(['WAREHOUSE', 'PLANT'])
+}
+
+Map findFacilitiesByTypes(List facilityTypeIds) {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    EntityQuery query = from('Facility')
+            .select('facilityId', 'facilityName', 'facilityTypeId')
+            .where(EntityCondition.makeCondition('facilityTypeId', 
EntityOperator.IN, facilityTypeIds))
+    long totalCount = query.queryCount()
+    List facilities = query.orderBy('facilityName', 'facilityId')
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+            .collect { GenericValue facility ->
+                String facilityId = facility.facilityId
+                String facilityName = facility.facilityName ?: facilityId
+                String label = facilityName == facilityId ? facilityId : 
"${facilityName} [${facilityId}]".toString()
+                [
+                        facilityId: facilityId,
+                        facilityName: facilityName,
+                        facilityTypeId: facility.facilityTypeId,
+                        label: label
+                ]
+    }
+    return success(RestApiUtil.getPagedResult('facilities', facilities, 
queryOptions, totalCount, null))
+}
diff --git 
a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingServiceUtil.groovy
 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingServiceUtil.groovy
new file mode 100644
index 0000000000..c1778e3e55
--- /dev/null
+++ 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingServiceUtil.groovy
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+package org.apache.ofbiz.manufacturing.api
+
+import org.apache.ofbiz.entity.GenericValue
+
+final class ManufacturingServiceUtil {
+
+    private ManufacturingServiceUtil() { }
+
+    static String displayProductName(GenericValue product) {
+        return product?.productName ?: product?.internalName ?: 
product?.productId
+    }
+
+}
diff --git 
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMServices.java
 
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMServices.java
index a019c5d996..97910d453a 100644
--- 
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMServices.java
+++ 
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMServices.java
@@ -30,6 +30,8 @@ import java.util.Locale;
 import java.util.Map;
 
 import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.base.util.ObjectType;
 import org.apache.ofbiz.base.util.UtilGenerics;
 import org.apache.ofbiz.base.util.UtilMisc;
 import org.apache.ofbiz.base.util.UtilProperties;
@@ -65,17 +67,10 @@ public class BOMServices {
         Map<String, Object> result = new HashMap<>();
         Delegator delegator = dctx.getDelegator();
         String productId = (String) context.get("productId");
-        String fromDateStr = (String) context.get("fromDate");
         String bomType = (String) context.get("bomType");
         Locale locale = (Locale) context.get("locale");
+        Date fromDate = getDateValue(context.get("fromDate"), locale);
 
-        Date fromDate = null;
-        if (UtilValidate.isNotEmpty(fromDateStr)) {
-            try {
-                fromDate = Timestamp.valueOf(fromDateStr);
-            } catch (Exception e) {
-            }
-        }
         if (fromDate == null) {
             fromDate = new Date();
         }
@@ -303,7 +298,6 @@ public class BOMServices {
         LocalDispatcher dispatcher = dctx.getDispatcher();
         GenericValue userLogin = (GenericValue) context.get("userLogin");
         String productId = (String) context.get("productId");
-        String fromDateStr = (String) context.get("fromDate");
         String bomType = (String) context.get("bomType");
         Integer type = (Integer) context.get("type");
         BigDecimal quantity = (BigDecimal) context.get("quantity");
@@ -313,13 +307,7 @@ public class BOMServices {
             type = 0;
         }
 
-        Date fromDate = null;
-        if (UtilValidate.isNotEmpty(fromDateStr)) {
-            try {
-                fromDate = Timestamp.valueOf(fromDateStr);
-            } catch (Exception e) {
-            }
-        }
+        Date fromDate = getDateValue(context.get("fromDate"), locale);
         if (fromDate == null) {
             fromDate = new Date();
         }
@@ -342,6 +330,15 @@ public class BOMServices {
         return result;
     }
 
+    private static Date getDateValue(Object dateValue, Locale locale) {
+        try {
+            Object convertedDate = 
ObjectType.simpleTypeOrObjectConvert(dateValue, "Timestamp", null, locale, 
false);
+            return convertedDate instanceof Timestamp ? (Timestamp) 
convertedDate : null;
+        } catch (GeneralException e) {
+            return null;
+        }
+    }
+
     /** It reads the product's bill of materials,
      * if necessary configures it, and it returns its (possibly configured) 
components in
      * a List of {@link BOMNode}).

Reply via email to