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 64b282c4eb572ec81f44452169df62096f604632 Author: diveshdut <[email protected]> AuthorDate: Thu Aug 13 15:19:21 2026 +0530 OFBIZ-13480 Align routing lists with REST query helpers Adds framework-compatible paging metadata, pagination links, and explicit permission services to routing and routing-task list service contracts. Routing list queries now use shared REST sort-to-order-by resolution for routings, routing tasks, and routing purpose types. Tests cover public sort aliases, stable default ordering, unsupported sort validation, and generated navigation metadata. --- .../manufacturing/servicedef/services_routing.xml | 21 ++++- .../api/RoutingCostManagementServices.groovy | 68 +++++++++----- .../api/RoutingCostManagementServicesTests.groovy | 101 +++++++++++++++++++++ 3 files changed, 164 insertions(+), 26 deletions(-) diff --git a/applications/manufacturing/servicedef/services_routing.xml b/applications/manufacturing/servicedef/services_routing.xml index 19229ce6f2..e0e8b277fb 100644 --- a/applications/manufacturing/servicedef/services_routing.xml +++ b/applications/manufacturing/servicedef/services_routing.xml @@ -68,10 +68,11 @@ under the License. <attribute mode="OUT" name="setupTime" type="BigDecimal" optional="true"/> <attribute mode="OUT" name="taskUnitTime" type="BigDecimal" optional="true"/> </service> - <!-- API wrappers for the manufacturing suite Routing and Cost Management PWA --> + <!-- Routing and cost management services for reusable list and detail data. --> <service name="findProductRoutings" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="findProductRoutings"> <description>Find routing templates with product-link and task-count summary fields needed by routing management list views, while keeping create and update operations delegated to native WorkEffort services.</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="routingId" optional="true" type="String"/> @@ -86,11 +87,15 @@ 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="routings" optional="false" type="java.util.List"/> </service> <service name="findRoutingTaskPurposeTypeOptions" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="findRoutingTaskPurposeTypeOptions"> <description>Find routing task purpose type options from WorkEffortPurposeType using the same paged REST response shape as other manufacturing lookups, so routing task forms can load valid operation-purpose values from OFBiz data.</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="IN" name="sort" optional="true" type="String"/> @@ -99,11 +104,15 @@ 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="purposeTypes" optional="false" type="java.util.List"/> </service> <service name="searchRoutingTasksForRouting" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="searchRoutingTasksForRouting"> <description>Search reusable routing task definitions by ID or name for operation selection, returning only task records suitable for linking into a routing through native WorkEffortAssoc services.</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"/> @@ -113,11 +122,15 @@ 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="tasks" optional="false" type="java.util.List"/> </service> <service name="getRoutingDetails" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="getRoutingDetails"> <description>Get one routing template with its product outputs, associated operations, available task choices, and cost rows in one response, reducing REST round trips for routing detail screens without replacing native write services.</description> + <permission-service service-name="manufacturingPermissionService" main-action="VIEW"/> <attribute mode="IN" name="routingId" optional="true" type="String"/> <attribute mode="IN" name="workEffortId" optional="true" type="String"/> <attribute mode="OUT" name="routingId" optional="false" type="String"/> @@ -134,6 +147,7 @@ under the License. <service name="expireRoutingGoodStandard" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="expireRoutingGoodStandard"> <description>Expire a routing product or task output association by setting WorkEffortGoodStandard.thruDate, preserving historical routing data while giving REST delete operations an explicit service for the native composite key.</description> + <permission-service service-name="manufacturingPermissionService" main-action="UPDATE"/> <attribute mode="IN" name="workEffortId" optional="false" type="String"/> <attribute mode="IN" name="productId" optional="false" type="String"/> <attribute mode="IN" name="workEffortGoodStdTypeId" optional="false" type="String"/> @@ -148,6 +162,7 @@ under the License. <service name="findRoutingTasks" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="findRoutingTasks"> <description>Find routing task definitions with work center, cost, output, tool, and operator summary data needed by routing operation list views, while keeping task maintenance backed by native WorkEffort services.</description> + <permission-service service-name="manufacturingPermissionService" main-action="VIEW"/> <attribute mode="IN" name="query" optional="true" type="String"/> <attribute mode="IN" name="workEffortId" optional="true" type="String"/> <attribute mode="IN" name="routingTaskId" optional="true" type="String"/> @@ -161,11 +176,15 @@ 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="tasks" optional="false" type="java.util.List"/> </service> <service name="getRoutingTaskDetails" engine="groovy" auth="true" location="component://manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy" invoke="getRoutingTaskDetails"> <description>Get one routing task definition with routing usage, cost rates, outputs, tool standards, operator assignments, and fixed asset context in one response, giving operation detail screens a complete read model without adding write wrappers for native child services.</description> + <permission-service service-name="manufacturingPermissionService" main-action="VIEW"/> <attribute mode="IN" name="routingTaskId" optional="true" type="String"/> <attribute mode="IN" name="taskId" optional="true" type="String"/> <attribute mode="IN" name="workEffortId" optional="true" type="String"/> diff --git a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy index e2e3756d25..d40bb96cec 100644 --- a/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy +++ b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy @@ -42,14 +42,7 @@ Map findProductRoutings() { Map filters = queryOptions.filters List orderBy try { - orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [ - routingId: 'workEffortId', - workEffortId: 'workEffortId', - workEffortName: 'workEffortName', - currentStatusId: 'currentStatusId', - status: 'currentStatusId', - quantityToProduce: 'quantityToProduce' - ], ['workEffortId']) + orderBy = routingOrderBy(queryOptions.sort) } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } @@ -57,7 +50,8 @@ Map findProductRoutings() { if (UtilValidate.isNotEmpty(filters.productId)) { Set productRoutingIds = routingIdsForProducts([filters.productId] as Set) if (!productRoutingIds) { - return success(RestApiUtil.getPagedResult('routings', [], queryOptions, 0L, null)) + return success(RestApiUtil.getPagedResult('routings', [], queryOptions, 0L, + RestApiUtil.getRelativeRequestPath(binding))) } conditions.add(EntityCondition.makeCondition('workEffortId', EntityOperator.IN, productRoutingIds as List)) } @@ -74,7 +68,8 @@ Map findProductRoutings() { matchingRoutingIds.addAll(routingIdsForProducts(matchingProductIds)) } if (!matchingRoutingIds) { - return success(RestApiUtil.getPagedResult('routings', [], queryOptions, 0L, null)) + return success(RestApiUtil.getPagedResult('routings', [], queryOptions, 0L, + RestApiUtil.getRelativeRequestPath(binding))) } conditions.add(EntityCondition.makeCondition('workEffortId', EntityOperator.IN, matchingRoutingIds as List)) } @@ -107,7 +102,8 @@ Map findProductRoutings() { taskCount: taskCounts[routing.workEffortId] ?: 0 ] } - return success(RestApiUtil.getPagedResult('routings', routingList, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('routings', routingList, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) } Map getRoutingDetails() { @@ -203,19 +199,14 @@ Map routingTaskList(boolean requireQuery) { Map filters = queryOptions.filters List orderBy try { - orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [ - routingTaskId: 'workEffortId', - workEffortId: 'workEffortId', - workEffortName: 'workEffortName', - workEffortPurposeTypeId: 'workEffortPurposeTypeId', - fixedAssetId: 'fixedAssetId' - ], ['workEffortName', 'workEffortId']) + orderBy = routingTaskOrderBy(queryOptions.sort) } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } String queryText = filters.query?.toString()?.trim() if (requireQuery && UtilValidate.isEmpty(queryText)) { - return RestApiUtil.getPagedResult('tasks', [], queryOptions, 0L, null) + return RestApiUtil.getPagedResult('tasks', [], queryOptions, 0L, + RestApiUtil.getRelativeRequestPath(binding)) } List conditions = [EntityCondition.makeCondition('workEffortTypeId', 'ROU_TASK')] @@ -243,7 +234,8 @@ Map routingTaskList(boolean requireQuery) { 'workEffortPurposeTypeId', rows*.workEffortPurposeTypeId.findAll { it } as Set) List tasks = rows.collect { GenericValue task -> taskMap(task, null, workCenters, purposeTypes) } - return RestApiUtil.getPagedResult('tasks', tasks, queryOptions, totalCount, null) + return RestApiUtil.getPagedResult('tasks', tasks, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding)) } Map getRoutingTaskDetails() { @@ -267,10 +259,7 @@ Map findRoutingTaskPurposeTypeOptions() { } List orderBy try { - orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [ - workEffortPurposeTypeId: 'workEffortPurposeTypeId', - description: 'description' - ], ['workEffortPurposeTypeId']) + orderBy = routingPurposeTypeOrderBy(queryOptions.sort) } catch (IllegalArgumentException e) { return ServiceUtil.returnError(e.message) } @@ -291,7 +280,36 @@ Map findRoutingTaskPurposeTypeOptions() { ] } - return success(RestApiUtil.getPagedResult('purposeTypes', purposeTypes, queryOptions, totalCount, null)) + return success(RestApiUtil.getPagedResult('purposeTypes', purposeTypes, queryOptions, totalCount, + RestApiUtil.getRelativeRequestPath(binding))) +} + +List routingOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + routingId: 'workEffortId', + workEffortId: 'workEffortId', + workEffortName: 'workEffortName', + currentStatusId: 'currentStatusId', + status: 'currentStatusId', + quantityToProduce: 'quantityToProduce' + ], ['workEffortId']) +} + +List routingTaskOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + routingTaskId: 'workEffortId', + workEffortId: 'workEffortId', + workEffortName: 'workEffortName', + workEffortPurposeTypeId: 'workEffortPurposeTypeId', + fixedAssetId: 'fixedAssetId' + ], ['workEffortName', 'workEffortId']) +} + +List routingPurposeTypeOrderBy(String sortExpression) { + return RestApiUtil.resolveOrderBy(sortExpression, [ + workEffortPurposeTypeId: 'workEffortPurposeTypeId', + description: 'description' + ], ['workEffortPurposeTypeId']) } Map taskDetailMap(GenericValue task) { diff --git a/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServicesTests.groovy b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServicesTests.groovy new file mode 100644 index 0000000000..ed005cacc8 --- /dev/null +++ b/applications/manufacturing/src/test/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServicesTests.groovy @@ -0,0 +1,101 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertThrows + +import org.apache.ofbiz.ws.rs.util.RestApiUtil +import org.apache.ofbiz.ws.rs.util.RestQueryOptions +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest + +class RoutingCostManagementServicesTests { + + @Test + void testRoutingSortMapsFrameworkFields() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + assert services.routingOrderBy('-status') == ['-currentStatusId', 'workEffortId'] + } + + @Test + void testRoutingTaskSortMapsFrameworkFields() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + assert services.routingTaskOrderBy('-routingTaskId') == ['-workEffortId', 'workEffortName'] + } + + @Test + void testRoutingPurposeTypeSortMapsFrameworkFields() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + assert services.routingPurposeTypeOrderBy('-description') == ['-description', 'workEffortPurposeTypeId'] + } + + @Test + void testRoutingDefaultSortsMatchContracts() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + assert services.routingOrderBy(null) == ['workEffortId'] + assert services.routingTaskOrderBy(null) == ['workEffortName', 'workEffortId'] + assert services.routingPurposeTypeOrderBy(null) == ['workEffortPurposeTypeId'] + } + + @Test + void testRoutingRejectsUnsupportedSortField() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + IllegalArgumentException exception = assertThrows(IllegalArgumentException) { + services.routingOrderBy('productId') + } + assert exception.message == 'Unsupported sort field: productId' + } + + @Test + void testFrameworkRequestPathReturnsNullWithoutRequestBinding() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + + assert RestApiUtil.getRelativeRequestPath(services.binding) == null + } + + @Test + void testFrameworkNavigationMetadataAndLinksUseRequestPath() { + RoutingCostManagementServices services = new RoutingCostManagementServices() + MockHttpServletRequest request = new MockHttpServletRequest() + request.setRequestURI('/rest/manufacturing/routings') + request.setQueryString('pageIndex=1&pageSize=1¤tStatusId=ROU_ACTIVE') + services.binding.setVariable('request', request) + RestQueryOptions queryOptions = RestQueryOptions.fromParameters([ + pageIndex: 1, + pageSize: 1, + currentStatusId: 'ROU_ACTIVE' + ]) + + Map result = RestApiUtil.getPagedResult('routings', [], 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/routings?currentStatusId=ROU_ACTIVE&pageIndex=1&pageSize=1' + assert result.links.prev.href == '/rest/manufacturing/routings?currentStatusId=ROU_ACTIVE&pageIndex=0&pageSize=1' + assert result.links.next.href == '/rest/manufacturing/routings?currentStatusId=ROU_ACTIVE&pageIndex=2&pageSize=1' + } + +}

