Hello, here’s my proposed “solution”. As I’ve been working for a UK client for the past two weeks on the very topic of BOM and MO, I felt “brave” enough to try something in this regard. 
Disclaimer: The only drawback is that the solution I’m providing is a bit “raw”. I mean that you’ll need to modify a Dolibarr PHP file, so it will be lost in your next update (i.e., you’ll have to apply the same change again). However, I’m going to propose it in the official Github repository so they can add it to the next version (@eldy ).
What I’ve done is very simple: add the following code to the file: /htdocs/mrp/mo_card.php
above the lines (#228):
// Create MO with Childs
if ($action == 'add' && empty($id) && !empty($TBomLineId)) {
you must paste this:
if (!function_exists('f_query')) {
function f_query($sql, $index_field = '')
{
global $db;
$elements = array();
$resql=$db->query($sql);
if ($resql) {
if ($db->num_rows($resql)) {
while ($row = $db->fetch_array($resql)) {
if (is_array($row)) {
if (!empty($index_field) && isset($row[$index_field])) {
$elements[$row[$index_field]] = $row;
} else {
$elements[] = $row;
}
}
}
}
$db->free($resql);
return $elements;
} else {
return "Error ".$db->lasterror();
dol_syslog(get_class($this)."::query ERROR: ".$db->lasterror(), LOG_ERR);
}
}
}
if (!function_exists('_is_there_enough_stock')) {
function _is_there_enough_stock(){
// Get BOM id and quantity to produce
if (empty($_POST['fk_bom']) || empty($_POST['qty'])) return;
$fk_bom = GETPOST('fk_bom', 'alpha');
$qty = GETPOST('qty', 'alpha');
// Get BOM quantity produced
$sql = "SELECT rowid,qty
FROM ".MAIN_DB_PREFIX."bom_bom
WHERE rowid=".$fk_bom;
$elements = f_query($sql, 'rowid');
if (!is_array($elements) || !isset($elements[$fk_bom])) return;
$bom = $elements[$fk_bom];
// Get BOM subproducts (not services) and stocks
$sql = "SELECT ".MAIN_DB_PREFIX."bom_bomline.qty,".MAIN_DB_PREFIX."bom_bomline.fk_product,
".MAIN_DB_PREFIX."bom_bomline.qty_frozen,
".MAIN_DB_PREFIX."product.ref,".MAIN_DB_PREFIX."product.label ,
".MAIN_DB_PREFIX."product.stock, ".MAIN_DB_PREFIX."product.fk_product_type
FROM ".MAIN_DB_PREFIX."bom_bomline JOIN ".MAIN_DB_PREFIX."product
ON ".MAIN_DB_PREFIX."bom_bomline.fk_product=".MAIN_DB_PREFIX."product.rowid
WHERE ".MAIN_DB_PREFIX."bom_bomline.fk_bom=".$fk_bom."
AND ".MAIN_DB_PREFIX."product.fk_product_type='0'";
$bomlines = f_query($sql, 'fk_product');
if (!is_array($bomlines) || count($bomlines)==0) return;
// check stocks depending of quantity
$without_stock = array();
$qty_to_produce = floatval($qty);
$qty_per_BOM = floatval($bom['qty']);
foreach ($bomlines as $prod) {
if (empty($prod['qty_frozen'])) {
$qty_needed = $qty_to_produce * (floatval($prod['qty']) / $qty_per_BOM);
} else {
$qty_needed = floatval($prod['qty']);
}
if ($qty_needed > floatval($prod['stock'])) {
$without_stock[] = "Missing ".round($qty_needed - floatval($prod['stock']),1)
." units stock of ".$prod['label']." [".$prod['ref']."]. ";
}
}
// if it's missing stock for any product
if (count($without_stock)) {
setEventMessage(implode('<br />', $without_stock), 'errors');
header("Location: ".dol_buildpath('/mrp/mo_card.php?action=create&fk_bom='.((int) $fk_bom), 1).'&qty='.$qty);
exit;
}
}
}
if ($action == 'add') {
_is_there_enough_stock();
}
It took me about 3 hours, but it finally worked correctly:
- When you try to create a new MO, this stock check will be executed
- And if any of the required products doesn’t have sufficient stock, the system will return you to the MO creation page, displaying an error message that lists the missing stock units of the products in short supply.
Example of use on my dev instance:

If you have any questions, let me know. Don’t worry about this “patch”… it only makes two database queries and performs some calculations comparing units to be consumed and physical stock, showing an error message if necessary and interrupting the MO creation before it even begins.
Best regards.