Magento Invoice Creation

Magento invoice creation is a feature in Magento whereby an invoice can be generated either by a payment being captured online or offline.

Capturing offline is simply generating the invoice via the admin panel. Within the admin area, by clicking on an order that hasn’t been invoiced yet in Sales -> Orders, an Invoice button should appear in the top right hand corner.

This button is rendered within the Mage_Adminhtml_Block_Sales_Order_View class.

class Mage_Adminhtml_Block_Sales_Order_View extends Mage_Adminhtml_Block_Widget_Form_Container 
{
    ....
    public function viewAction() {
        ....
        if ($this->_isAllowedAction('invoice') && $order->canInvoice()) {
            $_label = $order->getForcedDoShipmentWithInvoice() ?
                Mage::helper('sales')->__('Invoice and Ship') :
                Mage::helper('sales')->__('Invoice');
            $this->_addButton('order_invoice', array(
                'label'     => $_label,
                'onclick'   => 'setLocation(\'' . $this->getInvoiceUrl() . '\')',
                'class'     => 'go'
            ));
        }
        ....
    }
    ....
}

There is an if statement that decides whether the button should appear. The _isAllowedAction() method simply checks if the admin user has permission to view the invoice screen.

The canInvoice() method is as follows.

public function canInvoice()
{
    if ($this->canUnhold() || $this->isPaymentReview()) {
        return false;
    }
    $state = $this->getState();
    if ($this->isCanceled() || $state === self::STATE_COMPLETE || $state === self::STATE_CLOSED) {
        return false;
    }

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

    foreach ($this->getAllItems() as $item) {
        if ($item->getQtyToInvoice()>0 && !$item->getLockedDoInvoice()) {
            return true;
        }
    }
    return false;
}

We can see the conditions that need to be met in order for the invoice button to show.

States in Magento are internal statuses that Magento uses to keep track of order progress. Statuses are the values that are seen when viewing an admin order.

A state can have many statuses but a status can only have one state.

To see more information regarding order statuses being assigned to states, in the admin area, view the System -> Manage Statuses section.

When the invoice information is saved, the saveAction() method is called within the InvoiceController.php class.

public function saveAction()
{
    $data = $this->getRequest()->getPost('invoice');
    $orderId = $this->getRequest()->getParam('order_id');

    if (!empty($data['comment_text'])) {
        Mage::getSingleton('adminhtml/session')->setCommentText($data['comment_text']);
    }

    try {
        $invoice = $this->_initInvoice();
        if ($invoice) {

            if (!empty($data['capture_case'])) {
                $invoice->setRequestedCaptureCase($data['capture_case']);
            }

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

            $invoice->register();

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

            $invoice->getOrder()->setCustomerNoteNotify(!empty($data['send_email']));
            $invoice->getOrder()->setIsInProcess(true);

            $transactionSave = Mage::getModel('core/resource_transaction')
                ->addObject($invoice)
                ->addObject($invoice->getOrder());
            $shipment = false;
            if (!empty($data['do_shipment']) || (int) $invoice->getOrder()->getForcedDoShipmentWithInvoice()) {
                $shipment = $this->_prepareShipment($invoice);
                if ($shipment) {
                    $shipment->setEmailSent($invoice->getEmailSent());
                    $transactionSave->addObject($shipment);
                }
            }
            $transactionSave->save();

            if (isset($shippingResponse) && $shippingResponse->hasErrors()) {
                $this->_getSession()->addError($this->__('The invoice and the shipment  have been created. The shipping label cannot be created at the moment.'));
            } elseif (!empty($data['do_shipment'])) {
                $this->_getSession()->addSuccess($this->__('The invoice and shipment have been created.'));
            } else {
                $this->_getSession()->addSuccess($this->__('The invoice has been created.'));
            }

            // send invoice/shipment emails
            $comment = '';
            if (isset($data['comment_customer_notify'])) {
                $comment = $data['comment_text'];
            }
            try {
                $invoice->sendEmail(!empty($data['send_email']), $comment);
            } catch (Exception $e) {
                Mage::logException($e);
                $this->_getSession()->addError($this->__('Unable to send the invoice email.'));
            }
            if ($shipment) {
                try {
                    $shipment->sendEmail(!empty($data['send_email']));
                } catch (Exception $e) {
                    Mage::logException($e);
                    $this->_getSession()->addError($this->__('Unable to send the shipment email.'));
                }
            }
            Mage::getSingleton('adminhtml/session')->getCommentText(true);
            $this->_redirect('*/sales_order/view', array('order_id' => $orderId));
        } else {
            $this->_redirect('*/*/new', array('order_id' => $orderId));
        }
        return;
    } catch (Mage_Core_Exception $e) {
        $this->_getSession()->addError($e->getMessage());
    } catch (Exception $e) {
        $this->_getSession()->addError($this->__('Unable to save the invoice.'));
        Mage::logException($e);
    }
    $this->_redirect('*/*/new', array('order_id' => $orderId));
}

