diff --git a/Backend/ERPCore/Dtos/Production/RunDtos.cs b/Backend/ERPCore/Dtos/Production/RunDtos.cs
index 9c45f1a..28743c9 100644
--- a/Backend/ERPCore/Dtos/Production/RunDtos.cs
+++ b/Backend/ERPCore/Dtos/Production/RunDtos.cs
@@ -99,8 +99,10 @@ public sealed class CreateRunRequest
}
///
-/// Per-run scaling override (FR-MFG-08). Only accepted while the stage has not started
-/// (409 STAGE_NOT_EDITABLE).
+/// Per-run scaling override (FR-MFG-08). Accepted before the stage starts, and — for a Stock
+/// input only — while it is running too: raising
+/// draws the extra amount from FIFO, lowering it returns the excess (409
+/// STAGE_NOT_EDITABLE for every other status).
///
public sealed class UpdateStageQuantitiesRequest
{
@@ -116,4 +118,11 @@ public sealed class StageQuantityLine
[Range(0.0001, double.MaxValue)]
public decimal PlannedQty { get; set; }
+
+ ///
+ /// Required only when this line lowers a running Stock input below what it has already
+ /// consumed — the excess is returned to stock the same way ReturnLeftoverAsync
+ /// does, and that always needs a reason (FR-MFG-14). Ignored otherwise.
+ ///
+ public int? ReasonCodeId { get; set; }
}
diff --git a/Backend/ERPCore/Services/Production/ProductionRunService.cs b/Backend/ERPCore/Services/Production/ProductionRunService.cs
index 2a00825..5c25218 100644
--- a/Backend/ERPCore/Services/Production/ProductionRunService.cs
+++ b/Backend/ERPCore/Services/Production/ProductionRunService.cs
@@ -294,17 +294,58 @@ public sealed class ProductionRunService : IProductionRunService
stage = run.Stages.FirstOrDefault(s => s.RunStageId == runStageId)
?? throw new NotFoundException($"Stage {runStageId} is not part of run {runId}.");
- // FR-MFG-08: editable until the stage starts. Once started, consumption has
- // already happened against the planned figures.
- if (stage.Status is not (ProductionStageStatus.Waiting or ProductionStageStatus.Ready))
+ // FR-MFG-08: freely editable before the stage starts. Once it is running, a Stock
+ // input can still move — raising it draws the extra amount via FIFO (mirroring
+ // StartStageAsync's initial draw), lowering it returns the excess to stock
+ // (mirroring ReturnLeftoverAsync) — but every other status stays locked.
+ if (stage.Status is not (ProductionStageStatus.Waiting or ProductionStageStatus.Ready or ProductionStageStatus.InProgress))
throw new DomainException(ErrorCodes.StageNotEditable,
- $"Stage '{stage.Name}' is {stage.Status}; quantities can only be edited before it starts.", 409);
+ $"Stage '{stage.Name}' is {stage.Status}; quantities can only be edited before it starts or while it is running.", 409);
+
+ var now = DateTime.UtcNow;
+ var balances = new Dictionary();
+ var consumed = new List();
+ var returned = new List