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 d34901e830085b6ac64d9e23c905c9391cd5ee78
Author: diveshdut <[email protected]>
AuthorDate: Fri Aug 21 12:20:25 2026 +0530

    OFBIZ-13460 Add MRP run lifecycle logging
    
    Add durable run lifecycle logging for MRP execution so queued, running, 
finished, and failed runs can be tracked independently from the transient event 
rows produced by the engine.
    
    Update services_mrp.xml with the run-log service contracts. launchMrpRun 
queues executeMrp as persisted async work, 
createMrpRunLogAndTrackerBeforeQueueingMrp creates the durable MrpRunLog and 
JobTracker records before queueing, and the update services keep the run 
status, timing, job id, error text, and generated mrpId current as the run 
progresses.
    
    Update MrpServices.java with the core lifecycle behavior used by both API 
callers and Manufacturing screens. The launch path creates the run log before 
queueing executeMrp, records queue failures when executeMrp never starts, 
attaches the JobSandbox job id when queueing succeeds, and lets executeMrp mark 
MrpRunLog as SERVICE_RUNNING, SERVICE_FINISHED, or SERVICE_FAILED.
    
    Update the Manufacturing run-MRP controller action to call launchMrpRun 
instead of directly queueing executeMrp, so the standard screen and REST/API 
flows use the same lifecycle handling.
    
    Add ManufacturingUiLabels entries for the run-log outcome and queue-failure 
messages shown by the lifecycle services.
---
 .../manufacturing/config/ManufacturingUiLabels.xml |  12 +
 .../manufacturing/servicedef/services_mrp.xml      |  43 +++
 .../ofbiz/manufacturing/mrp/MrpServices.java       | 380 ++++++++++++++++++---
 .../webapp/manufacturing/WEB-INF/controller.xml    |   2 +-
 4 files changed, 394 insertions(+), 43 deletions(-)

diff --git a/applications/manufacturing/config/ManufacturingUiLabels.xml 
b/applications/manufacturing/config/ManufacturingUiLabels.xml
index 8a87582410..a2fc697327 100644
--- a/applications/manufacturing/config/ManufacturingUiLabels.xml
+++ b/applications/manufacturing/config/ManufacturingUiLabels.xml
@@ -3131,6 +3131,9 @@
         <value xml:lang="zh">上一个工作</value>
         <value xml:lang="zh-TW">上一個工作</value>
     </property>
+    <property key="ManufacturingMrpJobQueueError">
+        <value xml:lang="en">MRP job could not be queued: 
${errorString}</value>
+    </property>
     <property key="ManufacturingMrpJobScheduledOrRunning">
         <value xml:lang="de">Geplanter oder aktiver MRP-Lauf</value>
         <value xml:lang="en">Scheduled or running Jobs</value>
@@ -3171,6 +3174,15 @@
         <value xml:lang="zh">生产资源计划名称</value>
         <value xml:lang="zh-TW">生產資源計劃名稱</value>
     </property>
+    <property key="ManufacturingMrpRunDefaultName">
+        <value xml:lang="en">MRP run</value>
+    </property>
+    <property key="ManufacturingMrpRunLogAndTrackerCreateError">
+        <value xml:lang="en">MRP run log and tracker could not be created: 
${errorString}</value>
+    </property>
+    <property key="ManufacturingMrpRunOutcomeSummary">
+        <value xml:lang="en">Created ${mrpEventCount} MRP events and 
${proposedRequirementCount} proposed requirements</value>
+    </property>
     <property key="ManufacturingMrpRunScheduledSuccessfully">
         <value xml:lang="en">Mrp run scheduled successfully</value>
         <value xml:lang="fr">Exécution de Mrp programmée avec succès</value>
diff --git a/applications/manufacturing/servicedef/services_mrp.xml 
b/applications/manufacturing/servicedef/services_mrp.xml
index e8e3251f61..ec0be0f5ae 100644
--- a/applications/manufacturing/servicedef/services_mrp.xml
+++ b/applications/manufacturing/servicedef/services_mrp.xml
@@ -29,6 +29,7 @@ under the License.
             location="org.apache.ofbiz.manufacturing.mrp.MrpServices" 
invoke="executeMrp" auth="true"
             transaction-timeout="7200" max-retry="0">
         <description>Performs a run of Mrp</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="CREATE"/>
         <attribute name="facilityGroupId" type="String" mode="IN" 
optional="true"/>
         <attribute name="facilityId" type="String" mode="IN" optional="true"/>
         <attribute name="mrpName" type="String" mode="IN" optional="true"/>
@@ -47,6 +48,48 @@ under the License.
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
     </service>