The saveAction() method is quite lengthy. The key line here is within the _initInvoice() method a call to the prepareInvoice() method. The $savedQtys variable is made up of the total items quantity.

protected function _initInvoice($update = false)
{
    $this->_title($this->__('Sales'))->_title($this->__('Invoices'));

    $invoice = false;
    $itemsToInvoice = 0;
    $invoiceId = $this->getRequest()->getParam('invoice_id');
    $orderId = $this->getRequest()->getParam('order_id');
    if ($invoiceId) {
        $invoice = Mage::getModel('sales/order_invoice')->load($invoiceId);
        if (!$invoice->getId()) {
            $this->_getSession()->addError($this->__('The invoice no longer exists.'));
            return false;
        }
    } elseif ($orderId) {
        $order = Mage::getModel('sales/order')->load($orderId);
        /**
         * Check order existing
         */
        if (!$order->getId()) {
            $this->_getSession()->addError($this->__('The order no longer exists.'));
            return false;
        }
        /**
         * Check invoice create availability
         */
        if (!$order->canInvoice()) {
            $this->_getSession()->addError($this->__('The order does not allow creating an invoice.'));
            return false;
        }
        $savedQtys = $this->_getItemQtys();
        $invoice = Mage::getModel('sales/service_order', $order)->prepareInvoice($savedQtys);
        if (!$invoice->getTotalQty()) {
            Mage::throwException($this->__('Cannot create an invoice without products.'));
        }
    }

    Mage::register('current_invoice', $invoice);
    return $invoice;
}

The prepareInvoice() method is part of Mage_Sales_Model_Service_Order class.

public function prepareInvoice($qtys = array())
{
    $this->updateLocaleNumbers($qtys);
    $invoice = $this->_convertor->toInvoice($this->_order);
    $totalQty = 0;
    foreach ($this->_order->getAllItems() as $orderItem) {
        if (!$this->_canInvoiceItem($orderItem, array())) {
            continue;
        }
        $item = $this->_convertor->itemToInvoiceItem($orderItem);
        if ($orderItem->isDummy()) {
            $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1;
        } else {
            if (isset($qtys[$orderItem->getId()])) {
                $qty = (float) $qtys[$orderItem->getId()];
            } elseif (!count($qtys)) {
                $qty = $orderItem->getQtyToInvoice();
            } else {
                $qty = 0;
            }
        }

        $totalQty += $qty;
        $item->setQty($qty);
        $invoice->addItem($item);
    }

    $invoice->setTotalQty($totalQty);
    $invoice->collectTotals();
    $this->_order->getInvoiceCollection()->addItem($invoice);

    return $invoice;
}

Multiple invoices get be created per order if the order can be captured and captured partially.

public function canEditQty()
{
    if ($this->getInvoice()->getOrder()->getPayment()->canCapture()) {
        return $this->getInvoice()->getOrder()->getPayment()->canCapturePartial();
    }
    return true;
}

