4

I have cancelled an order in magento, I need to get back the order with status pending and wanted to process the same order again. Is there any possibility of doing it.

  • If you're using a payment gateway, it might not be possible because that transaction is voided. You would have to hit "Re-Order" and re-create the order. – andyjv Aug 28 '13 at 19:11
  • i tried the mamoc extension and it´s true that a uncancel button appears but it seems to do nothing. It sends me to the backend homepage. The order still canceled. Someone have used it with success??? – user3153 Sep 5 '13 at 12:43
  • @Nived: Is your issue solved? – zus Nov 27 '17 at 5:02
13

Massive props to Marius for reminding me that there are columns that are updated with canceled data.

$incrementId = '1000001'; //replace this with the increment id of your actual order
$order = Mage::getModel('sales/order')->loadByIncrementId($incrementId);

$order->setState(Mage_Sales_Model_Order::STATE_PROCESSING);
$order->setStatus('pending');

$order->setBaseDiscountCanceled(0);
$order->setBaseShippingCanceled(0);
$order->setBaseSubtotalCanceled(0);
$order->setBaseTaxCanceled(0);
$order->setBaseTotalCanceled(0);
$order->setDiscountCanceled(0);
$order->setShippingCanceled(0);
$order->setSubtotalCanceled(0);
$order->setTaxCanceled(0);
$order->setTotalCanceled(0);

foreach($order->getAllItems() as $item){
    $item->setQtyCanceled(0);
    $item->setTaxCanceled(0);
    $item->setHiddenTaxCanceled(0);
    $item->save();
}

$order->save();

Voila!

  • 2
    You are on the right track, but there is more than that. You also need to change some other fields like total_qty_canceled (or something like that), you also need to change the state of the ordered items. It may be more but this popped into my mind now. – Marius Sep 5 '13 at 18:31
  • X-O gosh you're totally right lol – philwinkle Sep 5 '13 at 18:34
  • That's more like it. I knew you can do it :) – Marius Sep 5 '13 at 18:45
  • See this script here, just used it and it is based on this answer: gist.github.com/derak-kilgo/3b778f49c2ba6b680bab6d1a1fa76daf – SR_Magento Jun 28 '17 at 10:09
  • 1
    Wouldn't we need to also touch the product model to take the product back out of stock? Or is there an observer on Mage_Sale_Model_Order_Item? – Wayne Theisinger Jun 29 '17 at 10:00
4

You can use this free extension for this: http://mamoc.com/uncancel-order

UPDATE: It adds a button to the order view page when the order status is cancelled. When you undo a cancelled order the extension also updates the product stock information and sets the order state to pending allowing you to process the order like any other.

We're looking for long answers that provide some explanation and context. Don't just give a one-line answer; explain why your answer is right, ideally with citations. Answers that don't include explanations may be removed.

3

