*/ class Mage_Sales_Model_Order_Invoice extends Mage_Sales_Model_Abstract { /** * Invoice states */ const STATE_OPEN = 1; const STATE_PAID = 2; const STATE_CANCELED = 3; const CAPTURE_ONLINE = 'online'; const CAPTURE_OFFLINE = 'offline'; const NOT_CAPTURE = 'not_capture'; const XML_PATH_EMAIL_TEMPLATE = 'sales_email/invoice/template'; const XML_PATH_EMAIL_GUEST_TEMPLATE = 'sales_email/invoice/guest_template'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/invoice/identity'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/invoice/copy_to'; const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/invoice/copy_method'; const XML_PATH_EMAIL_ENABLED = 'sales_email/invoice/enabled'; const XML_PATH_UPDATE_EMAIL_TEMPLATE = 'sales_email/invoice_comment/template'; const XML_PATH_UPDATE_EMAIL_GUEST_TEMPLATE = 'sales_email/invoice_comment/guest_template'; const XML_PATH_UPDATE_EMAIL_IDENTITY = 'sales_email/invoice_comment/identity'; const XML_PATH_UPDATE_EMAIL_COPY_TO = 'sales_email/invoice_comment/copy_to'; const XML_PATH_UPDATE_EMAIL_COPY_METHOD = 'sales_email/invoice_comment/copy_method'; const XML_PATH_UPDATE_EMAIL_ENABLED = 'sales_email/invoice_comment/enabled'; const REPORT_DATE_TYPE_ORDER_CREATED = 'order_created'; const REPORT_DATE_TYPE_INVOICE_CREATED = 'invoice_created'; /* * Identifier for order history item */ const HISTORY_ENTITY_NAME = 'invoice'; protected static $_states; protected $_items; protected $_comments; protected $_order; /** * Calculator instances for delta rounding of prices * * @var array */ protected $_rounders = array(); protected $_saveBeforeDestruct = false; protected $_eventPrefix = 'sales_order_invoice'; protected $_eventObject = 'invoice'; /** * Whether the pay() was called * @var bool */ protected $_wasPayCalled = false; public function __destruct() { if ($this->_saveBeforeDestruct) { $this->save(); } } /** * Initialize invoice resource model */ protected function _construct() { $this->_init('sales/order_invoice'); } /** * Init mapping array of short fields to its full names * * @return Mage_Sales_Model_Order_Invoice */ protected function _initOldFieldsMap() { $this->_oldFieldsMap = Mage::helper('sales')->getOldFieldMap('order_invoice'); return $this; } /** * Load invoice by increment id * * @param string $incrementId * @return Mage_Sales_Model_Order_Invoice */ public function loadByIncrementId($incrementId) { $ids = $this->getCollection() ->addAttributeToFilter('increment_id', $incrementId) ->getAllIds(); if (!empty($ids)) { reset($ids); $this->load(current($ids)); } return $this; } /** * Retrieve invoice configuration model * * @return Mage_Sales_Model_Order_Invoice_Config */ public function getConfig() { return Mage::getSingleton('sales/order_invoice_config'); } /** * Retrieve store model instance * * @return Mage_Core_Model_Store */ public function getStore() { return $this->getOrder()->getStore(); } /** * Declare order for invoice * * @param Mage_Sales_Model_Order $order * @return Mage_Sales_Model_Order_Invoice */ public function setOrder(Mage_Sales_Model_Order $order) { $this->_order = $order; $this->setOrderId($order->getId()) ->setStoreId($order->getStoreId()); return $this; } /** * Retrieve the order the invoice for created for * * @return Mage_Sales_Model_Order */ public function getOrder() { if (!$this->_order instanceof Mage_Sales_Model_Order) { $this->_order = Mage::getModel('sales/order')->load($this->getOrderId()); } return $this->_order->setHistoryEntityName(self::HISTORY_ENTITY_NAME); } /** * Retrieve the increment_id of the order * * @return string */ public function getOrderIncrementId() { return Mage::getModel('sales/order')->getResource()->getIncrementId($this->getOrderId()); } /** * Retrieve billing address * * @return Mage_Sales_Model_Order_Address */ public function getBillingAddress() { return $this->getOrder()->getBillingAddress(); } /** * Retrieve shipping address * * @return Mage_Sales_Model_Order_Address */ public function getShippingAddress() { return $this->getOrder()->getShippingAddress(); } /** * Check invoice cancel state * * @return bool */ public function isCanceled() { return $this->getState() == self::STATE_CANCELED; } /** * Check invice capture action availability * * @return bool */ public function canCapture() { return $this->getState() != self::STATE_CANCELED && $this->getState() != self::STATE_PAID && $this->getOrder()->getPayment()->canCapture(); } /** * Check invice void action availability * * @return bool */ public function canVoid() { $canVoid = false; if ($this->getState() == self::STATE_PAID) { $canVoid = $this->getCanVoidFlag(); /** * If we not retrieve negative answer from payment yet */ if (is_null($canVoid)) { $canVoid = $this->getOrder()->getPayment()->canVoid($this); if ($canVoid === false) { $this->setCanVoidFlag(false); $this->_saveBeforeDestruct = true; } } else { $canVoid = (bool) $canVoid; } } return $canVoid; } /** * Check invoice cancel action availability * * @return bool */ public function canCancel() { return $this->getState() == self::STATE_OPEN; } /** * Check invoice refund action availability * * @return bool */ public function canRefund() { if ($this->getState() != self::STATE_PAID) { return false; } if (abs($this->getBaseGrandTotal() - $this->getBaseTotalRefunded()) < .0001) { return false; } return true; } /** * Capture invoice * * @return Mage_Sales_Model_Order_Invoice */ public function capture() { $this->getOrder()->getPayment()->capture($this); if ($this->getIsPaid()) { $this->pay(); } return $this; } /** * Pay invoice * * @return Mage_Sales_Model_Order_Invoice */ 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; } /** * Whether pay() method was called (whether order and payment totals were updated) * @return bool */ public function wasPayCalled() { return $this->_wasPayCalled; } /** * Void invoice * * @return Mage_Sales_Model_Order_Invoice */ public function void() { $this->getOrder()->getPayment()->void($this); $this->cancel(); return $this; } /** * Cancel invoice action * * @return Mage_Sales_Model_Order_Invoice */ public function cancel() { $order = $this->getOrder(); $order->getPayment()->cancelInvoice($this); foreach ($this->getAllItems() as $item) { $item->cancel(); } /** * Unregister order totals only for invoices in state PAID */ $order->setTotalInvoiced($order->getTotalInvoiced() - $this->getGrandTotal()); $order->setBaseTotalInvoiced($order->getBaseTotalInvoiced() - $this->getBaseGrandTotal()); $order->setSubtotalInvoiced($order->getSubtotalInvoiced() - $this->getSubtotal()); $order->setBaseSubtotalInvoiced($order->getBaseSubtotalInvoiced() - $this->getBaseSubtotal()); $order->setTaxInvoiced($order->getTaxInvoiced() - $this->getTaxAmount()); $order->setBaseTaxInvoiced($order->getBaseTaxInvoiced() - $this->getBaseTaxAmount()); $order->setHiddenTaxInvoiced($order->getHiddenTaxInvoiced() - $this->getHiddenTaxAmount()); $order->setBaseHiddenTaxInvoiced($order->getBaseHiddenTaxInvoiced() - $this->getBaseHiddenTaxAmount()); $order->setShippingTaxInvoiced($order->getShippingTaxInvoiced() - $this->getShippingTaxAmount()); $order->setBaseShippingTaxInvoiced($order->getBaseShippingTaxInvoiced() - $this->getBaseShippingTaxAmount()); $order->setShippingInvoiced($order->getShippingInvoiced() - $this->getShippingAmount()); $order->setBaseShippingInvoiced($order->getBaseShippingInvoiced() - $this->getBaseShippingAmount()); $order->setDiscountInvoiced($order->getDiscountInvoiced() - $this->getDiscountAmount()); $order->setBaseDiscountInvoiced($order->getBaseDiscountInvoiced() - $this->getBaseDiscountAmount()); $order->setBaseTotalInvoicedCost($order->getBaseTotalInvoicedCost() - $this->getBaseCost()); if ($this->getState() == self::STATE_PAID) { $this->getOrder()->setTotalPaid($this->getOrder()->getTotalPaid()-$this->getGrandTotal()); $this->getOrder()->setBaseTotalPaid($this->getOrder()->getBaseTotalPaid()-$this->getBaseGrandTotal()); } $this->setState(self::STATE_CANCELED); $this->getOrder()->setState(Mage_Sales_Model_Order::STATE_PROCESSING, true); Mage::dispatchEvent('sales_order_invoice_cancel', array($this->_eventObject=>$this)); return $this; } /** * Invoice totals collecting * * @return Mage_Sales_Model_Order_Invoice */ public function collectTotals() { foreach ($this->getConfig()->getTotalModels() as $model) { $model->collect($this); } return $this; } /** * Round price considering delta * * @param float $price * @param string $type * @param bool $negative Indicates if we perform addition (true) or subtraction (false) of rounded value * @return float */ public function roundPrice($price, $type = 'regular', $negative = false) { if ($price) { if (!isset($this->_rounders[$type])) { $this->_rounders[$type] = Mage::getModel('core/calculator', $this->getStore()); } $price = $this->_rounders[$type]->deltaRound($price, $negative); } return $price; } /** * Get invoice items collection * * @return Mage_Sales_Model_Mysql4_Order_Invoice_Item_Collection */ public function getItemsCollection() { if (empty($this->_items)) { $this->_items = Mage::getResourceModel('sales/order_invoice_item_collection') ->setInvoiceFilter($this->getId()); if ($this->getId()) { foreach ($this->_items as $item) { $item->setInvoice($this); } } } return $this->_items; } public function getAllItems() { $items = array(); foreach ($this->getItemsCollection() as $item) { if (!$item->isDeleted()) { $items[] = $item; } } return $items; } public function getItemById($itemId) { foreach ($this->getItemsCollection() as $item) { if ($item->getId()==$itemId) { return $item; } } return false; } public function addItem(Mage_Sales_Model_Order_Invoice_Item $item) { $item->setInvoice($this) ->setParentId($this->getId()) ->setStoreId($this->getStoreId()); if (!$item->getId()) { $this->getItemsCollection()->addItem($item); } return $this; } /** * Retrieve invoice states array * * @return array */ public static function getStates() { if (is_null(self::$_states)) { self::$_states = array( self::STATE_OPEN => Mage::helper('sales')->__('Pending'), self::STATE_PAID => Mage::helper('sales')->__('Paid'), self::STATE_CANCELED => Mage::helper('sales')->__('Canceled'), ); } return self::$_states; } /** * Retrieve invoice state name by state identifier * * @param int $stateId * @return string */ public function getStateName($stateId = null) { if (is_null($stateId)) { $stateId = $this->getState(); } if (is_null(self::$_states)) { self::getStates(); } if (isset(self::$_states[$stateId])) { return self::$_states[$stateId]; } return Mage::helper('sales')->__('Unknown State'); } /** * Register invoice * * Apply to order, order items etc. * * @return unknown */ public function register() { if ($this->getId()) { Mage::throwException(Mage::helper('sales')->__('Cannot register existing invoice')); } foreach ($this->getAllItems() as $item) { if ($item->getQty()>0) { $item->register(); } else { $item->isDeleted(true); } } $order = $this->getOrder(); $captureCase = $this->getRequestedCaptureCase(); if ($this->canCapture()) { if ($captureCase) { if ($captureCase == self::CAPTURE_ONLINE) { $this->capture(); } elseif ($captureCase == self::CAPTURE_OFFLINE) { $this->setCanVoidFlag(false); $this->pay(); } } } elseif(!$order->getPayment()->getMethodInstance()->isGateway() || $captureCase == self::CAPTURE_OFFLINE) { if (!$order->getPayment()->getIsTransactionPending()) { $this->setCanVoidFlag(false); $this->pay(); } } $order->setTotalInvoiced($order->getTotalInvoiced() + $this->getGrandTotal()); $order->setBaseTotalInvoiced($order->getBaseTotalInvoiced() + $this->getBaseGrandTotal()); $order->setSubtotalInvoiced($order->getSubtotalInvoiced() + $this->getSubtotal()); $order->setBaseSubtotalInvoiced($order->getBaseSubtotalInvoiced() + $this->getBaseSubtotal()); $order->setTaxInvoiced($order->getTaxInvoiced() + $this->getTaxAmount()); $order->setBaseTaxInvoiced($order->getBaseTaxInvoiced() + $this->getBaseTaxAmount()); $order->setHiddenTaxInvoiced($order->getHiddenTaxInvoiced() + $this->getHiddenTaxAmount()); $order->setBaseHiddenTaxInvoiced($order->getBaseHiddenTaxInvoiced() + $this->getBaseHiddenTaxAmount()); $order->setShippingTaxInvoiced($order->getShippingTaxInvoiced() + $this->getShippingTaxAmount()); $order->setBaseShippingTaxInvoiced($order->getBaseShippingTaxInvoiced() + $this->getBaseShippingTaxAmount()); $order->setShippingInvoiced($order->getShippingInvoiced() + $this->getShippingAmount()); $order->setBaseShippingInvoiced($order->getBaseShippingInvoiced() + $this->getBaseShippingAmount()); $order->setDiscountInvoiced($order->getDiscountInvoiced() + $this->getDiscountAmount()); $order->setBaseDiscountInvoiced($order->getBaseDiscountInvoiced() + $this->getBaseDiscountAmount()); $order->setBaseTotalInvoicedCost($order->getBaseTotalInvoicedCost() + $this->getBaseCost()); $state = $this->getState(); if (is_null($state)) { $this->setState(self::STATE_OPEN); } Mage::dispatchEvent('sales_order_invoice_register', array($this->_eventObject=>$this, 'order' => $order)); return $this; } /** * Checking if the invoice is last * * @return bool */ public function isLast() { foreach ($this->getAllItems() as $item) { if (!$item->isLast()) { return false; } } return true; } /** * Adds comment to invoice with additional possibility to send it to customer via email * and show it in customer account * * @param bool $notify * @param bool $visibleOnFront * * @return Mage_Sales_Model_Order_Invoice */ public function addComment($comment, $notify=false, $visibleOnFront=false) { if (!($comment instanceof Mage_Sales_Model_Order_Invoice_Comment)) { $comment = Mage::getModel('sales/order_invoice_comment') ->setComment($comment) ->setIsCustomerNotified($notify) ->setIsVisibleOnFront($visibleOnFront); } $comment->setInvoice($this) ->setStoreId($this->getStoreId()) ->setParentId($this->getId()); if (!$comment->getId()) { $this->getCommentsCollection()->addItem($comment); } $this->_hasDataChanges = true; return $this; } public function getCommentsCollection($reload=false) { if (is_null($this->_comments) || $reload) { $this->_comments = Mage::getResourceModel('sales/order_invoice_comment_collection') ->setInvoiceFilter($this->getId()) ->setCreatedAtOrder(); /** * When invoice created with adding comment, comments collection * must be loaded before we added this comment. */ $this->_comments->load(); if ($this->getId()) { foreach ($this->_comments as $comment) { $comment->setInvoice($this); } } } return $this->_comments; } /** * Send email with invoice data * * @param boolean $notifyCustomer * @param string $comment * @return Mage_Sales_Model_Order_Invoice */ public function sendEmail($notifyCustomer = true, $comment = '') { $order = $this->getOrder(); $storeId = $order->getStore()->getId(); if (!Mage::helper('sales')->canSendNewInvoiceEmail($storeId)) { return $this; } // Get the destination email addresses to send copies to $copyTo = $this->_getEmails(self::XML_PATH_EMAIL_COPY_TO); $copyMethod = Mage::getStoreConfig(self::XML_PATH_EMAIL_COPY_METHOD, $storeId); // Check if at least one recepient is found if (!$notifyCustomer && !$copyTo) { return $this; } // Start store emulation process $appEmulation = Mage::getSingleton('core/app_emulation'); $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($storeId); try { // Retrieve specified view block from appropriate design package (depends on emulated store) $paymentBlock = Mage::helper('payment')->getInfoBlock($order->getPayment()) ->setIsSecureMode(true); $paymentBlock->getMethod()->setStore($storeId); $paymentBlockHtml = $paymentBlock->toHtml(); } catch (Exception $exception) { // Stop store emulation process $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo); throw $exception; } // Stop store emulation process $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo); // Retrieve corresponding email template id and customer name if ($order->getCustomerIsGuest()) { $templateId = Mage::getStoreConfig(self::XML_PATH_EMAIL_GUEST_TEMPLATE, $storeId); $customerName = $order->getBillingAddress()->getName(); } else { $templateId = Mage::getStoreConfig(self::XML_PATH_EMAIL_TEMPLATE, $storeId); $customerName = $order->getCustomerName(); } $mailer = Mage::getModel('core/email_template_mailer'); if ($notifyCustomer) { $emailInfo = Mage::getModel('core/email_info'); $emailInfo->addTo($order->getCustomerEmail(), $customerName); if ($copyTo && $copyMethod == 'bcc') { // Add bcc to customer email foreach ($copyTo as $email) { $emailInfo->addBcc($email); } } $mailer->addEmailInfo($emailInfo); } // Email copies are sent as separated emails if their copy method is 'copy' or a customer should not be notified if ($copyTo && ($copyMethod == 'copy' || !$notifyCustomer)) { foreach ($copyTo as $email) { $emailInfo = Mage::getModel('core/email_info'); $emailInfo->addTo($email); $mailer->addEmailInfo($emailInfo); } } // Set all required params and send emails $mailer->setSender(Mage::getStoreConfig(self::XML_PATH_EMAIL_IDENTITY, $storeId)); $mailer->setStoreId($storeId); $mailer->setTemplateId($templateId); $mailer->setTemplateParams(array( 'order' => $order, 'invoice' => $this, 'comment' => $comment, 'billing' => $order->getBillingAddress(), 'payment_html' => $paymentBlockHtml ) ); $mailer->send(); $this->setEmailSent(true); $this->_getResource()->saveAttribute($this, 'email_sent'); return $this; } /** * Send email with invoice update information * * @param boolean $notifyCustomer * @param string $comment * @return Mage_Sales_Model_Order_Invoice */ public function sendUpdateEmail($notifyCustomer = true, $comment = '') { $order = $this->getOrder(); $storeId = $order->getStore()->getId(); if (!Mage::helper('sales')->canSendInvoiceCommentEmail($storeId)) { return $this; } // Get the destination email addresses to send copies to $copyTo = $this->_getEmails(self::XML_PATH_UPDATE_EMAIL_COPY_TO); $copyMethod = Mage::getStoreConfig(self::XML_PATH_UPDATE_EMAIL_COPY_METHOD, $storeId); // Check if at least one recepient is found if (!$notifyCustomer && !$copyTo) { return $this; } // Retrieve corresponding email template id and customer name if ($order->getCustomerIsGuest()) { $templateId = Mage::getStoreConfig(self::XML_PATH_UPDATE_EMAIL_GUEST_TEMPLATE, $storeId); $customerName = $order->getBillingAddress()->getName(); } else { $templateId = Mage::getStoreConfig(self::XML_PATH_UPDATE_EMAIL_TEMPLATE, $storeId); $customerName = $order->getCustomerName(); } $mailer = Mage::getModel('core/email_template_mailer'); if ($notifyCustomer) { $emailInfo = Mage::getModel('core/email_info'); $emailInfo->addTo($order->getCustomerEmail(), $customerName); if ($copyTo && $copyMethod == 'bcc') { // Add bcc to customer email foreach ($copyTo as $email) { $emailInfo->addBcc($email); } } $mailer->addEmailInfo($emailInfo); } // Email copies are sent as separated emails if their copy method is 'copy' or a customer should not be notified if ($copyTo && ($copyMethod == 'copy' || !$notifyCustomer)) { foreach ($copyTo as $email) { $emailInfo = Mage::getModel('core/email_info'); $emailInfo->addTo($email); $mailer->addEmailInfo($emailInfo); } } // Set all required params and send emails $mailer->setSender(Mage::getStoreConfig(self::XML_PATH_UPDATE_EMAIL_IDENTITY, $storeId)); $mailer->setStoreId($storeId); $mailer->setTemplateId($templateId); $mailer->setTemplateParams(array( 'order' => $order, 'invoice' => $this, 'comment' => $comment, 'billing' => $order->getBillingAddress() ) ); $mailer->send(); return $this; } protected function _getEmails($configPath) { $data = Mage::getStoreConfig($configPath, $this->getStoreId()); if (!empty($data)) { return explode(',', $data); } return false; } protected function _beforeDelete() { $this->_protectFromNonAdmin(); return parent::_beforeDelete(); } /** * Reset invoice object * * @return Mage_Sales_Model_Order_Invoice */ public function reset() { $this->unsetData(); $this->_origData = null; $this->_items = null; $this->_comments = null; $this->_order = null; $this->_saveBeforeDestruct = false; $this->_wasPayCalled = false; return $this; } /** * Before object save manipulations * * @return Mage_Sales_Model_Order_Shipment */ protected function _beforeSave() { parent::_beforeSave(); if (!$this->getOrderId() && $this->getOrder()) { $this->setOrderId($this->getOrder()->getId()); $this->setBillingAddressId($this->getOrder()->getBillingAddress()->getId()); } return $this; } /** * After object save manipulation * * @return Mage_Sales_Model_Order_Shipment */ protected function _afterSave() { if (null !== $this->_items) { /** * Save invoice items */ foreach ($this->_items as $item) { $item->setOrderItem($item->getOrderItem()); $item->save(); } } if (null !== $this->_comments) { foreach($this->_comments as $comment) { $comment->save(); } } return parent::_afterSave(); } }