The fact that the order can be captured is decided by these properties in each payment model.

protected $_canCapture
protected $_canCapturePartial

Payment vendors may automatically invoice. The ordering process will eventually call a capture() method.

public function capture()
{
    $this->getOrder()->getPayment()->capture($this);
    if ($this->getIsPaid()) {
        $this->pay();
    }
    return $this;
}

The capture method will then call the pay() method if the order has been paid for.

public function pay()
{
    if ($this->_wasPayCalled) {
        return $this;
    }
    $this->_wasPayCalled = true;

    $invoiceState = self::STATE_PAID;
    if ($this->getOrder()->getPayment()->hasForcedState()) {
        $invoiceState = $this->getOrder()->getPayment()->getForcedState();
    }

    $this->setState($invoiceState);

    $this->getOrder()->getPayment()->pay($this);
    $this->getOrder()->setTotalPaid(
        $this->getOrder()->getTotalPaid()+$this->getGrandTotal()
    );
    $this->getOrder()->setBaseTotalPaid(
        $this->getOrder()->getBaseTotalPaid()+$this->getBaseGrandTotal()
    );
    Mage::dispatchEvent('sales_order_invoice_pay', array($this->_eventObject=>$this));
    return $this;
}

Some vendors like PayPal use an IPN (Instant Payment Notification) feature to generate an invoice within Magento. The notification URL within a merchant’s PayPal account will look like the following.

http://www.yoursite.com/paypal/ipn

This will then get Magento to render the indexAction() method of the IpnController.php file.

public function indexAction()
{
    if (!$this->getRequest()->isPost()) {
        return;
    }

    try {
        $data = $this->getRequest()->getPost();
        Mage::getModel('paypal/ipn')->processIpnRequest($data, new Varien_Http_Adapter_Curl());
    } catch (Mage_Paypal_UnavailableException $e) {
        Mage::logException($e);
        $this->getResponse()->setHeader('HTTP/1.1','503 Service Unavailable')->sendResponse();
        exit;
    } catch (Exception $e) {
        Mage::logException($e);
        $this->getResponse()->setHttpResponseCode(500);
    }
}

Eventually, we come to the _registerPaymentCapture() method where we get the created invoice and notify the customer of their payment.

protected function _registerPaymentCapture($skipFraudDetection = false)
{
    if ($this->getRequestData('transaction_entity') == 'auth') {
        return;
    }
    $parentTransactionId = $this->getRequestData('parent_txn_id');
    $this->_importPaymentInformation();
    $payment = $this->_order->getPayment();
    $payment->setTransactionId($this->getRequestData('txn_id'))
        ->setCurrencyCode($this->getRequestData('mc_currency'))
        ->setPreparedMessage($this->_createIpnComment(''))
        ->setParentTransactionId($parentTransactionId)
        ->setShouldCloseParentTransaction('Completed' === $this->getRequestData('auth_status'))
        ->setIsTransactionClosed(0)
        ->registerCaptureNotification(
            $this->getRequestData('mc_gross'),
            $skipFraudDetection && $parentTransactionId
        );
    $this->_order->save();

    // notify customer
    $invoice = $payment->getCreatedInvoice();
    if ($invoice && !$this->_order->getEmailSent()) {
        $this->_order->queueNewOrderEmail()->addStatusHistoryComment(
            Mage::helper('paypal')->__('Notified customer about invoice #%s.', $invoice->getIncrementId())
        )
        ->setIsCustomerNotified(true)
        ->save();
    }
}

Invoice information gets saved into four different tables in the database:

  • sales_flat_invoice – stores main information of invoices.
  • sales_flat_invoice_comment – stores comments on an invoice.
  • sales_flat_invoice_item – stores the list of items against an invoice.
  • sales_flat_invoice_grid – stores some information of invoices to increase load speed when showing a grid invoice.

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