Magento Credit Memo Creation

Refunds in Magento are created in the form of Credit Memos. Magento credit memo creation occurs within the Sales -> Orders section of the admin area and choosing an order to edit.

Similar to the invoice and shipment buttons in this area, the __construct() method of the Mage_Adminhtml_Sales_Order_View class determines whether the button should get shown.

<?php
class Mage_Adminhtml_Block_Sales_Order_View extends Mage_Adminhtml_Block_Widget_Form_Container {
    ....
    public function __construct() {
        if ($this->_isAllowedAction('creditmemo') && $order->canCreditmemo()) {
            $confirmationMessage = $coreHelper->jsQuoteEscape(
                Mage::helper('sales')->__('This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you wish to proceed?')
            );
            $onClick = "setLocation('{$this->getCreditmemoUrl()}')";
            if ($order->getPayment()->getMethodInstance()->isGateway()) {
                $onClick = "confirmSetLocation('{$confirmationMessage}', '{$this->getCreditmemoUrl()}')";
            }
            $this->_addButton('order_creditmemo', array(
                'label'     => Mage::helper('sales')->__('Credit Memo'),
                'onclick'   => $onClick,
                'class'     => 'go'
            ));
        }
        ....
    }
    ....
}

The canCreditMemo() method can be seen below.

public function canCreditmemo()
{
    if ($this->hasForcedCanCreditmemo()) {
        return $this->getForcedCanCreditmemo();
    }

    if ($this->canUnhold() || $this->isPaymentReview()) {
        return false;
    }

    if ($this->isCanceled() || $this->getState() === self::STATE_CLOSED) {
        return false;
    }

    /**
     * We can have problem with float in php (on some server $a=762.73;$b=762.73; $a-$b!=0)
     * for this we have additional diapason for 0
     * TotalPaid - contains amount, that were not rounded.
     */
    if (abs($this->getStore()->roundPrice($this->getTotalPaid()) - $this->getTotalRefunded()) < .0001) {
        return false;
    }

    if ($this->getActionFlag(self::ACTION_FLAG_EDIT) === false) {
        return false;
    }
    return true;
}

When we create and save a credit memo, the saveAction() method of the CreditmemoController class is executed.

public function saveAction()
{
    $data = $this->getRequest()->getPost('creditmemo');
    if (!empty($data['comment_text'])) {
        Mage::getSingleton('adminhtml/session')->setCommentText($data['comment_text']);
    }

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

            $comment = '';
            if (!empty($data['comment_text'])) {
                $creditmemo->addComment(
                    $data['comment_text'],
                    isset($data['comment_customer_notify']),
                    isset($data['is_visible_on_front'])
                );
                if (isset($data['comment_customer_notify'])) {
                    $comment = $data['comment_text'];
                }
            }

            if (isset($data['do_refund'])) {
                $creditmemo->setRefundRequested(true);
            }
            if (isset($data['do_offline'])) {
                $creditmemo->setOfflineRequested((bool)(int)$data['do_offline']);
            }

            $creditmemo->register();
            if (!empty($data['send_email'])) {
                $creditmemo->setEmailSent(true);
            }

            $creditmemo->getOrder()->setCustomerNoteNotify(!empty($data['send_email']));
            $this->_saveCreditmemo($creditmemo);
            $creditmemo->sendEmail(!empty($data['send_email']), $comment);
            $this->_getSession()->addSuccess($this->__('The credit memo has been created.'));
            Mage::getSingleton('adminhtml/session')->getCommentText(true);
            $this->_redirect('*/sales_order/view', array('order_id' => $creditmemo->getOrderId()));
            return;
        } else {
            $this->_forward('noRoute');
            return;
        }
    }
    ....
}

Similarly to invoices and shipments, we have another _init() method for credit memos in the form of _initCreditMemo().

