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 2c8f52995cf2f9e9ddf5d5a43bd567a9fd2ba69b Author: diveshdut <[email protected]> AuthorDate: Thu Aug 13 15:19:26 2026 +0530 OFBIZ-13480 Align manufacturing lookups with REST query helpers Updates reusable manufacturing lookup services to expose framework-compatible sorting, pagination metadata, pagination links, and view permissions across product, party, fixed asset, and facility lookup flows. Lookup queries now resolve public sort expressions through the shared REST helper while preserving endpoint-specific defaults and aliases. Tests cover product and facility sort mappings, default ordering, unsupported sort handling, missing request binding, and generated pagination links. --- .../manufacturing/servicedef/services_lookup.xml | 22 +++++ .../api/ManufacturingLookupServices.groovy | 84 ++++++++++++++----- .../api/ManufacturingLookupServicesTests.groovy | 95 ++++++++++++++++++++++ 3 files changed, 181 insertions(+), 20 deletions(-) diff --git a/applications/manufacturing/servicedef/services_lookup.xml b/applications/manufacturing/servicedef/services_lookup.xml index df53c8f9a6..956f9dd2f0 100644 --- a/applications/manufacturing/servicedef/services_lookup.xml +++ b/applications/manufacturing/servicedef/services_lookup.xml @@ -25,6 +25,7 @@ under the License. <service name="findCostComponentOptions" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy" invoke="findCostComponentOptions"> <description>Find cost component types and calculation records used by manufacturing setup screens, so REST clients can populate cost-rate selectors without issuing separate generic entity queries.</description> + <permission-service service-name="manufacturingPermissionService" main-action="VIEW"/> <attribute mode="OUT" name="costComponentTypes" optional="false" type="java.util.List"/> <attribute mode="OUT" name="costComponentCalcs" optional="false" type="java.util.List"/> </service> @@ -34,11 +35,15 @@ under the License. <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="IN" name="sort" optional="true" type="String"/> <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="previousPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="nextPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="links" optional="true" type="Map"/> <attribute mode="OUT" name="facilities" optional="false" type="java.util.List"/> </service> <service name="findManufacturingPlants" engine="groovy" auth="true" @@ -47,16 +52,21 @@ under the License. <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="IN" name="sort" optional="true" type="String"/> <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="previousPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="nextPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="links" optional="true" type="Map"/> <attribute mode="OUT" name="facilities" optional="false" type="java.util.List"/> </service> <service name="findFixedAssets" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy" invoke="findFixedAssets"> <description>Find fixed assets available to manufacturing workflows, with REST paging and sorting, so routing and other manufacturing applications can select equipment or tool references from a stable lookup payload.</description> + <permission-service service-name="manufacturingPermissionService" main-action="VIEW"/> <attribute mode="IN" name="fixedAssetTypeId" optional="true" type="String"/> <attribute mode="IN" name="pageIndex" optional="true" type="Integer"/> <attribute mode="IN" name="pageSize" optional="true" type="Integer"/> @@ -66,24 +76,33 @@ under the License. <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="previousPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="nextPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="links" optional="true" type="Map"/> <attribute mode="OUT" name="fixedAssets" optional="false" type="java.util.List"/> </service> <service name="searchProducts" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy" invoke="searchProducts"> <description>Search products by ID, product name, or internal name using the shared manufacturing product lookup shape, so REST clients can reuse one product picker contract across BOM, routing, and related manufacturing flows.</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="IN" name="sort" optional="true" type="String"/> <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="previousPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="nextPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="links" optional="true" type="Map"/> <attribute mode="OUT" name="products" optional="false" type="java.util.List"/> </service> <service name="searchParties" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServices.groovy" invoke="searchParties"> <description>Search parties by ID or display name with REST paging and sorting, so manufacturing applications can assign people or groups to operations without depending on UI-specific party lookup screens.</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"/> @@ -93,6 +112,9 @@ under the License. <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="previousPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="nextPageCount" optional="true" type="Long"/> + <attribute mode="OUT" name="links" optional="true" type="Map"/> <attribute mode="OUT" name="parties" optional="false" type="java.util.List"/> </service> </services> 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 index 84013dbc1c..16d93dd103 100644 --- 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 @@ -36,9 +36,16 @@ Map findProductLookupOptions() { } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } + List orderBy + try { + orderBy = productLookupOrderBy(queryOptions.sort) + } 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)) + return success(RestApiUtil.getPagedResult('products', [], queryOptions, 0L, + RestApiUtil.getRelativeRequestPath(binding))) } EntityCondition searchCondition = EntityUtil.upperLikeAny(['productId', 'productName', 'internalName'], queryText) @@ -46,7 +53,7 @@ Map findProductLookupOptions() { .select('productId', 'productName', 'internalName', 'productTypeId', 'quantityUomId') .where(searchCondition) long totalCount = productQuery.queryCount() - List productRows = productQuery.orderBy('productId') + List productRows = productQuery.orderBy(orderBy) .queryPagedList(queryOptions.pageIndex, queryOptions.pageSize).getData() List products = productRows.collect { GenericValue product -> String productName = ManufacturingServiceUtil.displayProductName(product) @@ -59,7 +66,8 @@ Map findProductLookupOptions() { label: productName ] } - return success(RestApiUtil.getPagedResult('products', products, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('products', products, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) } Map searchProducts() { @@ -76,19 +84,14 @@ Map searchParties() { Map filters = queryOptions.filters List orderBy try { - orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [ - partyId: 'partyId', - partyName: 'groupName', - groupName: 'groupName', - firstName: 'firstName', - lastName: 'lastName' - ], ['partyId']) + orderBy = partyLookupOrderBy(queryOptions.sort) } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } String queryText = filters.query?.trim() if (UtilValidate.isEmpty(queryText)) { - return success(RestApiUtil.getPagedResult('parties', [], queryOptions, 0L, null)) + return success(RestApiUtil.getPagedResult('parties', [], queryOptions, 0L, + RestApiUtil.getRelativeRequestPath(binding))) } EntityCondition searchCondition = EntityUtil.upperLikeAny(['partyId', 'firstName', 'lastName', 'groupName'], queryText) @@ -107,7 +110,8 @@ Map searchParties() { label: partyName ] } - return success(RestApiUtil.getPagedResult('parties', parties, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('parties', parties, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) } Map findFixedAssets() { @@ -120,11 +124,7 @@ Map findFixedAssets() { Map filters = queryOptions.filters List orderBy try { - orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [ - fixedAssetId: 'fixedAssetId', - fixedAssetName: 'fixedAssetName', - fixedAssetTypeId: 'fixedAssetTypeId' - ], ['fixedAssetName', 'fixedAssetId']) + orderBy = fixedAssetLookupOrderBy(queryOptions.sort) } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } @@ -149,7 +149,8 @@ Map findFixedAssets() { label: fixedAssetName ] } - return success(RestApiUtil.getPagedResult('fixedAssets', fixedAssets, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('fixedAssets', fixedAssets, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) } Map findCostComponentOptions() { @@ -202,11 +203,17 @@ Map findFacilitiesByTypes(List facilityTypeIds) { } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } + List orderBy + try { + orderBy = facilityLookupOrderBy(queryOptions.sort) + } 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') + List facilities = query.orderBy(orderBy) .queryPagedList(queryOptions.pageIndex, queryOptions.pageSize).getData() .collect { GenericValue facility -> String facilityId = facility.facilityId @@ -219,5 +226,42 @@ Map findFacilitiesByTypes(List facilityTypeIds) { label: label ] } - return success(RestApiUtil.getPagedResult('facilities', facilities, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('facilities', facilities, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) +} + +List productLookupOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + productId: 'productId', + productName: 'productName', + internalName: 'internalName', + productTypeId: 'productTypeId', + quantityUomId: 'quantityUomId' + ], ['productId']) +} + +List partyLookupOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + partyId: 'partyId', + partyName: 'groupName', + groupName: 'groupName', + firstName: 'firstName', + lastName: 'lastName' + ], ['partyId']) +} + +List fixedAssetLookupOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + fixedAssetId: 'fixedAssetId', + fixedAssetName: 'fixedAssetName', + fixedAssetTypeId: 'fixedAssetTypeId' + ], ['fixedAssetName', 'fixedAssetId']) +} + +List facilityLookupOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + facilityId: 'facilityId', + facilityName: 'facilityName', + facilityTypeId: 'facilityTypeId' + ], ['facilityName', 'facilityId']) } diff --git a/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServicesTests.groovy b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServicesTests.groovy new file mode 100644 index 0000000000..50204fd134 --- /dev/null +++ b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/ManufacturingLookupServicesTests.groovy @@ -0,0 +1,95 @@ +/* + * 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.ws.rs.util.RestApiUtil +import org.apache.ofbiz.ws.rs.util.RestQueryOptions +import org.apache.ofbiz.service.ServiceUtil +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest + +class ManufacturingLookupServicesTests { + + @Test + void testProductSortMapsFrameworkFields() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + + assert services.productLookupOrderBy('-productName') == ['-productName', 'productId'] + } + + @Test + void testProductLookupRejectsUnsupportedSortWithoutQuery() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + services.binding.setVariable('parameters', [sort: 'notAField']) + + Map result = services.findProductLookupOptions() + + assert ServiceUtil.isError(result) + assert ServiceUtil.getErrorMessage(result) == 'Unsupported sort field: notAField' + } + + @Test + void testFacilitySortMapsFrameworkFields() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + + assert services.facilityLookupOrderBy('-facilityName') == ['-facilityName', 'facilityId'] + } + + @Test + void testLookupDefaultSortsMatchContracts() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + + assert services.productLookupOrderBy(null) == ['productId'] + assert services.partyLookupOrderBy(null) == ['partyId'] + assert services.fixedAssetLookupOrderBy(null) == ['fixedAssetName', 'fixedAssetId'] + assert services.facilityLookupOrderBy(null) == ['facilityName', 'facilityId'] + } + + @Test + void testFrameworkRequestPathReturnsNullWithoutRequestBinding() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + + assert RestApiUtil.getRelativeRequestPath(services.binding) == null + } + + @Test + void testFrameworkNavigationMetadataAndLinksUseRequestPath() { + ManufacturingLookupServices services = new ManufacturingLookupServices() + MockHttpServletRequest request = new MockHttpServletRequest() + request.setRequestURI('/rest/manufacturing/lookups/products') + request.setQueryString('pageIndex=1&pageSize=1&query=MAT') + services.binding.setVariable('request', request) + RestQueryOptions queryOptions = RestQueryOptions.fromParameters([ + pageIndex: 1, + pageSize: 1, + query: 'MAT' + ]) + + Map result = RestApiUtil.getPagedResult('products', [], queryOptions, 3L, + RestApiUtil.getRelativeRequestPath(services.binding)) + + assert result.previousPageCount == 1L + assert result.nextPageCount == 1L + assert result.links instanceof Map + assert result.links.self.href == '/rest/manufacturing/lookups/products?query=MAT&pageIndex=1&pageSize=1' + assert result.links.prev.href == '/rest/manufacturing/lookups/products?query=MAT&pageIndex=0&pageSize=1' + assert result.links.next.href == '/rest/manufacturing/lookups/products?query=MAT&pageIndex=2&pageSize=1' + } + +}

