You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by di...@apache.org on 2026/07/31 14:10:01 UTC
(ofbiz-framework) 03/04: OFBIZ-13468 Improved: add BOM management service APIs
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 <di...@hotwax.co>
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}).