Magento – How to credit-memo and/or return-to-stock a “completed” $0 order

creditmemoinventoryinvoicestockzero-subtotal

Magento won't let you create a credit memo for $0. In the case of a $0 order, what is the correct way to reverse/close/cancel the order, and return the item to stock?

From looking at the core code, it looks like Magento has some reason for not allowing credit-memos for $0 orders, but I can't understand what it is (although that isn't what this question is about.)

The order's canCreditmemo() method returns false if the total minus the refunded is zero (which it always will be for a $0 order).

if (abs($this->getStore()->roundPrice($this->getTotalPaid()) - $this->getTotalRefunded()) < .0001) {
    return false;
}

In the credit memo controller, it requires that you enter a positive number, but for a $0 order, there is no actual money to refund or credit.

if (($creditmemo->getGrandTotal() <=0) && (!$creditmemo->getAllowZeroGrandTotal())) {
    Mage::throwException(
        $this->__('Credit memo\'s total must be positive.')
    );
}

You are probably wondering why on earth this is a use-case:

Our client was creating a manual order through the admin for someone. The order total was (intentionally) $0. Unfortunately, they "purchased" the wrong product, which throws off their inventory and their reports. We ended up setting the order to "Canceled" status manually by calling $order->setStatus('Canceled');, but to me this isn't a very ideal solution because it doesn't return the item to stock, nor does it save any history of what happened, such that a future user could understand that the product was/wasn't returned to stock.

As a Magento developer, what would you advise your client to do in this scenario?

Since this is Magento stack-exchange, "Use a real ERP and inventory management system" is not an answer.

Best Answer

I ran into the same problem but I approached the solution a bit differently using di preferences; here is the code:

My/Module/etc/adminhtml/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader" type="My\Module\Controller\Adminhtml\Order\CreditmemoLoader"/>
</config>

My/Module/Controller/Adminhtml/Order/CreditmemoLoader.php

<?php

namespace My\Module\Controller\Adminhtml\Order;

class CreditmemoLoader extends \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader {

    protected function _canCreditmemo($order)
    {
        if(!$order->isCanceled() && $order->getState() !== \Magento\Sales\Model\Order::STATE_CLOSED)
            $order->setForcedCanCreditmemo(true);

        return parent::_canCreditmemo($order);
    }

    public function load()
    {
        $result = parent::load();
        if($result instanceof \Magento\Sales\Model\Order\Creditmemo){
            $result->setAllowZeroGrandTotal(true);
        }
        return $result;
    }

}

Here I'm using preferences (we can't use plugins because of the protected function) to override the CreditmemoLoader class which is used to create CMs. I use setForcedCanCreditmemo to make sure we can create the CM on the Order and setAllowZeroGrandTotal to allow a $0 CM. You could perform additional checks on the order here too.

Related Topic