Avoid saving entire objects
One of the biggest problem in your case is that you save the entire stock item every time.
It's not performance friendly.
For some models, Magento provides a saveAttribute
method which can save you a lot of resources, however it does not exist for the cataloginventory/stock_item
model.
So I reckon you should implement it via a custom module:
In your config.xml
, you first need to override the resource model:
<models>
<cataloginventory_resource>
<rewrite>
<stock_item>Vendor_Module_Model_CatalogInventory_Resource_Stock_Item</stock_item>
</rewrite>
</cataloginventory_resource>
</models>
Then in your Model/CatalogInventory/Resource/Stock/Item.php
file:
<?php
class Vendor_Module_Model_CatalogInventory_Resource_Stock_Item extends Mage_CatalogInventory_Model_Resource_Stock_Item
{
/**
* Save attribute to model
*
* @param $model - the model
* @param $attributes - array of attributes to get and save from model
* @return $this
* @throws Exception
*/
public function saveAttribute($model, $attributes)
{
try {
$adapter = $this->_getWriteAdapter();
$adapter->beginTransaction();
$condition = $this->_getWriteAdapter()->quoteInto($this->getIdFieldName() . '=?', $model->getId());
$data = array();
foreach ($attributes as $attribute) {
$value = $model->getData($attribute);
if (isset($value)) {
$data[$attribute] = $value;
}
}
if (!empty($data)) {
$this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);
}
$adapter->commit();
} catch (Exception $e) {
$adapter->rollBack();
throw $e;
}
return $this;
}
}
Now in your code, replace:
Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
With:
$stockItem = Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity);
$stockItem->getResource()->saveAttribute($stockItem,array('is_in_stock','qty'));
Avoid loading entire objects
Another issue in your case, is that you load entire objects.
Same as the load, it's pretty bad in terms of performance.
As you're dealing with 20k products, I'm not sure how it will work but maybe using collections will save you some time.
So instead of :
foreach($products AS $product) {
$quantity = $productQuantities[$product->getId()]; // A helper array that I've built before
Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}
Maybe you should try:
$stockItems = Mage::getResourceModel('cataloginventory/stock_item_collection')->addFieldToSelect('product_id')->addFieldToFilter('product_id',array('in'=>$productIds));
foreach($stockItems as $stockItem)
{
$quantity = $productQuantities[$stockItem->getProductId()];
$stockItem->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}
Where $productIds
is an array containing all your product ids.
Avoid looping a collection
I don't know what your $products
variable is but if it is a collection, I suggest you should avoid looping on it.
Magento provides a great iterator walk method with callback method to handle massive collection.
So instead of:
foreach($products AS $product) {
$quantity = $productQuantities[$product->getId()]; // A helper array that I've built before
Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}
You should try:
Mage::getSingleton('core/resource_iterator')->walk($products ->getSelect(), array(array($this, 'updateStock')));
Then declare the following method in your class:
public function updateStock($args)
{
$quantity = $this->productQuantities[$args['row']['entity_id']];
Mage::getModel('cataloginventory/stock_item')->loadByProduct($args['row']['entity_id'])->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}
Note that you will have to make your productQuantities
array accessible globally in your class to be able to do so.
More information about the walk iterator: http://inchoo.net/magento/working-with-large-magento-collections/
Best Answer
This is the default Magento behaviour when you update the stock quantity from backend admin. In the admin product form if you look over the html, for the stock quantity input, you will actually find two inputs:
So when you submit the form after changing the quantity text input to 7 you will actually send two variables to Magento:
product[stock_data][original_inventory_qty] = 6
andproduct[stock_data][qty] = 7
.If you look into the observer that handles the product stock update
Mage_CatalogInventory_Model_Observer
under the_prepareItemForSave($item, $product)
method you will find:So Magento will check each time when you submit an original quantity and only apply a quantity correction and not just update the quantity to the one you've set.
Then the resource model that handles the save,
Mage_CatalogInventory_Model_Resource_Stock_Item
, will check the quantity correction in the_prepareDataForTable(Varien_Object $object, $table)
method:So you don't need to worry about changing the stock quantity from the admin backend because Magento will apply the price correction by default.
You can also do this programmatically, but make sure you use the models available in the
CatalogInventory
module for loading and saving the stock.Updating directly by performing an SQL statement while a customer is placing an order would not be a good idea.