All other answers are not considering the product stock... (and neither do the extensions mentioned... I've checked the code).

There is an observer defined in app/code/core/Mage/CatalogInventory/etc/config.xml

            <sales_order_item_cancel>
            <observers>
                <inventory>
                    <class>cataloginventory/observer</class>
                    <method>cancelOrderItem</method>
                </inventory>
            </observers>
        </sales_order_item_cancel>

And that observer increases the stock when an order is cancelled...

    /**
 * Cancel order item
 *
 * @param   Varien_Event_Observer $observer
 * @return  Mage_CatalogInventory_Model_Observer
 */
public function cancelOrderItem($observer)
{
    $item = $observer->getEvent()->getItem();

    $children = $item->getChildrenItems();
    $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled();

    $productId = $item->getProductId();
    if ($item->getId() && $productId && empty($children) && $qty) {
        Mage::getSingleton('cataloginventory/stock')->backItemQty($productId, $qty);
    }

    return $this;
}

To fully uncancel an order, not only do we need to deal with the order as demonstrated by @philwinkle:

We also need to decrement the product quantity, and for this I believe that we need to reproduce the code from another observer registered in app/code/core/Mage/CatalogInventory/etc/config.xml and fires when an order is placed.

<sales_model_service_quote_submit_before>
    <observers>
        <inventory>
            <class>cataloginventory/observer</class>
            <method>subtractQuoteInventory</method>
        </inventory>
    </observers>
</sales_model_service_quote_submit_before>

The observer function registers a products sale, but also sets a reindex flag...

/**
 * Subtract quote items qtys from stock items related with quote items products.
 *
 * Used before order placing to make order save/place transaction smaller
 * Also called after every successful order placement to ensure subtraction of inventory
 *
 * @param Varien_Event_Observer $observer
 */
public function subtractQuoteInventory(Varien_Event_Observer $observer)
{
    $quote = $observer->getEvent()->getQuote();

    // Maybe we've already processed this quote in some event during order placement
    // e.g. call in event 'sales_model_service_quote_submit_before' and later in 'checkout_submit_all_after'
    if ($quote->getInventoryProcessed()) {
        return;
    }
    $items = $this->_getProductsQty($quote->getAllItems());

    /**
     * Remember items
     */
    $this->_itemsForReindex = Mage::getSingleton('cataloginventory/stock')->registerProductsSale($items);

    $quote->setInventoryProcessed(true);
    return $this;
}

registerProductsSale() is expecting an array, but an array containing what values? The function's docbloc doesn't give us much of a hint...

/**
 * Subtract product qtys from stock.
 * Return array of items that require full save
 *
 * @param array $items
 * @return array
 */

whoopie its an array - as I said not very helpful... however, if we step into it, we see that the first line calls $qtys = $this->_prepareProductQtys($items):

and the docbloc for this function is a lot more helpful...

/**
 * Prepare array($productId=>$qty) based on array($productId => array('qty'=>$qty, 'item'=>$stockItem))
 *
 * @param array $items
 */

ahh... Mage::getSingleton('cataloginventory/stock')->registerProductsSale($items) is expecting array($productId => array('qty'=>$qty, 'item'=>$stockItem))

and the code itself:

if (empty($item['item'])) {
    $stockItem = Mage::getModel('cataloginventory/stock_item')->loadByProduct($productId);
}

shows that if $stockItem is empty it will helpfully instantiate it from the productId, meaning that we only really need to pass array($productId => array('qty'=>$qty)

At this point we might think we are finished... but hold your horses... According to this Stack Exchange we're not done yet.

In Magento 1, Inventory information is stored in a Stock Item object. Each Stock Item object is associated with a product object. Magento persists Stock Item objects to the cataloginventory_stock_item table.

However, each Stock Item object has a related Stock Status object. Stock Status appears to be an (index|cache|de-normalized) table that stores a product's stock status as in integer constant, and allows you to associate multiple stock items and products with a different website_id. Magento persists stock status objects to the cataloginventory_stock_status. The class constants and table schema for this are below.

class Mage_CatalogInventory_Model_Stock_Status extends 
Mage_Core_Model_Abstract
{
    const STATUS_OUT_OF_STOCK       = 0;
    const STATUS_IN_STOCK           = 1;
}

mysql> describe cataloginventory_stock_status;
+--------------+----------------------+------+-----+---------+-------+
| Field        | Type                 | Null | Key | Default | Extra |
+--------------+----------------------+------+-----+---------+-------+
| product_id   | int(10) unsigned     | NO   | PRI | NULL    |       |
| website_id   | smallint(5) unsigned | NO   | PRI | NULL    |       |
| stock_id     | smallint(5) unsigned | NO   | PRI | NULL    |       |
| qty          | decimal(12,4)        | NO   |     | 0.0000  |       |
| stock_status | smallint(5) unsigned | NO   |     | NULL    |       |
+--------------+----------------------+------+-----+---------+-------+
5 rows in set (0.01 sec)

However... there is also a qty column in this table.

qty in cataloginventory_stock_item is the source of truth, and qty in cataloginventory_stock_status is the cached/indexed value. Rebuilding the Stock Status index appears to always copy the value from cataloginventory_stock_item into cataloginventory_stock_status. If there are multiple websites, there will also be a record for each product_id/website_id combo in cataloginventory_stock_status. This suggests the possibility of different stock levels per website, but as far as I can tell the qty value is always the same for each one.

So the upshot of this is, not only do we need to update qty in cataloginventory_stock_item which is what we've achieved so far... we also need to kick off the re-indexing...

In a community version of Magento an re-index is kicked off in

/**
 * Refresh stock index for specific stock items after succesful order placement
 *
 * @param $observer
 */
public function reindexQuoteInventory($observer)
{
    // Reindex quote ids
    $quote = $observer->getEvent()->getQuote();
    $productIds = array();
    foreach ($quote->getAllItems() as $item) {
        $productIds[$item->getProductId()] = $item->getProductId();
        $children   = $item->getChildrenItems();
        if ($children) {
            foreach ($children as $childItem) {
                $productIds[$childItem->getProductId()] = $childItem->getProductId();
            }
        }
    }

    if (count($productIds)) {
        Mage::getResourceSingleton('cataloginventory/indexer_stock')->reindexProducts($productIds);
    }

    // Reindex previously remembered items
    $productIds = array();
    foreach ($this->_itemsForReindex as $item) {
        $item->save();
        $productIds[] = $item->getProductId();
    }
    Mage::getResourceSingleton('catalog/product_indexer_price')->reindexProductIds($productIds);

    $this->_itemsForReindex = array(); // Clear list of remembered items - we don't need it anymore

    return $this;
}

It's interesting to note that in community, not only is Magento re-indexing stock status but also product price - this presumably is because tier pricing is dependent on stock availability??

but for enterprise there is different logic

/**
 * Refresh stock index for specific stock items after succesful order placement
 *
 * @param Varien_Event_Observer $observer
 * @return Enterprise_CatalogInventory_Model_Index_Observer
 */
public function reindexQuoteInventory($observer)
{
    Mage::log('logged in enterprise',null,'wrtInventory.log',true);
    foreach ($this->_itemsForReindex as $item) {
        $item->save();
        Mage::log(var_export($item,true),null,'wrtInventory.log',true);
    }
    $this->_itemsForReindex = array(); // Clear list of remembered items - we don't need it anymore
    return $this;
}

This seems to have been changed in Enterprise to prevent deadlocking the database and a duplicate insert into indexer *_tmp tables as detailed here

The upshot of which is that the qty value cataloginventory_stock_status doesn't update in enterprise on placing an order but instead waits until some other action kicks off a reindex... (a product save or maybe its being run on a cron somewhere).

So the full solution for Un-Cancel assuming you are on Community is:

//set the state back to Pending
$order->setState(
    Mage_Sales_Model_Order::STATE_NEW,
    true,
    'Reset Order',
    false
    );

    $order->setStatus('pending');

    $order->setBaseDiscountCanceled(0);
    $order->setBaseShippingCanceled(0);
    $order->setBaseSubtotalCanceled(0);
    $order->setBaseTaxCanceled(0);
    $order->setBaseTotalCanceled(0);
    $order->setDiscountCanceled(0);
    $order->setShippingCanceled(0);
    $order->setSubtotalCanceled(0);
    $order->setTaxCanceled(0);
    $order->setTotalCanceled(0);

    $stockItems = [];
    $productIds = [];

    foreach ($order->getAllItems() as $item) {
            /** @var $item Mage_Sales_Model_Order_Item */
            $item->setQtyCanceled(0);
            $item->setTaxCanceled(0);
            $item->setHiddenTaxCanceled(0);
            $item->save();

            $stockItems[$item->getProductId()] = ['qty'=>$item->getQtyOrdered()];
            $productIds[$item->getProductId()] = $item->getProductId();
            $children   = $item->getChildrenItems();
            if ($children) {
                foreach ($children as $childItem) {
                    $productIds[$childItem->getProductId()] = $childItem->getProductId();
                }
            }
        }


    /** @var Mage_CatalogInventory_Model_Stock $stockModel */
    $stockModel = Mage::getSingleton('cataloginventory/stock');
    $itemsForReindex = $stockModel->registerProductsSale($stockItems);

    if (count($productIds)) {
        Mage::getResourceSingleton('cataloginventory/indexer_stock')->reindexProducts($productIds);
    } 

    $stockProductIds = array();
    foreach ($itemsForReindex as $item) {
        $item->save();
        $stockProductIds[] = $item->getProductId();
    }
    if (count($stockProductIds)) {
        Mage::getResourceSingleton('catalog/product_indexer_price')->reindexProductIds($stockProductIds);
    }
    $order->save();
}

I'll let those on Enterprise figure out the simpler logic themselves, but suffice it to say that you only need to call save on the items contained within itemsForReindex and don't need to worry about actually calling the 2 index functions.

foreach ($itemsForReindex as $item) {
    $item->save();
}
  • Great and elaborate answer! If the asking guy ever decides to accept an answer (after almost 5 years now) I definitely suggest this one. – mybinaryromance Jul 25 '18 at 8:10
1

What if I tell you a simpler and easier way... Try using this extension and it really works well: https://www.magentocommerce.com/magento-connect/undo-cancel-orders.html

1

If you need the same in M2, you can uncancel order using this product. It restores order to its orignal state.

https://marketplace.magento.com/addify-uncancel.html

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.