+    <service name="createMrpRunLogAndTrackerBeforeQueueingMrp" engine="java"
+            location="org.apache.ofbiz.manufacturing.mrp.MrpServices" 
invoke="createMrpRunLogAndTrackerBeforeQueueingMrp" auth="true"
+            require-new-transaction="true">
+        <description>Create the durable MrpRunLog, RuntimeData, and JobTracker 
records before launchMrpRun queues
+            MRP through executeMrp. Runs in a new transaction so the persisted 
async executeMrp job can reference committed tracking records.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="CREATE"/>
+        <attribute name="mrpRunLogId" type="String" mode="INOUT" 
optional="true"/>
+        <attribute name="jobTrackerId" type="String" mode="IN" 
optional="true"/>
+        <attribute name="mrpName" type="String" mode="IN" optional="true"/>
+        <attribute name="facilityId" type="String" mode="IN" optional="true"/>
+        <attribute name="facilityGroupId" type="String" mode="IN" 
optional="true"/>
+        <attribute name="defaultYearsOffset" type="Integer" mode="IN" 
optional="true"/>
+        <attribute name="runStatusId" type="String" mode="IN" optional="true"/>
+        <attribute name="startedAt" type="Timestamp" mode="IN" 
optional="true"/>
+    </service>
+    <service name="launchMrpRun" engine="java"
+            location="org.apache.ofbiz.manufacturing.mrp.MrpServices" 
invoke="launchMrpRun" auth="true">
+        <description>Launch an async MRP run using generic MRP infrastructure. 
This service owns queued JobSandbox
+            attachment and queue-failure logging so manufacturing screens and 
API clients share the same durable run tracking behavior.</description>
+        <permission-service service-name="manufacturingPermissionService" 
main-action="CREATE"/>
+        <attribute name="facilityGroupId" type="String" mode="IN" 
optional="true"/>
+        <attribute name="facilityId" type="String" mode="IN" optional="true"/>
+        <attribute name="mrpName" type="String" mode="IN" optional="true"/>
+        <attribute name="defaultYearsOffset" type="Integer" mode="IN" 
optional="true"/>
+        <attribute name="runId" type="String" mode="OUT" optional="false"/>
+        <attribute name="mrpRunLogId" type="String" mode="OUT" 
optional="false"/>
+        <attribute name="jobId" type="String" mode="OUT" optional="true"/>
+        <attribute name="mrpId" type="String" mode="OUT" optional="true"/>
+        <attribute name="mrpName" type="String" mode="OUT" optional="true"/>
+        <attribute name="facilityGroupId" type="String" mode="OUT" 
optional="true"/>
+        <attribute name="facilityId" type="String" mode="OUT" optional="true"/>
+        <attribute name="defaultYearsOffset" type="Integer" mode="OUT" 
optional="true"/>
+        <attribute name="statusId" type="String" mode="OUT" optional="true"/>
+        <attribute name="statusDescription" type="String" mode="OUT" 
optional="true"/>
+        <attribute name="failureReason" type="String" mode="OUT" 
optional="true"/>
+        <attribute name="failureMessage" type="String" mode="OUT" 
optional="true"/>
+        <attribute name="runTime" type="String" mode="OUT" optional="true"/>
+        <attribute name="durationMillis" type="Long" mode="OUT" 
optional="true"/>
+        <attribute name="startDateTime" type="Timestamp" mode="OUT" 
optional="true"/>
+        <attribute name="finishDateTime" type="Timestamp" mode="OUT" 
optional="true"/>
+        <attribute name="runAsUser" type="String" mode="OUT" optional="true"/>
+    </service>
     <service name="initMrpEvents" engine="java"
             location="org.apache.ofbiz.manufacturing.mrp.MrpServices" 
invoke="initMrpEvents" auth="true">
         <description>Initialize data for the MRP</description>
diff --git 
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
 
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
index 1ccec0126c..cac5ed6fd7 100644
--- 
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
+++ 
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
@@ -19,8 +19,10 @@
 
 package org.apache.ofbiz.manufacturing.mrp;
 
+import java.io.IOException;
 import java.math.BigDecimal;
 import java.sql.Timestamp;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -29,6 +31,7 @@ import java.util.ListIterator;
 import java.util.Locale;
 import java.util.Map;
 
+import org.apache.ofbiz.base.util.DateRange;
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilDateTime;
 import org.apache.ofbiz.base.util.UtilGenerics;
@@ -40,6 +43,8 @@ import org.apache.ofbiz.entity.GenericEntityException;
 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.serialize.SerializeException;
+import org.apache.ofbiz.entity.serialize.XmlSerializer;
 import org.apache.ofbiz.entity.util.EntityQuery;
 import org.apache.ofbiz.entity.util.EntityUtil;
 import org.apache.ofbiz.manufacturing.bom.BOMNode;
@@ -675,7 +680,7 @@ public class MrpServices {
         }
     }
 
