diff --git a/mrp/class/mo.class.php.org b/mrp/class/mo.class.php
index 666d29a..5db6b4f 100644
--- a/mrp/class/mo.class.php.org
+++ b/mrp/class/mo.class.php
@@ -225,6 +225,11 @@ class Mo extends CommonObject
 	 */
 	public $oldQty;
 
+	/**
+	 * @var array<int, float> Material override qty indexed by fk_product for current fk_bom
+	 */
+	protected $materialOverridesByProduct = array();
+
 
 	// If this object has a subtable with lines
 
@@ -756,6 +761,7 @@ class Mo extends CommonObject
 			include_once DOL_DOCUMENT_ROOT.'/bom/class/bom.class.php';
 			$bom = new BOM($this->db);
 			$bom->fetch($this->fk_bom);
+			$this->materialOverridesByProduct = $this->loadMaterialOverridesForBom((int) $this->fk_bom);
 			if ($bom->bomtype == 1) {
 				$role = 'toproduce';
 				$moline->role = 'toconsume';
@@ -781,7 +787,7 @@ class Mo extends CommonObject
 		if ($this->fk_bom > 0 && is_object($bom)) {	// If a BOM is defined, we know what to consume.
 			if ($bom->id > 0) {
 				// process lines to consume, this needs to recurse through BOM's
-				$error += $this->processBOM($user, $role, $bom, (float) $this->qty);
+				$error += $this->processBOM($user, $role, $bom, (float) $this->qty, (int) $this->fk_bom);
 			}
 		}
 
@@ -804,7 +810,7 @@ class Mo extends CommonObject
 	 * @param  float $quantity Quantity modifier for sub products/BOM
 	 * @return int             Return integer <0 if KO, >0 if OK
 	 */
-	public function processBOM(User $user, $role, $bom, $quantity)
+	public function processBOM(User $user, $role, $bom, $quantity, $rootBomId = 0)
 	{
 		$error = 0;
 
@@ -817,8 +823,16 @@ class Mo extends CommonObject
 			if ($line->fk_bom_child > 0) {
 				$bom = new BOM($this->db);
 				$bom->fetch((int) $line->fk_bom_child);
-				$error += $this->processBOM($user, $role, $bom, $quantity_line);
+				$error += $this->processBOM($user, $role, $bom, $quantity_line, $rootBomId);
 			} else {
+				// For root BOM consumption lines, prefer productionrules material overrides (if available).
+				if ($role === 'toconsume' && (int) $bom->id === (int) $rootBomId) {
+					$fkProduct = (int) $line->fk_product;
+					if (isset($this->materialOverridesByProduct[$fkProduct])) {
+						$quantity_line = (float) $this->materialOverridesByProduct[$fkProduct];
+					}
+				}
+
 				$moline = new MoLine($this->db);
 				$moline->fk_mo = $this->id;
 				$moline->origin_id = $line->id;
@@ -858,6 +872,50 @@ class Mo extends CommonObject
 		return $error;
 	}
 
+	/**
+	 * Load material override quantities for a BOM from custom productionrules table.
+	 * Returns [fk_product => qty]. Empty array means no override.
+	 *
+	 * @param int $fkBom
+	 * @return array<int, float>
+	 */
+	protected function loadMaterialOverridesForBom($fkBom)
+	{
+		global $conf;
+
+		$fkBom = (int) $fkBom;
+		if ($fkBom <= 0) {
+			return array();
+		}
+
+		$table = MAIN_DB_PREFIX.'prod_mo_material_overrides';
+		$sqlCheck = "SHOW TABLES LIKE '".$this->db->escape($table)."'";
+		$resCheck = $this->db->query($sqlCheck);
+		if (!$resCheck || $this->db->num_rows($resCheck) === 0) {
+			return array();
+		}
+
+		$out = array();
+		$sql = "SELECT fk_product, qty FROM ".$table
+			 . " WHERE fk_bom = ".$fkBom
+			 . " AND entity = ".((int) $conf->entity)
+			 . " ORDER BY pos ASC, rowid ASC";
+		$res = $this->db->query($sql);
+		if (!$res) {
+			return array();
+		}
+
+		while ($obj = $this->db->fetch_object($res)) {
+			$fkProduct = (int) $obj->fk_product;
+			$qty = (float) $obj->qty;
+			if ($fkProduct > 0 && $qty > 0) {
+				$out[$fkProduct] = $qty;
+			}
+		}
+
+		return $out;
+	}
+
 
 	/**
 	 * Update quantities in lines to consume and/or lines to produce.
@@ -1172,6 +1230,63 @@ class Mo extends CommonObject
 			// End call triggers
 		}
 
+		// Pre-populate operation tracking skeleton with correct op_rank from BOM line positions.
+		// This ensures operations display with correct rank from productionrules step_order.
+		// Planner will update qty_delta through consume/produce actions.
+		if (!$error) {
+			$sqlOps = "SELECT ml.fk_default_workstation, MIN(ml.position) as min_pos";
+			$sqlOps .= " FROM ".MAIN_DB_PREFIX."mrp_production ml";
+			$sqlOps .= " WHERE ml.fk_mo = ".(int)$this->id;
+			$sqlOps .= " AND ml.role = 'toconsume'";
+			$sqlOps .= " AND ml.fk_default_workstation > 0";
+			$sqlOps .= " GROUP BY ml.fk_default_workstation";
+
+			$resOps = $this->db->query($sqlOps);
+			if ($resOps) {
+				$operations = array(); // [fk_ws => min_position]
+				while ($objOp = $this->db->fetch_object($resOps)) {
+					$operations[(int)$objOp->fk_default_workstation] = (int)$objOp->min_pos;
+				}
+
+				if (!empty($operations)) {
+					// Resolve workstation labels
+					$sqlWs = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."workstation_workstation";
+					$sqlWs .= " WHERE rowid IN (".implode(',', array_map('intval', array_keys($operations))).")";
+					$resWs = $this->db->query($sqlWs);
+					$wsLabels = array();
+					if ($resWs) {
+						while ($objWs = $this->db->fetch_object($resWs)) {
+							$wsLabels[(int)$objWs->rowid] = $objWs->label ?: $objWs->ref;
+						}
+					}
+
+					// Insert skeleton entries (qty_delta=0) with correct op_rank
+					foreach ($operations as $ws_id => $op_rank) {
+						$ws_label = isset($wsLabels[$ws_id]) ? $wsLabels[$ws_id] : '';
+						$sqlIns = "INSERT INTO ".MAIN_DB_PREFIX."mrp_mo_optracking";
+						$sqlIns .= " (fk_mo, fk_ws, ws_label, op_rank, qty_delta, date_entry, fk_user, note)";
+						$sqlIns .= " VALUES (";
+						$sqlIns .= (int)$this->id.", ";
+						$sqlIns .= (int)$ws_id.", ";
+						$sqlIns .= "'".$this->db->escape($ws_label)."', ";
+						$sqlIns .= (int)$op_rank.", ";
+						$sqlIns .= "0, ";
+						$sqlIns .= "'".$this->db->idate($now)."', ";
+						$sqlIns .= (int)$user->id.", ";
+						$sqlIns .= "'Initial operation setup'";
+						$sqlIns .= ")";
+
+						$resIns = $this->db->query($sqlIns);
+						if (!$resIns) {
+							$error++;
+							$this->error = 'Failed to initialize operation tracking: '.$this->db->lasterror();
+							break;
+						}
+					}
+				}
+			}
+		}
+
 		if (!$error) {
 			$this->oldref = $this->ref;
 
