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}).