-    private static String mrpRunLogIdFromContext(Delegator delegator, 
Map<String, ? extends Object> context) {
+    private static String getMrpRunLogIdFromContext(Delegator delegator, 
Map<String, ? extends Object> context) {
         String mrpRunLogId = (String) context.get("mrpRunLogId");
         if (UtilValidate.isNotEmpty(mrpRunLogId)) {
             return mrpRunLogId;
@@ -684,6 +689,271 @@ public class MrpServices {
         return UtilValidate.isNotEmpty(mrpRunLogId) ? mrpRunLogId : 
delegator.getNextSeqId("MrpRunLog");
     }
 
+    /**
+     * Create queue-time run log and tracker metadata before queueing MRP 
through executeMrp.
+     */
+    public static Map<String, Object> 
createMrpRunLogAndTrackerBeforeQueueingMrp(DispatchContext ctx, Map<String, ? 
extends Object> context) {
+        Delegator delegator = ctx.getDelegator();
+        LocalDispatcher dispatcher = ctx.getDispatcher();
+        GenericValue userLogin = (GenericValue) context.get("userLogin");
+        Locale locale = (Locale) context.get("locale");
+        Timestamp startedAt = (Timestamp) context.get("startedAt");
+        if (startedAt == null) {
+            startedAt = UtilDateTime.nowTimestamp();
+        }
+        String mrpRunLogId = getMrpRunLogIdFromContext(delegator, context);
+        Map<String, Object> createMrpRunLogContext = new HashMap<>();
+        createMrpRunLogContext.put("mrpRunLogId", mrpRunLogId);
+        createMrpRunLogContext.put("mrpName", UtilValidate.isNotEmpty((String) 
context.get("mrpName")) ? context.get("mrpName")
+                : UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpRunDefaultName", locale));
+        if (UtilValidate.isNotEmpty((String) context.get("facilityId"))) {
+            createMrpRunLogContext.put("facilityId", 
context.get("facilityId"));
+        }
+        if (UtilValidate.isNotEmpty((String) context.get("facilityGroupId"))) {
+            createMrpRunLogContext.put("facilityGroupId", 
context.get("facilityGroupId"));
+        }
+        if (context.get("defaultYearsOffset") != null) {
+            createMrpRunLogContext.put("defaultYearsOffset", 
context.get("defaultYearsOffset"));
+        }
+        if (userLogin != null && 
UtilValidate.isNotEmpty(userLogin.getString("userLoginId"))) {
+            createMrpRunLogContext.put("runByUserLoginId", 
userLogin.getString("userLoginId"));
+        }
+        createMrpRunLogContext.put("runStatusId",
+                UtilValidate.isNotEmpty((String) context.get("runStatusId")) ? 
context.get("runStatusId") : "SERVICE_PENDING");
+        createMrpRunLogContext.put("startedAt", startedAt);
+        createMrpRunLogContext.put("userLogin", userLogin);
+        try {
+            createMrpRunLogContext = ctx.makeValidContext("createMrpRunLog", 
ModelService.IN_PARAM, createMrpRunLogContext);
+            Map<String, Object> response = 
dispatcher.runSync("createMrpRunLog", createMrpRunLogContext);
+            if (ServiceUtil.isError(response)) {
+                return 
ServiceUtil.returnError(ServiceUtil.getErrorMessage(response));
+            }
+            String jobTrackerId = (String) context.get("jobTrackerId");
+            if (mrpRunLogId.equals(jobTrackerId)) {
+                GenericValue existingJobTracker = 
EntityQuery.use(delegator).from("JobTracker").where("jobTrackerId", 
mrpRunLogId).queryOne();
+                if (existingJobTracker == null) {
+                    Map<String, Object> trackedParameters = new 
HashMap<>(context);
+                    trackedParameters.remove("timeZone");
+                    String runtimeDataId = 
delegator.getNextSeqId("RuntimeData");
+                    delegator.create("RuntimeData", 
UtilMisc.toMap("runtimeDataId", runtimeDataId,
+                            "runtimeInfo", 
XmlSerializer.serialize(trackedParameters)));
+
+                    String userLoginId = userLogin != null ? 
userLogin.getString("userLoginId") : null;
+                    Map<String, Object> createJobTrackerContext = new 
HashMap<>();
+                    createJobTrackerContext.put("jobTrackerId", mrpRunLogId);
+                    createJobTrackerContext.put("serviceName", "executeMrp");
+                    createJobTrackerContext.put("runtimeDataId", 
runtimeDataId);
+                    createJobTrackerContext.put("statusId", "JOB_T_SCHEDULED");
+                    createJobTrackerContext.put("jobsTotalQty", 1L);
+                    createJobTrackerContext.put("persistResult", "Y");
+                    if (UtilValidate.isNotEmpty(userLoginId)) {
+                        createJobTrackerContext.put("runAsUser", userLoginId);
+                    }
+                    createJobTrackerContext.put("startDate", startedAt);
+                    createJobTrackerContext.put("createdDate", startedAt);
+                    if (UtilValidate.isNotEmpty(userLoginId)) {
+                        createJobTrackerContext.put("createdByUserLogin", 
userLoginId);
+                    }
+                    createJobTrackerContext.put("lastModifiedDate", startedAt);
+                    if (UtilValidate.isNotEmpty(userLoginId)) {
+                        createJobTrackerContext.put("lastModifiedByUserLogin", 
userLoginId);
+                    }
+                    createJobTrackerContext.put("userLogin", userLogin);
+                    createJobTrackerContext = 
ctx.makeValidContext("createJobTracker", ModelService.IN_PARAM, 
createJobTrackerContext);
+                    response = dispatcher.runSync("createJobTracker", 
createJobTrackerContext);
+                    if (ServiceUtil.isError(response)) {
+                        return 
ServiceUtil.returnError(ServiceUtil.getErrorMessage(response));
+                    }
+                }
+            }
+        } catch (GenericEntityException | GenericServiceException e) {
+            Debug.logError(e, "Unable to create or refresh MrpRunLog [" + 
mrpRunLogId + "]", MODULE);
+            return ServiceUtil.returnError(e.getMessage());
+        } catch (SerializeException | IOException e) {
+            Debug.logError(e, "Unable to prepare JobTracker for MrpRunLog [" + 
mrpRunLogId + "]", MODULE);
+            return ServiceUtil.returnError(e.getMessage());
+        }
+        Map<String, Object> result = ServiceUtil.returnSuccess();
+        result.put("mrpRunLogId", mrpRunLogId);
+        return result;
+    }
+
+    /**
+     * Launch executeMrp through generic MRP infrastructure so API and direct 
callers share durable run tracking.
+     *
+     * MrpRunLog is the execution history record for the MRP run. It stays 
available even if later MRP events
+     * are re-created or deleted, and it keeps the JobSandbox jobId that shows 
which persisted async job did
+     * the work. This method starts the lifecycle and executeMrp completes it:
+     * 1. Create MrpRunLog as SERVICE_PENDING, plus RuntimeData and JobTracker 
for the queued executeMrp job.
+     * 2. Queue executeMrp as persisted async work.
+     * 3. When executeMrp starts, startMrpRunLog marks MrpRunLog as 
SERVICE_RUNNING.
+     * 4. When executeMrp ends, finishMrpRunLog marks MrpRunLog as 
SERVICE_FINISHED or SERVICE_FAILED.
+     * 5. If queueing fails and executeMrp never runs, mark MrpRunLog as 
SERVICE_FAILED.
+     * 6. If queueing succeeds, attach JobSandbox.jobId to MrpRunLog so the 
run history identifies the job.
+     */
+    public static Map<String, Object> launchMrpRun(DispatchContext ctx, 
Map<String, ? extends Object> context) {
+        Delegator delegator = ctx.getDelegator();
+        LocalDispatcher dispatcher = ctx.getDispatcher();
+        GenericValue userLogin = (GenericValue) context.get("userLogin");
+        Locale locale = (Locale) context.get("locale");
+        String facilityId = (String) context.get("facilityId");
+        String facilityGroupId = (String) context.get("facilityGroupId");
+        if (UtilValidate.isEmpty(facilityId) && 
UtilValidate.isEmpty(facilityGroupId)) {
+            return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpFacilityNotAvailable", locale));
+        }
+        Timestamp queuedAfter = UtilDateTime.nowTimestamp();
+        String mrpRunLogId = delegator.getNextSeqId("MrpRunLog");
+        Map<String, Object> executeMrpContext = new HashMap<>(context);
+        executeMrpContext.put("mrpRunLogId", mrpRunLogId);
+        executeMrpContext.put("jobTrackerId", mrpRunLogId);
+        executeMrpContext.put("userLogin", userLogin);
+
+        try {
+            executeMrpContext = ctx.makeValidContext("executeMrp", 
ModelService.IN_PARAM, executeMrpContext);
+            // Step 1: create durable pending run metadata before persisted 
async queueing can reference it.
+            /*
+             * createMrpRunLogAndTrackerBeforeQueueingMrp creates the durable 
MrpRunLog row and, when jobTrackerId matches,
+             * prepares RuntimeData and JobTracker for the upcoming executeMrp 
job. It requires a new transaction
+             * because the next step calls dispatcher.runAsync("executeMrp", 
executeMrpContext, true): persisted async
+             * execution creates a JobSandbox row and may start executeMrp 
before launchMrpRun commits.
+             * Without createMrpRunLogAndTrackerBeforeQueueingMrp's 
new-transaction boundary, executeMrp can race
+             * ahead of the metadata rows it needs for durable run tracking.
+             */
+            Map<String, Object> 
createMrpRunLogAndTrackerBeforeQueueingMrpContext =
+                    
ctx.makeValidContext("createMrpRunLogAndTrackerBeforeQueueingMrp", 
ModelService.IN_PARAM, executeMrpContext);
+            Map<String, Object> createMrpRunLogAndTrackerResponse = 
dispatcher.runSync("createMrpRunLogAndTrackerBeforeQueueingMrp",
+                    createMrpRunLogAndTrackerBeforeQueueingMrpContext, 60, 
true);
+            if (ServiceUtil.isError(createMrpRunLogAndTrackerResponse)) {
+                return 
ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpRunLogAndTrackerCreateError",
+                        UtilMisc.toMap("errorString", 
ServiceUtil.getErrorMessage(createMrpRunLogAndTrackerResponse)), locale));
+            }
+        } catch (GenericServiceException e) {
+            Debug.logError(e, "Unable to create MrpRunLog and tracker before 
queueing executeMrp", MODULE);
+            return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpRunLogAndTrackerCreateError",
+                    UtilMisc.toMap("errorString", e.getMessage()), locale));
+        }
+
+        try {
+            // Step 2: queue executeMrp as a persisted async job so the 
long-running MRP work can be tracked.
+            dispatcher.runAsync("executeMrp", executeMrpContext, true);
+            // Steps 3 and 4 happen inside executeMrp when the queued job 
starts and finishes.
+        } catch (GenericServiceException e) {
+            Timestamp failedAt = UtilDateTime.nowTimestamp();
+            Map<String, Object> updateMrpRunLogContext = 
UtilMisc.toMap("mrpRunLogId", mrpRunLogId, "runStatusId", "SERVICE_FAILED",
+                    "finishedAt", failedAt, "failureReason", "QUEUE_ERROR", 
"failureMessage", e.getMessage(), "userLogin", userLogin);
+            try {
+                GenericValue runLog = 
EntityQuery.use(delegator).from("MrpRunLog").where("mrpRunLogId", 
mrpRunLogId).queryOne();
+                if (runLog != null && runLog.getTimestamp("startedAt") != 
null) {
+                    updateMrpRunLogContext.put("durationMillis", new 
DateRange(runLog.getTimestamp("startedAt"), failedAt).durationInMillis());
+                }
+                // Step 5: record SERVICE_FAILED when executeMrp never starts 
because async queueing failed.
+                /*
+                 * updateMrpRunLog uses a new transaction to mark the queued 
MrpRunLog as failed because
+                 * dispatcher.runAsync("executeMrp", ...) failed and 
executeMrp will not run at all. Because
+                 * finishMrpRunLog only runs inside executeMrp, this update 
records the queue failure before
+                 * launchMrpRun returns an error.
+                 */
+                updateMrpRunLogContext = 
ctx.makeValidContext("updateMrpRunLog", ModelService.IN_PARAM, 
updateMrpRunLogContext);
+                Map<String, Object> updateMrpRunLogResponse = 
dispatcher.runSync("updateMrpRunLog", updateMrpRunLogContext, 60, true);
+                if (ServiceUtil.isError(updateMrpRunLogResponse)) {
+                    Debug.logWarning("Unable to mark queued MrpRunLog [" + 
mrpRunLogId + "] failed: "
+                            + 
ServiceUtil.getErrorMessage(updateMrpRunLogResponse), MODULE);
+                }
+            } catch (GenericEntityException | GenericServiceException 
updateMrpRunLogException) {
+                Debug.logWarning(updateMrpRunLogException,
+                        "Unable to call updateMrpRunLog to mark queued run 
failed for [" + mrpRunLogId + "]", MODULE);
+            }
+            return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpJobQueueError",
+                    UtilMisc.toMap("errorString", e.getMessage()), locale));
+        }
+
+        // Step 6: find the persisted async executeMrp JobSandbox row so 
MrpRunLog records which job did the work.
+        GenericValue queuedJob = findLaunchedMrpJob(delegator, queuedAfter, 
mrpRunLogId, userLogin);
+        if (queuedJob != null && 
UtilValidate.isNotEmpty(queuedJob.getString("jobId"))) {
+            Map<String, Object> updateMrpRunLogContext = 
UtilMisc.toMap("mrpRunLogId", mrpRunLogId, "jobId", 
queuedJob.getString("jobId"),
+                    "userLogin", userLogin);
+            try {
+                /*
+                 * updateMrpRunLog uses a new transaction so the JobSandbox 
link is retained even if
+                 * launchMrpRun later fails while reading status or shaping 
the response.
+                 */
+                updateMrpRunLogContext = 
ctx.makeValidContext("updateMrpRunLog", ModelService.IN_PARAM, 
updateMrpRunLogContext);
+                Map<String, Object> updateMrpRunLogResponse = 
dispatcher.runSync("updateMrpRunLog", updateMrpRunLogContext, 60, true);
+                if (ServiceUtil.isError(updateMrpRunLogResponse)) {
+                    Debug.logWarning("Unable to attach JobSandbox [" + 
queuedJob.getString("jobId") + "] to MrpRunLog [" + mrpRunLogId + "]: "
+                            + 
ServiceUtil.getErrorMessage(updateMrpRunLogResponse), MODULE);
+                }
+            } catch (GenericServiceException updateMrpRunLogException) {
+                Debug.logWarning(updateMrpRunLogException, "Unable to call 
updateMrpRunLog to attach job for [" + mrpRunLogId + "]", MODULE);
+            }
+        }
+        // Return the durable run state that was just queued. Steps 3 and 4 
happen later inside executeMrp.
+        Map<String, Object> result = ServiceUtil.returnSuccess();
+        GenericValue runLog = null;
+        try {
+            runLog = 
EntityQuery.use(delegator).from("MrpRunLog").where("mrpRunLogId", 
mrpRunLogId).queryOne();
+        } catch (GenericEntityException e) {
+            Debug.logWarning(e, "Unable to read MrpRunLog [" + mrpRunLogId + 
"] after queueing executeMrp", MODULE);
+        }
+        GenericValue status = null;
+        try {
+            if (runLog != null && 
UtilValidate.isNotEmpty(runLog.getString("runStatusId"))) {
+                status = 
EntityQuery.use(delegator).from("StatusItem").where("statusId", 
runLog.getString("runStatusId")).cache(true).queryOne();
+            }
+        } catch (GenericEntityException e) {
+            Debug.logWarning(e, "Unable to read status for MrpRunLog [" + 
mrpRunLogId + "]", MODULE);
+        }
+        Map<String, Object> resultFields = new HashMap<>();
+        resultFields.put("runId", mrpRunLogId);
+        resultFields.put("mrpRunLogId", mrpRunLogId);
+        resultFields.put("jobId", runLog != null ? runLog.getString("jobId") : 
null);
+        resultFields.put("mrpId", runLog != null ? runLog.getString("mrpId") : 
null);
+        resultFields.put("mrpName", runLog != null ? 
runLog.getString("mrpName") : context.get("mrpName"));
+        resultFields.put("facilityGroupId", runLog != null ? 
runLog.getString("facilityGroupId") : context.get("facilityGroupId"));
+        resultFields.put("facilityId", runLog != null ? 
runLog.getString("facilityId") : context.get("facilityId"));
+        resultFields.put("defaultYearsOffset", runLog != null && 
runLog.getLong("defaultYearsOffset") != null
+                ? runLog.getLong("defaultYearsOffset").intValue() : 
context.get("defaultYearsOffset"));
+        resultFields.put("statusId", runLog != null ? 
runLog.getString("runStatusId") : "SERVICE_PENDING");
+        resultFields.put("statusDescription", status != null ? 
status.getString("description") : null);
+        resultFields.put("failureReason", runLog != null ? 
runLog.getString("failureReason") : null);
+        resultFields.put("failureMessage", runLog != null ? 
runLog.getString("failureMessage") : null);
+        Timestamp startedAt = runLog != null ? 
runLog.getTimestamp("startedAt") : null;
+        Timestamp finishedAt = runLog != null ? 
runLog.getTimestamp("finishedAt") : null;
+        Long durationMillis = runLog != null ? 
runLog.getLong("durationMillis") : null;
+        if (durationMillis == null && startedAt != null && finishedAt != null) 
{
+            durationMillis = new DateRange(startedAt, 
finishedAt).durationInMillis();
+        }
+        resultFields.put("runTime", durationMillis != null ? 
Duration.ofMillis(durationMillis).toString() : null);
+        resultFields.put("durationMillis", durationMillis);
+        resultFields.put("startDateTime", startedAt);
+        resultFields.put("finishDateTime", finishedAt);
+        resultFields.put("runAsUser", runLog != null ? 
runLog.getString("runByUserLoginId") : null);
+        result.putAll(resultFields);
+        return result;
+    }
+
+    private static GenericValue findLaunchedMrpJob(Delegator delegator, 
Timestamp queuedAfter, String jobTrackerId, GenericValue userLogin) {
+        List<EntityCondition> conditions = new ArrayList<>();
+        conditions.add(EntityCondition.makeCondition("serviceName", 
"executeMrp"));
+        if (UtilValidate.isNotEmpty(jobTrackerId)) {
+            conditions.add(EntityCondition.makeCondition("jobTrackerId", 
jobTrackerId));
+        } else if (queuedAfter != null) {
+            conditions.add(EntityCondition.makeCondition("createdStamp", 
EntityOperator.GREATER_THAN_EQUAL_TO, queuedAfter));
+        }
+        if (userLogin != null && 
UtilValidate.isNotEmpty(userLogin.getString("userLoginId"))) {
+            conditions.add(EntityCondition.makeCondition("authUserLoginId", 
userLogin.getString("userLoginId")));
+        }
+        try {
+            return EntityQuery.use(delegator).from("JobSandbox")
+                    .where(EntityCondition.makeCondition(conditions, 
EntityOperator.AND))
+                    .orderBy("-createdStamp")
+                    .queryFirst();
+        } catch (GenericEntityException e) {
+            Debug.logWarning(e, "Unable to find queued executeMrp JobSandbox 
for MrpRunLog [" + jobTrackerId + "]", MODULE);
+            return null;
+        }
+    }
+
     private static String startMrpRunLog(DispatchContext ctx, Map<String, ? 
extends Object> context, GenericValue userLogin, String mrpRunLogId,
                                          Timestamp startedAt) {
         if (UtilValidate.isEmpty(mrpRunLogId)) {
@@ -691,31 +961,19 @@ public class MrpServices {
         }
         LocalDispatcher dispatcher = ctx.getDispatcher();
         Delegator delegator = ctx.getDelegator();
-        String userLoginId = userLogin != null ? 
userLogin.getString("userLoginId") : null;
         try {
             GenericValue existingMrpRunLog = 
EntityQuery.use(delegator).from("MrpRunLog").where("mrpRunLogId", 
mrpRunLogId).queryOne();
             if (existingMrpRunLog == null) {
-                Map<String, Object> createParameters = 
UtilMisc.toMap("mrpRunLogId", mrpRunLogId, "runStatusId", "SERVICE_RUNNING",
-                        "startedAt", startedAt, "userLogin", userLogin);
-                if (UtilValidate.isNotEmpty((String) context.get("mrpName"))) {
-                    createParameters.put("mrpName", context.get("mrpName"));
-                }
-                if (UtilValidate.isNotEmpty((String) 
context.get("facilityId"))) {
-                    createParameters.put("facilityId", 
context.get("facilityId"));
-                }
-                if (UtilValidate.isNotEmpty((String) 
context.get("facilityGroupId"))) {
-                    createParameters.put("facilityGroupId", 
context.get("facilityGroupId"));
-                }
-                if (context.get("defaultYearsOffset") != null) {
-                    createParameters.put("defaultYearsOffset", 
context.get("defaultYearsOffset"));
-                }
-                if (UtilValidate.isNotEmpty((String) context.get("jobId"))) {
-                    createParameters.put("jobId", context.get("jobId"));
-                }
-                if (UtilValidate.isNotEmpty(userLoginId)) {
-                    createParameters.put("runByUserLoginId", userLoginId);
-                }
-                Map<String, Object> createResponse = 
dispatcher.runSync("createMrpRunLog", createParameters, 60, true);
+                Map<String, Object> createParameters = new HashMap<>();
+                createParameters.put("mrpRunLogId", mrpRunLogId);
+                createParameters.put("mrpName", context.get("mrpName"));
+                createParameters.put("facilityId", context.get("facilityId"));
+                createParameters.put("facilityGroupId", 
context.get("facilityGroupId"));
+                createParameters.put("defaultYearsOffset", 
context.get("defaultYearsOffset"));
+                createParameters.put("runStatusId", "SERVICE_RUNNING");
+                createParameters.put("startedAt", startedAt);
+                createParameters.put("userLogin", userLogin);
+                Map<String, Object> createResponse = 
dispatcher.runSync("createMrpRunLogAndTrackerBeforeQueueingMrp", 
createParameters, 60, true);
                 if (ServiceUtil.isError(createResponse)) {
                     Debug.logWarning("Unable to create MrpRunLog [" + 
mrpRunLogId + "]: " + ServiceUtil.getErrorMessage(createResponse), MODULE);
                 } else {
@@ -727,8 +985,46 @@ public class MrpServices {
         } catch (GenericServiceException e) {
             Debug.logWarning(e, "Unable to call createMrpRunLog for [" + 
mrpRunLogId + "]", MODULE);
         }
-        updateMrpRunLog(dispatcher, UtilMisc.toMap("mrpRunLogId", mrpRunLogId, 
"runStatusId", "SERVICE_RUNNING",
-                "startedAt", startedAt, "userLogin", userLogin), "mark as 
running", mrpRunLogId);
+        Map<String, Object> updateMrpRunLogContext = 
UtilMisc.toMap("mrpRunLogId", mrpRunLogId, "runStatusId", "SERVICE_RUNNING",
+                "startedAt", startedAt, "userLogin", userLogin);
+        try {
+            /*
+             * updateMrpRunLog uses a new transaction so executeMrp records 
that the durable run log started,
+             * even if later MRP processing fails and executeMrp returns an 
error.
+             */
+            Map<String, Object> updateMrpRunLogResponse = 
dispatcher.runSync("updateMrpRunLog", updateMrpRunLogContext, 60, true);
+            if (ServiceUtil.isError(updateMrpRunLogResponse)) {
+                Debug.logWarning("Unable to mark as running MrpRunLog [" + 
mrpRunLogId + "]: "
+                        + 
ServiceUtil.getErrorMessage(updateMrpRunLogResponse), MODULE);
+            }
+        } catch (GenericServiceException e) {
+            Debug.logWarning(e, "Unable to call updateMrpRunLog to mark as 
running [" + mrpRunLogId + "]", MODULE);
+        }
+        String jobTrackerId = (String) context.get("jobTrackerId");
+        if (UtilValidate.isNotEmpty(jobTrackerId)) {
+            try {
+                GenericValue jobTracker = 
EntityQuery.use(delegator).from("JobTracker").where("jobTrackerId", 
jobTrackerId).queryOne();
+                if (jobTracker != null && 
"JOB_T_SCHEDULED".equals(jobTracker.getString("statusId"))) {
+                    Map<String, Object> updateJobTrackerContext = 
UtilMisc.toMap("jobTrackerId", jobTrackerId, "statusId", "JOB_T_RUNNING",
+                            "startDate", startedAt, "userLogin", userLogin);
+                    /*
+                     * updateJobTracker uses a new transaction because 
executeMrp is the queued service that moves the
+                     * durable MRP job from scheduled to running. If 
executeMrp later fails and rolls back, the service
+                     * engine still needs the committed JOB_T_RUNNING state so 
it can complete the tracker with OFBiz's
+                     * valid JOB_T_RUNNING to JOB_T_FINISHED or JOB_T_FAILED 
transition.
+                     */
+                    Map<String, Object> updateJobTrackerResponse = 
dispatcher.runSync("updateJobTracker", updateJobTrackerContext, 60, true);
+                    if (ServiceUtil.isError(updateJobTrackerResponse)) {
+                        Debug.logWarning("Unable to mark JobTracker [" + 
jobTrackerId + "] as running for MrpRunLog [" + mrpRunLogId + "]: "
+                                + 
ServiceUtil.getErrorMessage(updateJobTrackerResponse), MODULE);
+                    }
+                }
+            } catch (GenericEntityException e) {
+                Debug.logWarning(e, "Unable to query JobTracker [" + 
jobTrackerId + "] before starting MrpRunLog [" + mrpRunLogId + "]", MODULE);
+            } catch (GenericServiceException e) {
+                Debug.logWarning(e, "Unable to call updateJobTracker to mark 
as running [" + jobTrackerId + "]", MODULE);
+            }
+        }
         return mrpRunLogId;
     }
 
@@ -787,11 +1083,12 @@ public class MrpServices {
         }
     }
 
-    private static String buildOutcomeMessage(Long mrpEventCount, long 
proposedRequirementCount) {
+    private static String buildOutcomeMessage(Long mrpEventCount, long 
proposedRequirementCount, Locale locale) {
         if (mrpEventCount == null) {
             return null;
         }
-        return "Created " + mrpEventCount + " MRP events and " + 
proposedRequirementCount + " proposed requirements";
+        return UtilProperties.getMessage(RESOURCE, 
"ManufacturingMrpRunOutcomeSummary",
+                UtilMisc.toMap("mrpEventCount", mrpEventCount, 
"proposedRequirementCount", proposedRequirementCount), locale);
     }
 
     private static Map<String, Object> finishMrpRunLog(LocalDispatcher 
dispatcher, GenericValue userLogin, String mrpRunLogId, Timestamp startedAt,
@@ -826,24 +1123,23 @@ public class MrpServices {
                 parameters.put("outcomeMessage", outcomeMessage);
             }
             if (startedAt != null && finishedAt != null) {
-                parameters.put("durationMillis", finishedAt.getTime() - 
startedAt.getTime());
+                parameters.put("durationMillis", new DateRange(startedAt, 
finishedAt).durationInMillis());
+            }
+            /*
+             * updateMrpRunLog uses a new transaction so executeMrp preserves 
the final run status and counts,
+             * even when executeMrp returns an error result that rolls back 
the MRP processing transaction.
+             */
+            Map<String, Object> updateMrpRunLogResponse = 
dispatcher.runSync("updateMrpRunLog", parameters, 60, true);
+            if (ServiceUtil.isError(updateMrpRunLogResponse)) {
+                Debug.logWarning("Unable to finish MrpRunLog [" + mrpRunLogId 
+ "]: "
+                        + 
ServiceUtil.getErrorMessage(updateMrpRunLogResponse), MODULE);
             }
-            updateMrpRunLog(dispatcher, parameters, "finish", mrpRunLogId);
         } catch (RuntimeException e) {
             Debug.logWarning(e, "Unable to prepare MrpRunLog finish parameters 
for [" + mrpRunLogId + "]", MODULE);
-        }
-        return result;
-    }
-
-    private static void updateMrpRunLog(LocalDispatcher dispatcher, 
Map<String, Object> parameters, String action, String mrpRunLogId) {
-        try {
-            Map<String, Object> response = 
dispatcher.runSync("updateMrpRunLog", parameters, 60, true);
-            if (ServiceUtil.isError(response)) {
-                Debug.logWarning("Unable to " + action + " MrpRunLog [" + 
mrpRunLogId + "]: " + ServiceUtil.getErrorMessage(response), MODULE);
-            }
         } catch (GenericServiceException e) {
-            Debug.logWarning(e, "Unable to call updateMrpRunLog to " + action 
+ " [" + mrpRunLogId + "]", MODULE);
+            Debug.logWarning(e, "Unable to call updateMrpRunLog to finish [" + 
mrpRunLogId + "]", MODULE);
         }
+        return result;
     }
 
     /**
@@ -864,7 +1160,7 @@ public class MrpServices {
         LocalDispatcher dispatcher = ctx.getDispatcher();
         GenericValue userLogin = (GenericValue) context.get("userLogin");
         Timestamp now = UtilDateTime.nowTimestamp();
-        String mrpRunLogId = mrpRunLogIdFromContext(delegator, context);
+        String mrpRunLogId = getMrpRunLogIdFromContext(delegator, context);
         mrpRunLogId = startMrpRunLog(ctx, context, userLogin, mrpRunLogId, 
now);
         Locale locale = (Locale) context.get("locale");
         String mrpName = (String) context.get("mrpName");
@@ -1203,7 +1499,7 @@ public class MrpServices {
             Debug.logInfo("return from executeMrp", MODULE);
             Long mrpEventCount = countMrpEventsForRun(delegator, mrpId);
             Long requirementCount = proposedRequirementCount;
-            String outcomeMessage = buildOutcomeMessage(mrpEventCount, 
proposedRequirementCount);
+            String outcomeMessage = buildOutcomeMessage(mrpEventCount, 
proposedRequirementCount, locale);
             return finishMrpRunLog(dispatcher, userLogin, mrpRunLogId, now, 
mrpId, null, null, mrpEventCount, requirementCount,
                     outcomeMessage, result);
         } catch (RuntimeException e) {
diff --git 
a/applications/manufacturing/webapp/manufacturing/WEB-INF/controller.xml 
b/applications/manufacturing/webapp/manufacturing/WEB-INF/controller.xml
index c06b4f939d..0aaeb056fb 100644
--- a/applications/manufacturing/webapp/manufacturing/WEB-INF/controller.xml
+++ b/applications/manufacturing/webapp/manufacturing/WEB-INF/controller.xml
@@ -468,7 +468,7 @@ under the License.
     </request-map>
     <request-map uri="runMrpGo">
         <security https="true" auth="true"/>
-        <event type="service" invoke="executeMrp" path="async"/>
+        <event type="service" invoke="launchMrpRun"/>
         <response name="success" type="view" value="RunMrpGo"/>
         <response name="error" type="view" value="MrpExecution"/>
     </request-map>

Reply via email to