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 ae0a1df6021c808d3507ce741d89fb5c4b84396b
Author: diveshdut <[email protected]>
AuthorDate: Tue Aug 4 16:55:21 2026 +0530

    OFBIZ-13480 Add routing management service APIs
    
    Add routing-focused read and helper services for list/detail payloads, 
routing task search, task purpose options, and WorkEffortGoodStandard 
expiration. The read services aggregate WorkEffort, association, cost, output, 
tool, and operator data so the Routing PWA can load management screens without 
many client-side joins.
    
    Keep mutation behavior delegated to native OFBiz services wherever 
possible. The custom services in this commit are limited to read payload 
shaping and the explicit expire wrapper needed for REST delete semantics on 
WorkEffortGoodStandard.
---
 .../manufacturing/servicedef/services_routing.xml  | 122 +++++
 .../api/RoutingCostManagementServices.groovy       | 583 +++++++++++++++++++++
 2 files changed, 705 insertions(+)

diff --git a/applications/manufacturing/servicedef/services_routing.xml 
b/applications/manufacturing/servicedef/services_routing.xml
index ef4d4f5629..19229ce6f2 100644
--- a/applications/manufacturing/servicedef/services_routing.xml
+++ b/applications/manufacturing/servicedef/services_routing.xml
@@ -68,4 +68,126 @@ 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 -->
+    <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>
+        <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"/>
+        <attribute mode="IN" name="workEffortId" optional="true" 
type="String"/>
+        <attribute mode="IN" name="workEffortName" optional="true" 
type="String"/>
+        <attribute mode="IN" name="currentStatusId" 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="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>
+        <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="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>
+        <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="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>
+        <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"/>
+        <attribute mode="OUT" name="workEffortId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="workEffortName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="description" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="quantityToProduce" optional="true" 
type="BigDecimal"/>
+        <attribute mode="OUT" name="currentStatusId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="products" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="operations" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="tasks" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="costs" optional="false" 
type="java.util.List"/>
+    </service>
+    <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>
+        <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"/>
+        <attribute mode="IN" name="fromDate" optional="false" 
type="Timestamp"/>
+        <attribute mode="OUT" name="workEffortId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="productId" optional="false" type="String"/>
+        <attribute mode="OUT" name="workEffortGoodStdTypeId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="fromDate" optional="false" 
type="Timestamp"/>
+        <attribute mode="OUT" name="thruDate" optional="false" 
type="Timestamp"/>
+        <attribute mode="OUT" name="removed" optional="false" type="Boolean"/>
+    </service>
+    <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>
+        <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"/>
+        <attribute mode="IN" name="workEffortName" optional="true" 
type="String"/>
+        <attribute mode="IN" name="fixedAssetId" 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="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>
+        <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"/>
+        <attribute mode="OUT" name="routingTaskId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="workEffortId" optional="false" 
type="String"/>
+        <attribute mode="OUT" name="workEffortName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="description" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="workEffortPurposeTypeId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="workEffortPurposeTypeDescription" 
optional="true" type="String"/>
+        <attribute mode="OUT" name="fixedAssetId" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="fixedAssetName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="workCenterName" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="estimatedSetupMillis" optional="true" 
type="Double"/>
+        <attribute mode="OUT" name="estimatedMilliSeconds" optional="true" 
type="Double"/>
+        <attribute mode="OUT" name="estimateCalcMethod" optional="true" 
type="String"/>
+        <attribute mode="OUT" name="reservPersons" optional="true" 
type="BigDecimal"/>
+        <attribute mode="OUT" name="routings" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="costRates" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="outputs" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="tools" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="operators" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="costs" optional="false" 
type="java.util.List"/>
+        <attribute mode="OUT" name="fixedAssets" optional="false" 
type="java.util.List"/>
+    </service>
 </services>
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
new file mode 100644
index 0000000000..e2e3756d25
--- /dev/null
+++ 
b/applications/manufacturing/src/main/groovy/org/apache/ofbiz/manufacturing/api/RoutingCostManagementServices.groovy
@@ -0,0 +1,583 @@
+/*
+ * 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.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.party.party.PartyHelper
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.ws.rs.util.RestApiUtil
+import org.apache.ofbiz.ws.rs.util.RestQueryOptions
+
+Map findProductRoutings() {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    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'])
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    List conditions = [EntityCondition.makeCondition('workEffortTypeId', 
'ROUTING')]
+    if (UtilValidate.isNotEmpty(filters.productId)) {
+        Set productRoutingIds = routingIdsForProducts([filters.productId] as 
Set)
+        if (!productRoutingIds) {
+            return success(RestApiUtil.getPagedResult('routings', [], 
queryOptions, 0L, null))
+        }
+        conditions.add(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, productRoutingIds as List))
+    }
+    if (UtilValidate.isNotEmpty(filters.routingId ?: filters.workEffortId)) {
+        conditions.add(EntityCondition.makeCondition('workEffortId', 
filters.routingId ?: filters.workEffortId))
+    }
+    if (UtilValidate.isNotEmpty(filters.query)) {
+        Set matchingRoutingIds = EntityUtil.searchIds(delegator, 'WorkEffort', 
'workEffortId',
+                ['workEffortId', 'workEffortName', 'description'], 
filters.query,
+                [EntityCondition.makeCondition('workEffortTypeId', 
'ROUTING')], 500)
+        Set matchingProductIds = EntityUtil.searchIds(delegator, 'Product', 
'productId',
+                ['productId', 'productName', 'internalName'], filters.query, 
500)
+        if (matchingProductIds) {
+            
matchingRoutingIds.addAll(routingIdsForProducts(matchingProductIds))
+        }
+        if (!matchingRoutingIds) {
+            return success(RestApiUtil.getPagedResult('routings', [], 
queryOptions, 0L, null))
+        }
+        conditions.add(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, matchingRoutingIds as List))
+    }
+    if (UtilValidate.isNotEmpty(filters.workEffortName)) {
+        conditions.add(EntityCondition.makeCondition('workEffortName', 
EntityOperator.LIKE, '%' + filters.workEffortName + '%'))
+    }
+    if (UtilValidate.isNotEmpty(filters.currentStatusId)) {
+        conditions.add(EntityCondition.makeCondition('currentStatusId', 
filters.currentStatusId))
+    }
+    EntityCondition whereCondition = EntityCondition.makeCondition(conditions, 
EntityOperator.AND)
+    EntityQuery query = from('WorkEffort').where(whereCondition)
+    long totalCount = query.queryCount()
+    List rows = query.orderBy(orderBy).queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+    Set routingIds = rows*.workEffortId.findAll { it } as Set
+    Map taskCounts = routingTaskCountByRouting(routingIds)
+    Map productCounts = routingProductCountByRouting(routingIds)
+    Map statuses = EntityUtil.lookupById(delegator, 'StatusItem', 'statusId',
+            rows*.currentStatusId.findAll { it } as Set)
+
+    List routingList = rows.collect { GenericValue routing ->
+        [
+                routingId: routing.workEffortId,
+                workEffortId: routing.workEffortId,
+                workEffortName: routing.workEffortName,
+                description: routing.description,
+                quantityToProduce: routing.quantityToProduce,
+                currentStatusId: routing.currentStatusId,
+                currentStatusDescription: 
statuses[routing.currentStatusId]?.description,
+                productCount: productCounts[routing.workEffortId] ?: 0,
+                taskCount: taskCounts[routing.workEffortId] ?: 0
+        ]
+    }
+    return success(RestApiUtil.getPagedResult('routings', routingList, 
queryOptions, totalCount, null))
+}
+
+Map getRoutingDetails() {
+    String routingId = parameters.routingId ?: parameters.workEffortId
+    if (!routingId) {
+        return ServiceUtil.returnError('routingId is required.')
+    }
+    GenericValue routing = EntityUtil.lookupById(delegator, 'WorkEffort', 
'workEffortId',
+            [routingId])[routingId]
+    if (!routing || routing.workEffortTypeId != 'ROUTING') {
+        return ServiceUtil.returnError('Routing not found: ' + routingId)
+    }
+    List taskAssocs = routingTaskAssocs(routingId)
+    Map tasks = EntityUtil.lookupById(delegator, 'WorkEffort', 'workEffortId',
+            taskAssocs*.workEffortIdTo.findAll { it } as Set)
+    List products = routingProductLinks(routingId)
+    Map productLookups = EntityUtil.lookupById(delegator, 'Product', 
'productId',
+            products*.productId.findAll { it } as Set, false)
+    Map workCenters = EntityUtil.lookupById(delegator, 'FixedAsset', 
'fixedAssetId',
+            tasks.values()*.fixedAssetId.findAll { it } as Set, false)
+    Map purposeTypes = EntityUtil.lookupById(delegator, 
'WorkEffortPurposeType',
+            'workEffortPurposeTypeId', 
tasks.values()*.workEffortPurposeTypeId.findAll { it } as Set)
+    List operationMaps = taskAssocs.collect { GenericValue assoc ->
+        GenericValue task = tasks[assoc.workEffortIdTo]
+        [
+                routingId: assoc.workEffortIdFrom,
+                routingTaskId: assoc.workEffortIdTo,
+                workEffortId: assoc.workEffortIdTo,
+                workEffortName: task?.workEffortName,
+                operationName: task?.workEffortName,
+                sequenceNum: assoc.sequenceNum,
+                fromDate: assoc.fromDate,
+                thruDate: assoc.thruDate,
+                workEffortPurposeTypeId: task?.workEffortPurposeTypeId,
+                workEffortPurposeTypeDescription: 
purposeTypes[task?.workEffortPurposeTypeId]?.description,
+                fixedAssetId: task?.fixedAssetId,
+                fixedAssetName: 
ManufacturingServiceUtil.displayFixedAssetName(workCenters[task?.fixedAssetId]),
+                workCenterName: 
ManufacturingServiceUtil.displayFixedAssetName(workCenters[task?.fixedAssetId]),
+                estimatedSetupMillis: task?.estimatedSetupMillis,
+                estimatedMilliSeconds: task?.estimatedMilliSeconds,
+                estimateCalcMethod: task?.estimateCalcMethod,
+                reservPersons: task?.reservPersons
+        ]
+    }
+    Map response = [
+            routingId: routing.workEffortId,
+            workEffortId: routing.workEffortId,
+            workEffortTypeId: routing.workEffortTypeId,
+            workEffortName: routing.workEffortName,
+            description: routing.description,
+            currentStatusId: routing.currentStatusId,
+            quantityToProduce: routing.quantityToProduce,
+            products: products.collect { GenericValue link ->
+                GenericValue product = productLookups[link.productId]
+                [
+                        routingId: link.workEffortId,
+                        workEffortId: link.workEffortId,
+                        productId: link.productId,
+                        productName: 
ManufacturingServiceUtil.displayProductName(product),
+                        fromDate: link.fromDate,
+                        thruDate: link.thruDate,
+                        estimatedQuantity: link.estimatedQuantity,
+                        estimatedCost: link.estimatedCost,
+                        workEffortGoodStdTypeId: link.workEffortGoodStdTypeId
+                ]
+            },
+            operations: operationMaps,
+            tasks: operationMaps
+    ]
+    response.costs = costRowsByWorkEffortId(tasks.keySet()).values().flatten() 
as List
+    return success(response)
+}
+
+Map findRoutingTasks() {
+    Map result = ServiceUtil.returnSuccess()
+    result.putAll(routingTaskList(false))
+    return result
+}
+
+Map searchRoutingTasksForRouting() {
+    Map result = ServiceUtil.returnSuccess()
+    result.putAll(routingTaskList(true))
+    return result
+}
+
+Map routingTaskList(boolean requireQuery) {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    Map filters = queryOptions.filters
+    List orderBy
+    try {
+        orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [
+                routingTaskId: 'workEffortId',
+                workEffortId: 'workEffortId',
+                workEffortName: 'workEffortName',
+                workEffortPurposeTypeId: 'workEffortPurposeTypeId',
+                fixedAssetId: 'fixedAssetId'
+        ], ['workEffortName', 'workEffortId'])
+    } 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)
+    }
+
+    List conditions = [EntityCondition.makeCondition('workEffortTypeId', 
'ROU_TASK')]
+    if (UtilValidate.isNotEmpty(queryText)) {
+        conditions.add(EntityUtil.upperLikeAny(['workEffortId', 
'workEffortName', 'description', 'workEffortPurposeTypeId'], queryText))
+    }
+    if (!requireQuery && UtilValidate.isNotEmpty(filters.workEffortId ?: 
filters.routingTaskId)) {
+        conditions.add(EntityCondition.makeCondition('workEffortId', 
filters.workEffortId ?: filters.routingTaskId))
+    }
+    if (!requireQuery && UtilValidate.isNotEmpty(filters.workEffortName)) {
+        conditions.add(EntityCondition.makeCondition('workEffortName', 
EntityOperator.LIKE, '%' + filters.workEffortName + '%'))
+    }
+    if (!requireQuery && UtilValidate.isNotEmpty(filters.fixedAssetId)) {
+        conditions.add(EntityCondition.makeCondition('fixedAssetId', 
filters.fixedAssetId))
+    }
+
+    EntityCondition whereCondition = EntityCondition.makeCondition(conditions, 
EntityOperator.AND)
+    EntityQuery query = from('WorkEffort').where(whereCondition)
+    long totalCount = query.queryCount()
+    List rows = query.orderBy(orderBy)
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+    Map workCenters = EntityUtil.lookupById(delegator, 'FixedAsset', 
'fixedAssetId',
+            rows*.fixedAssetId.findAll { it } as Set, false)
+    Map purposeTypes = EntityUtil.lookupById(delegator, 
'WorkEffortPurposeType',
+            '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)
+}
+
+Map getRoutingTaskDetails() {
+    String taskId = parameters.routingTaskId ?: parameters.taskId ?: 
parameters.workEffortId
+    if (!taskId) {
+        return ServiceUtil.returnError('routingTaskId is required.')
+    }
+    GenericValue task = EntityUtil.lookupById(delegator, 'WorkEffort', 
'workEffortId', [taskId])[taskId]
+    if (!task || task.workEffortTypeId != 'ROU_TASK') {
+        return ServiceUtil.returnError('Routing task not found: ' + taskId)
+    }
+    return success(taskDetailMap(task))
+}
+
+Map findRoutingTaskPurposeTypeOptions() {
+    RestQueryOptions queryOptions
+    try {
+        queryOptions = RestQueryOptions.fromParameters(parameters)
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    List orderBy
+    try {
+        orderBy = RestApiUtil.resolveOrderBy(queryOptions.sort, [
+                workEffortPurposeTypeId: 'workEffortPurposeTypeId',
+                description: 'description'
+        ], ['workEffortPurposeTypeId'])
+    } catch (IllegalArgumentException e) {
+        return ServiceUtil.returnError(e.message)
+    }
+    EntityQuery query = from('WorkEffortPurposeType')
+            .select('workEffortPurposeTypeId', 'description')
+            .where(EntityCondition.makeCondition('workEffortPurposeTypeId', 
EntityOperator.LIKE, 'ROU%'))
+            .cache(true)
+    long totalCount = query.queryCount()
+    List purposeTypes = query.orderBy(orderBy)
+            .queryPagedList(queryOptions.pageIndex, 
queryOptions.pageSize).getData()
+            .collect { GenericValue purposeType ->
+                String purposeTypeId = purposeType.workEffortPurposeTypeId
+                String description = purposeType.description ?: purposeTypeId
+                [
+                        workEffortPurposeTypeId: purposeTypeId,
+                        description: description,
+                        label: "${description} [${purposeTypeId}]".toString()
+                ]
+            }
+
+    return success(RestApiUtil.getPagedResult('purposeTypes', purposeTypes, 
queryOptions, totalCount, null))
+}
+
+Map taskDetailMap(GenericValue task) {
+    if (!task) {
+        return [:]
+    }
+    List usage = from('WorkEffortAssoc')
+            .where(workEffortIdTo: task.workEffortId, workEffortAssocTypeId: 
'ROUTING_COMPONENT')
+            .filterByDate()
+            .orderBy('workEffortIdFrom', 'sequenceNum')
+            .queryList()
+    List costs = 
costRowsByWorkEffortId([task.workEffortId])[task.workEffortId] ?: []
+    List outputs = 
outputRowsByWorkEffortId([task.workEffortId])[task.workEffortId] ?: []
+    List tools = 
toolRowsByWorkEffortId([task.workEffortId])[task.workEffortId] ?: []
+    List operators = 
operatorRowsByWorkEffortId([task.workEffortId])[task.workEffortId] ?: []
+    Map workCenters = EntityUtil.lookupById(delegator, 'FixedAsset', 
'fixedAssetId',
+            [task.fixedAssetId], false)
+    Map purposeTypes = EntityUtil.lookupById(delegator, 
'WorkEffortPurposeType',
+            'workEffortPurposeTypeId', [task.workEffortPurposeTypeId])
+    return taskMap(task, usage, workCenters, purposeTypes) + [
+            costRates: costs,
+            outputs: outputs,
+            tools: tools,
+            operators: operators,
+            costs: costs,
+            fixedAssets: 
fixedAssetAssignmentsByWorkEffortId([task.workEffortId])[task.workEffortId] ?: 
[]
+    ]
+}
+
+Map expireRoutingGoodStandard() {
+    Timestamp thruDate = UtilDateTime.nowTimestamp()
+    Map updateResult = runService('updateWorkEffortGoodStandard', [
+            workEffortId: parameters.workEffortId,
+            productId: parameters.productId,
+            workEffortGoodStdTypeId: parameters.workEffortGoodStdTypeId,
+            fromDate: parameters.fromDate,
+            thruDate: thruDate,
+            userLogin: userLogin
+    ])
+    if (ServiceUtil.isError(updateResult)) {
+        return updateResult
+    }
+    return success([
+            workEffortId: parameters.workEffortId,
+            productId: parameters.productId,
+            workEffortGoodStdTypeId: parameters.workEffortGoodStdTypeId,
+            fromDate: parameters.fromDate,
+            thruDate: thruDate,
+            removed: true
+    ])
+}
+
+List routingTaskAssocs(String routingId) {
+    return from('WorkEffortAssoc')
+            .where(workEffortIdFrom: routingId, workEffortAssocTypeId: 
'ROUTING_COMPONENT')
+            .filterByDate()
+            .orderBy('sequenceNum', 'workEffortIdTo')
+            .queryList()
+}
+
+List routingProductLinks(String routingId) {
+    return from('WorkEffortGoodStandard')
+            .where(workEffortId: routingId, workEffortGoodStdTypeId: 
'ROU_PROD_TEMPLATE')
+            .filterByDate()
+            .orderBy('productId', 'fromDate')
+            .queryList()
+}
+
+Map taskMap(GenericValue task, List usage, Map workCenters = [:], Map 
purposeTypes = [:]) {
+    [
+            routingTaskId: task?.workEffortId,
+            workEffortId: task?.workEffortId,
+            workEffortTypeId: task?.workEffortTypeId,
+            workEffortName: task?.workEffortName,
+            description: task?.description,
+            currentStatusId: task?.currentStatusId,
+            workEffortPurposeTypeId: task?.workEffortPurposeTypeId,
+            workEffortPurposeTypeDescription: 
purposeTypes[task?.workEffortPurposeTypeId]?.description,
+            fixedAssetId: task?.fixedAssetId,
+            fixedAssetName: 
ManufacturingServiceUtil.displayFixedAssetName(workCenters[task?.fixedAssetId]),
+            workCenterName: 
ManufacturingServiceUtil.displayFixedAssetName(workCenters[task?.fixedAssetId]),
+            estimatedMilliSeconds: task?.estimatedMilliSeconds,
+            estimatedSetupMillis: task?.estimatedSetupMillis,
+            estimateCalcMethod: task?.estimateCalcMethod,
+            reservPersons: task?.reservPersons,
+            routings: (usage ?: []).collect { GenericValue assoc ->
+                [routingId: assoc.workEffortIdFrom, workEffortIdFrom: 
assoc.workEffortIdFrom,
+                        sequenceNum: assoc.sequenceNum, fromDate: 
assoc.fromDate, thruDate: assoc.thruDate]
+            }
+    ]
+}
+
+String costRateStatus(Timestamp fromDate, Timestamp thruDate) {
+    Timestamp now = UtilDateTime.nowTimestamp()
+    if (thruDate && thruDate.before(now)) {
+        return 'EXPIRED'
+    }
+    return fromDate && fromDate.after(now) ? 'SCHEDULED' : 'ACTIVE'
+}
+
+Map routingTaskCountByRouting(Collection routingIds) {
+    if (!routingIds) {
+        return [:]
+    }
+    List assocs = from('WorkEffortAssoc')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('workEffortIdFrom', 
EntityOperator.IN, routingIds as List),
+                    EntityCondition.makeCondition('workEffortAssocTypeId', 
'ROUTING_COMPONENT')
+            ], EntityOperator.AND))
+            .filterByDate()
+            .queryList()
+    return assocs.groupBy { it.workEffortIdFrom }.collectEntries { String id, 
List rows -> [(id): rows.size()] }
+}
+
+Map routingProductCountByRouting(Collection routingIds) {
+    if (!routingIds) {
+        return [:]
+    }
+    List links = from('WorkEffortGoodStandard')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, routingIds as List),
+                    EntityCondition.makeCondition('workEffortGoodStdTypeId', 
'ROU_PROD_TEMPLATE')
+            ], EntityOperator.AND))
+            .filterByDate()
+            .queryList()
+    return links.groupBy { it.workEffortId }.collectEntries { String id, List 
rows -> [(id): rows.size()] }
+}
+
+Set routingIdsForProducts(Collection productIds) {
+    if (!productIds) {
+        return [] as Set
+    }
+    return from('WorkEffortGoodStandard')
+            .select('workEffortId')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('productId', 
EntityOperator.IN, productIds as List),
+                    EntityCondition.makeCondition('workEffortGoodStdTypeId', 
'ROU_PROD_TEMPLATE')
+            ], EntityOperator.AND))
+            .filterByDate()
+            .maxRows(500)
+            .getFieldList('workEffortId') as Set
+}
+
+Map costRowsByWorkEffortId(Collection workEffortIds) {
+    if (!workEffortIds) {
+        return [:]
+    }
+    List costRows = from('WorkEffortCostCalc')
+            .where(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, workEffortIds as List))
+            .filterByDate()
+            .orderBy('workEffortId', 'costComponentTypeId')
+            .queryList()
+    Map calcs = EntityUtil.lookupById(delegator, 'CostComponentCalc', 
'costComponentCalcId',
+            costRows*.costComponentCalcId.findAll { it } as Set)
+    Map costTypes = EntityUtil.lookupById(delegator, 'CostComponentType', 
'costComponentTypeId',
+            costRows*.costComponentTypeId.findAll { it } as Set)
+    return groupedRowsByWorkEffortId(costRows) { GenericValue row, String 
ignoredWorkEffortId ->
+        GenericValue calc = calcs[row.costComponentCalcId]
+        GenericValue costType = costTypes[row.costComponentTypeId]
+        [
+                routingTaskId: row.workEffortId,
+                workEffortId: row.workEffortId,
+                costComponentTypeId: row.costComponentTypeId,
+                costCategory: costType?.description,
+                costComponentCalcId: row.costComponentCalcId,
+                calculationRule: calc?.description,
+                setupCost: calc?.fixedCost,
+                runRate: calc?.variableCost,
+                runRateUom: calc?.currencyUomId,
+                fromDate: row.fromDate,
+                thruDate: row.thruDate,
+                status: costRateStatus(row.getTimestamp('fromDate'), 
row.getTimestamp('thruDate'))
+        ]
+    }
+}
+
+Map fixedAssetAssignmentsByWorkEffortId(Collection workEffortIds) {
+    if (!workEffortIds) {
+        return [:]
+    }
+    return groupedRowsByWorkEffortId(from('WorkEffortFixedAssetAssign')
+            .where(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, workEffortIds as List))
+            .filterByDate()
+            .orderBy('workEffortId', 'fixedAssetId')
+            .queryList()) { GenericValue row, String ignoredWorkEffortId ->
+        [
+                fixedAssetId: row.fixedAssetId,
+                fromDate: row.fromDate,
+                thruDate: row.thruDate,
+                statusId: row.statusId,
+                availabilityStatusId: row.availabilityStatusId,
+                allocatedCost: row.allocatedCost
+        ]
+    }
+}
+
+Map outputRowsByWorkEffortId(Collection workEffortIds) {
+    if (!workEffortIds) {
+        return [:]
+    }
+    List outputs = from('WorkEffortGoodStandard')
+            .where(EntityCondition.makeCondition([
+                    EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, workEffortIds as List),
+                    EntityCondition.makeCondition('workEffortGoodStdTypeId', 
'PRUNT_PROD_DELIV')
+            ], EntityOperator.AND))
+            .filterByDate()
+            .orderBy('workEffortId', 'productId', 'fromDate')
+            .queryList()
+    Map products = EntityUtil.lookupById(delegator, 'Product', 'productId',
+            outputs*.productId.findAll { it } as Set, false)
+    return groupedRowsByWorkEffortId(outputs) { GenericValue row, String 
ignoredWorkEffortId ->
+        GenericValue product = products[row.productId]
+        [
+                routingTaskId: row.workEffortId,
+                workEffortId: row.workEffortId,
+                productId: row.productId,
+                productName: 
ManufacturingServiceUtil.displayProductName(product),
+                estimatedQuantity: row.estimatedQuantity,
+                quantityUomId: product?.quantityUomId,
+                estimatedCost: row.estimatedCost,
+                fromDate: row.fromDate,
+                thruDate: row.thruDate,
+                workEffortGoodStdTypeId: row.workEffortGoodStdTypeId
+        ]
+    }
+}
+
+Map toolRowsByWorkEffortId(Collection workEffortIds) {
+    if (!workEffortIds) {
+        return [:]
+    }
+    List tools = from('WorkEffortFixedAssetStd')
+            .where(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, workEffortIds as List))
+            .orderBy('workEffortId', 'fixedAssetTypeId')
+            .queryList()
+    Map fixedAssetTypes = EntityUtil.lookupById(delegator, 'FixedAssetType', 
'fixedAssetTypeId',
+            tools*.fixedAssetTypeId.findAll { it } as Set)
+    Map tasks = EntityUtil.lookupById(delegator, 'WorkEffort', 'workEffortId',
+            tools*.workEffortId.findAll { it } as Set)
+    return groupedRowsByWorkEffortId(tools) { GenericValue row, String 
workEffortId ->
+        GenericValue fixedAssetType = fixedAssetTypes[row.fixedAssetTypeId]
+        GenericValue task = tasks[workEffortId]
+        [
+                fixedAssetTypeId: row.fixedAssetTypeId,
+                fixedAssetId: task?.fixedAssetId,
+                toolName: fixedAssetType?.description,
+                estimatedQuantity: row.estimatedQuantity,
+                estimatedDuration: row.estimatedDuration,
+                estimatedCost: row.estimatedCost,
+                fromDate: null,
+                thruDate: null
+        ]
+    }
+}
+
+Map operatorRowsByWorkEffortId(Collection workEffortIds) {
+    if (!workEffortIds) {
+        return [:]
+    }
+    List operators = from('WorkEffortPartyAssignment')
+            .where(EntityCondition.makeCondition('workEffortId', 
EntityOperator.IN, workEffortIds as List))
+            .filterByDate()
+            .orderBy('workEffortId', 'partyId', 'roleTypeId')
+            .queryList()
+    Map partyNames = EntityUtil.lookupById(delegator, 'PartyNameView', 
'partyId',
+            operators*.partyId.findAll { it } as Set, false)
+    Map roleTypes = EntityUtil.lookupById(delegator, 'RoleType', 'roleTypeId',
+            operators*.roleTypeId.findAll { it } as Set)
+    return groupedRowsByWorkEffortId(operators) { GenericValue row, String 
ignoredWorkEffortId ->
+        GenericValue partyName = partyNames[row.partyId]
+        GenericValue roleType = roleTypes[row.roleTypeId]
+        [
+                partyId: row.partyId,
+                operatorName: PartyHelper.getPartyName(partyName, false) ?: 
row.partyId,
+                roleTypeId: row.roleTypeId,
+                roleTypeDescription: roleType?.description ?: row.roleTypeId,
+                statusId: row.statusId,
+                comments: row.comments,
+                fromDate: row.fromDate,
+                thruDate: row.thruDate
+        ]
+    }
+}
+
+Map groupedRowsByWorkEffortId(List rows, Closure rowMapper) {
+    return rows.groupBy { it.workEffortId }.collectEntries { String 
workEffortId, List groupedRows ->
+        [(workEffortId): groupedRows.collect { GenericValue row -> 
rowMapper(row, workEffortId) }]
+    }
+}

Reply via email to