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&currentStatusId=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'
+    }
+
+}

Reply via email to