protected function _initCreditmemo($update = false)
{
    $this->_title($this->__('Sales'))->_title($this->__('Credit Memos'));

    $creditmemo = false;
    $creditmemoId = $this->getRequest()->getParam('creditmemo_id');
    $orderId = $this->getRequest()->getParam('order_id');
    if ($creditmemoId) {
        $creditmemo = Mage::getModel('sales/order_creditmemo')->load($creditmemoId);
    } elseif ($orderId) {
        $data   = $this->getRequest()->getParam('creditmemo');
        $order  = Mage::getModel('sales/order')->load($orderId);
        $invoice = $this->_initInvoice($order);

        if (!$this->_canCreditmemo($order)) {
            return false;
        }

        $savedData = $this->_getItemData();

        $qtys = array();
        $backToStock = array();
        foreach ($savedData as $orderItemId =>$itemData) {
            if (isset($itemData['qty'])) {
                $qtys[$orderItemId] = $itemData['qty'];
            }
            if (isset($itemData['back_to_stock'])) {
                $backToStock[$orderItemId] = true;
            }
        }
        $data['qtys'] = $qtys;

        $service = Mage::getModel('sales/service_order', $order);
        if ($invoice) {
            $creditmemo = $service->prepareInvoiceCreditmemo($invoice, $data);
        } else {
            $creditmemo = $service->prepareCreditmemo($data);
        }

        /**
         * Process back to stock flags
         */
        foreach ($creditmemo->getAllItems() as $creditmemoItem) {
            $orderItem = $creditmemoItem->getOrderItem();
            $parentId = $orderItem->getParentItemId();
            if (isset($backToStock[$orderItem->getId()])) {
                $creditmemoItem->setBackToStock(true);
            } elseif ($orderItem->getParentItem() && isset($backToStock[$parentId]) && $backToStock[$parentId]) {
                $creditmemoItem->setBackToStock(true);
            } elseif (empty($savedData)) {
                $creditmemoItem->setBackToStock(Mage::helper('cataloginventory')->isAutoReturnEnabled());
            } else {
                $creditmemoItem->setBackToStock(false);
            }
        }
    }

    $args = array('creditmemo' => $creditmemo, 'request' => $this->getRequest());
    Mage::dispatchEvent('adminhtml_sales_order_creditmemo_register_before', $args);

    Mage::register('current_creditmemo', $creditmemo);
    return $creditmemo;
}

There are a couple of interesting sections of code here. We can see that items’ stock are replenished using the setBackToStock() method.

We can also see that if an $invoice variable returns true, we run the prepareInvoiceCreditmemo() method and if it doesn’t, we run the prepareCreditmemo() method.

Magento uses the model Mage_Sales_Model_Order_Creditmemo to work with the data of the refund. The main method here is the refund() method.

This method gets called by looking back at the saveAction() method of the CreditmemoController.php file, and viewing the register() method.

public function register()
{
    if ($this->getId()) {
        Mage::throwException(
            Mage::helper('sales')->__('Cannot register an existing credit memo.')
        );
    }

    foreach ($this->getAllItems() as $item) {
        if ($item->getQty()>0) {
            $item->register();
        }
        else {
            $item->isDeleted(true);
        }
    }

    $this->setDoTransaction(true);
    if ($this->getOfflineRequested()) {
        $this->setDoTransaction(false);
    }
    $this->refund();
    ....
}

The refund() method is as follows.

public function refund()
{
    $this->setState(self::STATE_REFUNDED);
    $orderRefund = Mage::app()->getStore()->roundPrice(
        $this->getOrder()->getTotalRefunded()+$this->getGrandTotal()
    );
    $baseOrderRefund = Mage::app()->getStore()->roundPrice(
        $this->getOrder()->getBaseTotalRefunded()+$this->getBaseGrandTotal()
    );

    if ($baseOrderRefund > Mage::app()->getStore()->roundPrice($this->getOrder()->getBaseTotalPaid())) {

        $baseAvailableRefund = $this->getOrder()->getBaseTotalPaid()- $this->getOrder()->getBaseTotalRefunded();

        Mage::throwException(
            Mage::helper('sales')->__('Maximum amount available to refund is %s', $this->getOrder()->formatBasePrice($baseAvailableRefund))
        );
    }
    $order = $this->getOrder();
    ....

    if ($this->getInvoice()) {
        $this->getInvoice()->setIsUsedForRefund(true);
        $this->getInvoice()->setBaseTotalRefunded(
            $this->getInvoice()->getBaseTotalRefunded() + $this->getBaseGrandTotal()
        );
        $this->setInvoiceId($this->getInvoice()->getId());
    }

    if (!$this->getPaymentRefundDisallowed()) {
        $order->getPayment()->refund($this);
    }

    Mage::dispatchEvent('sales_order_creditmemo_refund', array($this->_eventObject=>$this));
    return $this;
}

We can see that the state of the order gets set to STATE_REFUNDED.

Similar to the invoices, credit memos in Magento can also be created via payment gateways such as PayPal using the IPN functionality.

The tables for storing refund data are similar to those that store invoice information. The tables involved for credit memos are:

  • sales_flat_creditmemo – Stores generic information about credit memos.
  • sales_flat_creditmemo_comment – Stores comments against credit memos.
  • sales_flat_creditmemo_item – Stores information about the items against credit memos.
  • sales_flat_creditmemo_grid – Stores information to render on the grid page for improved performance.

Note: This article is based on Magento Community/Open Source version 1.9.