PageRenderTime 52ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/app/code/core/Mage/Sales/Model/Quote.php

https://bitbucket.org/claudiu_marginean/magento-hg-mirror
PHP | 1554 lines | 951 code | 164 blank | 439 comment | 172 complexity | fb44e1e2dd76fd3723efd7dfd456968f MD5 | raw file
Possible License(s): CC-BY-SA-3.0, LGPL-2.1, GPL-2.0, WTFPL
  1. <?php
  2. /**
  3. * Magento
  4. *
  5. * NOTICE OF LICENSE
  6. *
  7. * This source file is subject to the Open Software License (OSL 3.0)
  8. * that is bundled with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://opensource.org/licenses/osl-3.0.php
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@magentocommerce.com so we can send you a copy immediately.
  14. *
  15. * DISCLAIMER
  16. *
  17. * Do not edit or add to this file if you wish to upgrade Magento to newer
  18. * versions in the future. If you wish to customize Magento for your
  19. * needs please refer to http://www.magentocommerce.com for more information.
  20. *
  21. * @category Mage
  22. * @package Mage_Sales
  23. * @copyright Copyright (c) 2010 Magento Inc. (http://www.magentocommerce.com)
  24. * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
  25. */
  26. /**
  27. * Quote model
  28. *
  29. * Supported events:
  30. * sales_quote_load_after
  31. * sales_quote_save_before
  32. * sales_quote_save_after
  33. * sales_quote_delete_before
  34. * sales_quote_delete_after
  35. *
  36. * @author Magento Core Team <core@magentocommerce.com>
  37. */
  38. class Mage_Sales_Model_Quote extends Mage_Core_Model_Abstract
  39. {
  40. protected $_eventPrefix = 'sales_quote';
  41. protected $_eventObject = 'quote';
  42. /**
  43. * Quote customer model object
  44. *
  45. * @var Mage_Customer_Model_Customer
  46. */
  47. protected $_customer;
  48. /**
  49. * Quote addresses collection
  50. *
  51. * @var Mage_Eav_Model_Entity_Collection_Abstract
  52. */
  53. protected $_addresses = null;
  54. /**
  55. * Quote items collection
  56. *
  57. * @var Mage_Eav_Model_Entity_Collection_Abstract
  58. */
  59. protected $_items = null;
  60. /**
  61. * Quote payments
  62. *
  63. * @var Mage_Eav_Model_Entity_Collection_Abstract
  64. */
  65. protected $_payments = null;
  66. /**
  67. * Init resource model
  68. */
  69. protected function _construct()
  70. {
  71. $this->_init('sales/quote');
  72. }
  73. /**
  74. * Get quote store identifier
  75. *
  76. * @return int
  77. */
  78. public function getStoreId()
  79. {
  80. if (!$this->hasStoreId()) {
  81. return Mage::app()->getStore()->getId();
  82. }
  83. return $this->_getData('store_id');
  84. }
  85. /**
  86. * Get quote store model object
  87. *
  88. * @return Mage_Core_Model_Store
  89. */
  90. public function getStore()
  91. {
  92. return Mage::app()->getStore($this->getStoreId());
  93. }
  94. /**
  95. * Declare quote store model
  96. *
  97. * @param Mage_Core_Model_Store $store
  98. * @return Mage_Sales_Model_Quote
  99. */
  100. public function setStore(Mage_Core_Model_Store $store)
  101. {
  102. $this->setStoreId($store->getId());
  103. return $this;
  104. }
  105. /**
  106. * Get all available store ids for quote
  107. *
  108. * @return array
  109. */
  110. public function getSharedStoreIds()
  111. {
  112. $ids = $this->_getData('shared_store_ids');
  113. if (is_null($ids) || !is_array($ids)) {
  114. if ($website = $this->getWebsite()) {
  115. return $website->getStoreIds();
  116. }
  117. return $this->getStore()->getWebsite()->getStoreIds();
  118. }
  119. return $ids;
  120. }
  121. /**
  122. * Prepare data before save
  123. *
  124. * @return Mage_Sales_Model_Quote
  125. */
  126. protected function _beforeSave()
  127. {
  128. /**
  129. * Currency logic
  130. *
  131. * global - currency which is set for default in backend
  132. * base - currency which is set for current website. all attributes that
  133. * have 'base_' prefix saved in this currency
  134. * store - all the time it was currency of website and all attributes
  135. * with 'base_' were saved in this currency. From now on it is
  136. * deprecated and will be duplication of base currency code.
  137. * quote/order - currency which was selected by customer or configured by
  138. * admin for current store. currency in which customer sees
  139. * price thought all checkout.
  140. *
  141. * Rates:
  142. * store_to_base & store_to_quote/store_to_order - are deprecated
  143. * base_to_global & base_to_quote/base_to_order - must be used instead
  144. */
  145. $globalCurrencyCode = Mage::app()->getBaseCurrencyCode();
  146. $baseCurrency = $this->getStore()->getBaseCurrency();
  147. if ($this->hasForcedCurrency()){
  148. $quoteCurrency = $this->getForcedCurrency();
  149. } else {
  150. $quoteCurrency = $this->getStore()->getCurrentCurrency();
  151. }
  152. $this->setGlobalCurrencyCode($globalCurrencyCode);
  153. $this->setBaseCurrencyCode($baseCurrency->getCode());
  154. $this->setStoreCurrencyCode($baseCurrency->getCode());
  155. $this->setQuoteCurrencyCode($quoteCurrency->getCode());
  156. //deprecated, read above
  157. $this->setStoreToBaseRate($baseCurrency->getRate($globalCurrencyCode));
  158. $this->setStoreToQuoteRate($baseCurrency->getRate($quoteCurrency));
  159. $this->setBaseToGlobalRate($baseCurrency->getRate($globalCurrencyCode));
  160. $this->setBaseToQuoteRate($baseCurrency->getRate($quoteCurrency));
  161. if (!$this->hasChangedFlag() || $this->getChangedFlag() == true) {
  162. $this->setIsChanged(1);
  163. } else {
  164. $this->setIsChanged(0);
  165. }
  166. if ($this->_customer) {
  167. $this->setCustomerId($this->_customer->getId());
  168. }
  169. parent::_beforeSave();
  170. }
  171. /**
  172. * Save related items
  173. *
  174. * @return Mage_Sales_Model_Quote
  175. */
  176. protected function _afterSave()
  177. {
  178. parent::_afterSave();
  179. if (null !== $this->_addresses) {
  180. $this->getAddressesCollection()->save();
  181. }
  182. if (null !== $this->_items) {
  183. $this->getItemsCollection()->save();
  184. }
  185. if (null !== $this->_payments) {
  186. $this->getPaymentsCollection()->save();
  187. }
  188. return $this;
  189. }
  190. /**
  191. * Loading quote data by customer
  192. *
  193. * @return Mage_Sales_Model_Quote
  194. */
  195. public function loadByCustomer($customer)
  196. {
  197. if ($customer instanceof Mage_Customer_Model_Customer) {
  198. $customerId = $customer->getId();
  199. }
  200. else {
  201. $customerId = (int) $customer;
  202. }
  203. $this->_getResource()->loadByCustomerId($this, $customerId);
  204. $this->_afterLoad();
  205. return $this;
  206. }
  207. /**
  208. * Loading only active quote
  209. *
  210. * @param int $quoteId
  211. * @return Mage_Sales_Model_Quote
  212. */
  213. public function loadActive($quoteId)
  214. {
  215. $this->_getResource()->loadActive($this, $quoteId);
  216. $this->_afterLoad();
  217. return $this;
  218. }
  219. /**
  220. * Loading quote by identifier
  221. *
  222. * @param int $quoteId
  223. * @return Mage_Sales_Model_Quote
  224. */
  225. public function loadByIdWithoutStore($quoteId)
  226. {
  227. $this->_getResource()->loadByIdWithoutStore($this, $quoteId);
  228. $this->_afterLoad();
  229. return $this;
  230. }
  231. /**
  232. * Assign customer model object data to quote
  233. *
  234. * @param Mage_Customer_Model_Customer $customer
  235. * @return Mage_Sales_Model_Quote
  236. */
  237. public function assignCustomer(Mage_Customer_Model_Customer $customer)
  238. {
  239. return $this->assignCustomerWithAddressChange($customer);
  240. }
  241. /**
  242. * Assign customer model to quote with billing and shipping address change
  243. *
  244. * @param Mage_Customer_Model_Customer $customer
  245. * @param Mage_Sales_Model_Quote_Address $billingAddress
  246. * @param Mage_Sales_Model_Quote_Address $shippingAddress
  247. * @return Mage_Sales_Model_Quote
  248. */
  249. public function assignCustomerWithAddressChange(
  250. Mage_Customer_Model_Customer $customer,
  251. Mage_Sales_Model_Quote_Address $billingAddress = null,
  252. Mage_Sales_Model_Quote_Address $shippingAddress = null
  253. )
  254. {
  255. if ($customer->getId()) {
  256. $this->setCustomer($customer);
  257. if (!is_null($billingAddress)) {
  258. $this->setBillingAddress($billingAddress);
  259. } else {
  260. $defaultBillingAddress = $customer->getDefaultBillingAddress();
  261. if ($defaultBillingAddress && $defaultBillingAddress->getId()) {
  262. $billingAddress = Mage::getModel('sales/quote_address')
  263. ->importCustomerAddress($defaultBillingAddress);
  264. $this->setBillingAddress($billingAddress);
  265. }
  266. }
  267. if (is_null($shippingAddress)) {
  268. $defaultShippingAddress = $customer->getDefaultShippingAddress();
  269. if ($defaultShippingAddress && $defaultShippingAddress->getId()) {
  270. $shippingAddress = Mage::getModel('sales/quote_address')
  271. ->importCustomerAddress($defaultShippingAddress);
  272. } else {
  273. $shippingAddress = Mage::getModel('sales/quote_address');
  274. }
  275. }
  276. $this->setShippingAddress($shippingAddress);
  277. }
  278. return $this;
  279. }
  280. /**
  281. * Define customer object
  282. *
  283. * @param Mage_Customer_Model_Customer $customer
  284. * @return Mage_Sales_Model_Quote
  285. */
  286. public function setCustomer(Mage_Customer_Model_Customer $customer)
  287. {
  288. $this->_customer = $customer;
  289. $this->setCustomerId($customer->getId());
  290. Mage::helper('core')->copyFieldset('customer_account', 'to_quote', $customer, $this);
  291. return $this;
  292. }
  293. /**
  294. * Retrieve customer model object
  295. *
  296. * @return Mage_Customer_Model_Customer
  297. */
  298. public function getCustomer()
  299. {
  300. if (is_null($this->_customer)) {
  301. $this->_customer = Mage::getModel('customer/customer');
  302. if ($customerId = $this->getCustomerId()) {
  303. $this->_customer->load($customerId);
  304. if (!$this->_customer->getId()) {
  305. $this->_customer->setCustomerId(null);
  306. }
  307. }
  308. }
  309. return $this->_customer;
  310. }
  311. /**
  312. * Retrieve customer group id
  313. *
  314. * @return int
  315. */
  316. public function getCustomerGroupId()
  317. {
  318. if ($this->getCustomerId()) {
  319. return ($this->getData('customer_group_id')) ? $this->getData('customer_group_id')
  320. : $this->getCustomer()->getGroupId();
  321. } else {
  322. return Mage_Customer_Model_Group::NOT_LOGGED_IN_ID;
  323. }
  324. }
  325. public function getCustomerTaxClassId()
  326. {
  327. /*
  328. * tax class can vary at any time. so instead of using the value from session,
  329. * we need to retrieve from db everytime to get the correct tax class
  330. */
  331. //if (!$this->getData('customer_group_id') && !$this->getData('customer_tax_class_id')) {
  332. $classId = Mage::getModel('customer/group')->getTaxClassId($this->getCustomerGroupId());
  333. $this->setCustomerTaxClassId($classId);
  334. //}
  335. return $this->getData('customer_tax_class_id');
  336. }
  337. /**
  338. * Retrieve quote address collection
  339. *
  340. * @return Mage_Eav_Model_Entity_Collection_Abstract
  341. */
  342. public function getAddressesCollection()
  343. {
  344. if (is_null($this->_addresses)) {
  345. $this->_addresses = Mage::getModel('sales/quote_address')->getCollection()
  346. ->setQuoteFilter($this->getId());
  347. if ($this->getId()) {
  348. foreach ($this->_addresses as $address) {
  349. $address->setQuote($this);
  350. }
  351. }
  352. }
  353. return $this->_addresses;
  354. }
  355. /**
  356. * Retrieve quote address by type
  357. *
  358. * @param string $type
  359. * @return Mage_Sales_Model_Quote_Address
  360. */
  361. protected function _getAddressByType($type)
  362. {
  363. foreach ($this->getAddressesCollection() as $address) {
  364. if ($address->getAddressType() == $type && !$address->isDeleted()) {
  365. return $address;
  366. }
  367. }
  368. $address = Mage::getModel('sales/quote_address')->setAddressType($type);
  369. $this->addAddress($address);
  370. return $address;
  371. }
  372. /**
  373. * Retrieve quote billing address
  374. *
  375. * @return Mage_Sales_Model_Quote_Address
  376. */
  377. public function getBillingAddress()
  378. {
  379. return $this->_getAddressByType(Mage_Sales_Model_Quote_Address::TYPE_BILLING);
  380. }
  381. /**
  382. * retrieve quote shipping address
  383. *
  384. * @return Mage_Sales_Model_Quote_Address
  385. */
  386. public function getShippingAddress()
  387. {
  388. return $this->_getAddressByType(Mage_Sales_Model_Quote_Address::TYPE_SHIPPING);
  389. }
  390. public function getAllShippingAddresses()
  391. {
  392. $addresses = array();
  393. foreach ($this->getAddressesCollection() as $address) {
  394. if ($address->getAddressType()==Mage_Sales_Model_Quote_Address::TYPE_SHIPPING
  395. && !$address->isDeleted()) {
  396. $addresses[] = $address;
  397. }
  398. }
  399. return $addresses;
  400. }
  401. public function getAllAddresses()
  402. {
  403. $addresses = array();
  404. foreach ($this->getAddressesCollection() as $address) {
  405. if (!$address->isDeleted()) {
  406. $addresses[] = $address;
  407. }
  408. }
  409. return $addresses;
  410. }
  411. /**
  412. *
  413. * @param int $addressId
  414. * @return Mage_Sales_Model_Quote_Address
  415. */
  416. public function getAddressById($addressId)
  417. {
  418. foreach ($this->getAddressesCollection() as $address) {
  419. if ($address->getId()==$addressId) {
  420. return $address;
  421. }
  422. }
  423. return false;
  424. }
  425. public function getAddressByCustomerAddressId($addressId)
  426. {
  427. foreach ($this->getAddressesCollection() as $address) {
  428. if (!$address->isDeleted() && $address->getCustomerAddressId()==$addressId) {
  429. return $address;
  430. }
  431. }
  432. return false;
  433. }
  434. public function getShippingAddressByCustomerAddressId($addressId)
  435. {
  436. foreach ($this->getAddressesCollection() as $address) {
  437. if (!$address->isDeleted() && $address->getAddressType()==Mage_Sales_Model_Quote_Address::TYPE_SHIPPING
  438. && $address->getCustomerAddressId()==$addressId) {
  439. return $address;
  440. }
  441. }
  442. return false;
  443. }
  444. public function removeAddress($addressId)
  445. {
  446. foreach ($this->getAddressesCollection() as $address) {
  447. if ($address->getId()==$addressId) {
  448. $address->isDeleted(true);
  449. break;
  450. }
  451. }
  452. return $this;
  453. }
  454. public function removeAllAddresses()
  455. {
  456. foreach ($this->getAddressesCollection() as $address) {
  457. $address->isDeleted(true);
  458. }
  459. return $this;
  460. }
  461. public function addAddress(Mage_Sales_Model_Quote_Address $address)
  462. {
  463. $address->setQuote($this);
  464. if (!$address->getId()) {
  465. $this->getAddressesCollection()->addItem($address);
  466. }
  467. return $this;
  468. }
  469. /**
  470. * Enter description here...
  471. *
  472. * @param Mage_Sales_Model_Quote_Address $address
  473. * @return Mage_Sales_Model_Quote
  474. */
  475. public function setBillingAddress(Mage_Sales_Model_Quote_Address $address)
  476. {
  477. $old = $this->getBillingAddress();
  478. if (!empty($old)) {
  479. $old->addData($address->getData());
  480. } else {
  481. $this->addAddress($address->setAddressType(Mage_Sales_Model_Quote_Address::TYPE_BILLING));
  482. }
  483. return $this;
  484. }
  485. /**
  486. * Enter description here...
  487. *
  488. * @param Mage_Sales_Model_Quote_Address $address
  489. * @return Mage_Sales_Model_Quote
  490. */
  491. public function setShippingAddress(Mage_Sales_Model_Quote_Address $address)
  492. {
  493. if ($this->getIsMultiShipping()) {
  494. $this->addAddress($address->setAddressType(Mage_Sales_Model_Quote_Address::TYPE_SHIPPING));
  495. }
  496. else {
  497. $old = $this->getShippingAddress();
  498. if (!empty($old)) {
  499. $old->addData($address->getData());
  500. } else {
  501. $this->addAddress($address->setAddressType(Mage_Sales_Model_Quote_Address::TYPE_SHIPPING));
  502. }
  503. }
  504. return $this;
  505. }
  506. public function addShippingAddress(Mage_Sales_Model_Quote_Address $address)
  507. {
  508. $this->setShippingAddress($address);
  509. return $this;
  510. }
  511. /**
  512. * Retrieve quote items collection
  513. *
  514. * @param bool $loaded
  515. * @return Mage_Eav_Model_Entity_Collection_Abstract
  516. */
  517. public function getItemsCollection($useCache = true)
  518. {
  519. if (is_null($this->_items)) {
  520. $this->_items = Mage::getModel('sales/quote_item')->getCollection();
  521. $this->_items->setQuote($this);
  522. }
  523. return $this->_items;
  524. }
  525. /**
  526. * Retrieve quote items array
  527. *
  528. * @return array
  529. */
  530. public function getAllItems()
  531. {
  532. $items = array();
  533. foreach ($this->getItemsCollection() as $item) {
  534. if (!$item->isDeleted()) {
  535. $items[] = $item;
  536. }
  537. }
  538. return $items;
  539. }
  540. /**
  541. * Get array of all items what can be display directly
  542. *
  543. * @return array
  544. */
  545. public function getAllVisibleItems()
  546. {
  547. $items = array();
  548. foreach ($this->getItemsCollection() as $item) {
  549. if (!$item->isDeleted() && !$item->getParentItemId()) {
  550. $items[] = $item;
  551. }
  552. }
  553. return $items;
  554. }
  555. /**
  556. * Checking items availability
  557. *
  558. * @return bool
  559. */
  560. public function hasItems()
  561. {
  562. return sizeof($this->getAllItems())>0;
  563. }
  564. /**
  565. * Checking availability of items with decimal qty
  566. *
  567. * @return bool
  568. */
  569. public function hasItemsWithDecimalQty()
  570. {
  571. foreach ($this->getAllItems() as $item) {
  572. if ($item->getProduct()->getStockItem()
  573. && $item->getProduct()->getStockItem()->getIsQtyDecimal()) {
  574. return true;
  575. }
  576. }
  577. return false;
  578. }
  579. /**
  580. * Checking product exist in Quote
  581. *
  582. * @param int $productId
  583. * @return bool
  584. */
  585. public function hasProductId($productId)
  586. {
  587. foreach ($this->getAllItems() as $item) {
  588. if ($item->getProductId() == $productId) {
  589. return true;
  590. }
  591. }
  592. return false;
  593. }
  594. /**
  595. * Retrieve item model object by item identifier
  596. *
  597. * @param int $itemId
  598. * @return Mage_Sales_Model_Quote_Item
  599. */
  600. public function getItemById($itemId)
  601. {
  602. return $this->getItemsCollection()->getItemById($itemId);
  603. }
  604. /**
  605. * Remove quote item by item identifier
  606. *
  607. * @param int $itemId
  608. * @return Mage_Sales_Model_Quote
  609. */
  610. public function removeItem($itemId)
  611. {
  612. $item = $this->getItemById($itemId);
  613. if ($item) {
  614. $item->setQuote($this);
  615. /**
  616. * If we remove item from quote - we can't use multishipping mode
  617. */
  618. $this->setIsMultiShipping(false);
  619. $item->isDeleted(true);
  620. if ($item->getHasChildren()) {
  621. foreach ($item->getChildren() as $child) {
  622. $child->isDeleted(true);
  623. }
  624. }
  625. $parent = $item->getParentItem();
  626. if ($parent) {
  627. $parent->isDeleted(true);
  628. }
  629. Mage::dispatchEvent('sales_quote_remove_item', array('quote_item' => $item));
  630. }
  631. return $this;
  632. }
  633. /**
  634. * Adding new item to quote
  635. *
  636. * @param Mage_Sales_Model_Quote_Item $item
  637. * @return Mage_Sales_Model_Quote
  638. */
  639. public function addItem(Mage_Sales_Model_Quote_Item $item)
  640. {
  641. /**
  642. * Temporary workaround for purchase process: it is too dangerous to purchase more than one nominal item
  643. * or a mixture of nominal and non-nominal items, although technically possible.
  644. *
  645. * The problem is that currently it is implemented as sequential submission of nominal items and order, by one click.
  646. * It makes logically impossible to make the process of the purchase failsafe.
  647. * Proper solution is to submit items one by one with customer confirmation each time.
  648. */
  649. if ($item->isNominal() && $this->hasItems() || $this->hasNominalItems()) {
  650. Mage::throwException(
  651. Mage::helper('sales')->__('Nominal item can be purchased standalone only. To proceed please remove other items from the quote.')
  652. );
  653. }
  654. $item->setQuote($this);
  655. if (!$item->getId()) {
  656. $this->getItemsCollection()->addItem($item);
  657. Mage::dispatchEvent('sales_quote_add_item', array('quote_item' => $item));
  658. }
  659. return $this;
  660. }
  661. /**
  662. * Advanced func to add product to quote - processing mode can be specified there.
  663. * Returns error message if product type instance can't prepare product.
  664. *
  665. * @param mixed $product
  666. * @param null|float|Varien_Object $request
  667. * @param null|string $processMode
  668. * @return Mage_Sales_Model_Quote_Item|string
  669. */
  670. public function addProductAdvanced(Mage_Catalog_Model_Product $product, $request = null, $processMode = null)
  671. {
  672. if ($request === null) {
  673. $request = 1;
  674. }
  675. if (is_numeric($request)) {
  676. $request = new Varien_Object(array('qty'=>$request));
  677. }
  678. if (!($request instanceof Varien_Object)) {
  679. Mage::throwException(Mage::helper('sales')->__('Invalid request for adding product to quote.'));
  680. }
  681. $cartCandidates = $product->getTypeInstance(true)
  682. ->prepareForCartAdvanced($request, $product, $processMode);
  683. /**
  684. * Error message
  685. */
  686. if (is_string($cartCandidates)) {
  687. return $cartCandidates;
  688. }
  689. /**
  690. * If prepare process return one object
  691. */
  692. if (!is_array($cartCandidates)) {
  693. $cartCandidates = array($cartCandidates);
  694. }
  695. $parentItem = null;
  696. $errors = array();
  697. $items = array();
  698. foreach ($cartCandidates as $candidate) {
  699. // Child items can be sticked together only within their parent
  700. $stickWithinParent = $candidate->getParentProductId() ? $parentItem : null;
  701. $candidate->setStickWithinParent($stickWithinParent);
  702. $item = $this->_addCatalogProduct($candidate, $candidate->getCartQty());
  703. $items[] = $item;
  704. /**
  705. * As parent item we should always use the item of first added product
  706. */
  707. if (!$parentItem) {
  708. $parentItem = $item;
  709. }
  710. if ($parentItem && $candidate->getParentProductId() && !$item->getId()) {
  711. $item->setParentItem($parentItem);
  712. }
  713. /**
  714. * We specify qty after we know about parent (for stock)
  715. */
  716. $item->addQty($candidate->getCartQty());
  717. // collect errors instead of throwing first one
  718. if ($item->getHasError()) {
  719. $errors[] = $item->getMessage();
  720. }
  721. }
  722. if (!empty($errors)) {
  723. Mage::throwException(implode("\n", $errors));
  724. }
  725. Mage::dispatchEvent('sales_quote_product_add_after', array('items' => $items));
  726. return $item;
  727. }
  728. /**
  729. * Add product to quote
  730. *
  731. * return error message if product type instance can't prepare product
  732. *
  733. * @param mixed $product
  734. * @param null|float|Varien_Object $request
  735. * @return Mage_Sales_Model_Quote_Item|string
  736. */
  737. public function addProduct(Mage_Catalog_Model_Product $product, $request = null)
  738. {
  739. return $this->addProductAdvanced(
  740. $product,
  741. $request,
  742. Mage_Catalog_Model_Product_Type_Abstract::PROCESS_MODE_FULL
  743. );
  744. }
  745. /**
  746. * Adding catalog product object data to quote
  747. *
  748. * @param Mage_Catalog_Model_Product $product
  749. * @return Mage_Sales_Model_Quote_Item
  750. */
  751. protected function _addCatalogProduct(Mage_Catalog_Model_Product $product, $qty = 1)
  752. {
  753. $newItem = false;
  754. $item = $this->getItemByProduct($product);
  755. if (!$item) {
  756. $item = Mage::getModel('sales/quote_item');
  757. $item->setQuote($this);
  758. if (Mage::app()->getStore()->isAdmin()) {
  759. $item->setStoreId($this->getStore()->getId());
  760. }
  761. else {
  762. $item->setStoreId(Mage::app()->getStore()->getId());
  763. }
  764. $newItem = true;
  765. }
  766. /**
  767. * We can't modify existing child items
  768. */
  769. if ($item->getId() && $product->getParentProductId()) {
  770. return $item;
  771. }
  772. $item->setOptions($product->getCustomOptions())
  773. ->setProduct($product);
  774. // Add only item that is not in quote already (there can be other new or already saved item
  775. if ($newItem) {
  776. $this->addItem($item);
  777. }
  778. return $item;
  779. }
  780. /**
  781. * Updates quote item with new configuration
  782. *
  783. * $params sets how current item configuration must be taken into account and additional options.
  784. * It's passed to Mage_Catalog_Helper_Product->addParamsToBuyRequest() to compose resulting buyRequest.
  785. *
  786. * Basically it can hold
  787. * - 'current_config', Varien_Object or array - current buyRequest that configures product in this item,
  788. * used to restore currently attached files
  789. * - 'files_prefix': string[a-z0-9_] - prefix that was added at frontend to names of file options (file inputs), so they won't
  790. * intersect with other submitted options
  791. *
  792. * For more options see Mage_Catalog_Helper_Product->addParamsToBuyRequest()
  793. *
  794. * @param int $itemId
  795. * @param Varien_Object $buyRequest
  796. * @param null|array|Varien_Object $params
  797. * @return Mage_Sales_Model_Quote_Item
  798. *
  799. * @see Mage_Catalog_Helper_Product::addParamsToBuyRequest()
  800. */
  801. public function updateItem($itemId, $buyRequest, $params = null)
  802. {
  803. $item = $this->getItemById($itemId);
  804. if (!$item) {
  805. Mage::throwException(Mage::helper('sales')->__('Wrong quote item id to update configuration.'));
  806. }
  807. $productId = $item->getProduct()->getId();
  808. //We need to create new clear product instance with same $productId
  809. //to set new option values from $buyRequest
  810. $product = Mage::getModel('catalog/product')
  811. ->setStoreId($this->getStore()->getId())
  812. ->load($productId);
  813. if (!$params) {
  814. $params = new Varien_Object();
  815. } else if (is_array($params)) {
  816. $params = new Varien_Object($params);
  817. }
  818. $params->setCurrentConfig($item->getBuyRequest());
  819. $buyRequest = Mage::helper('catalog/product')->addParamsToBuyRequest($buyRequest, $params);
  820. $resultItem = $this->addProduct($product, $buyRequest);
  821. if (is_string($resultItem)) {
  822. Mage::throwException($resultItem);
  823. }
  824. if ($resultItem->getParentItem()) {
  825. $resultItem = $resultItem->getParentItem();
  826. }
  827. if ($resultItem->getId() != $itemId) {
  828. /*
  829. * Product configuration didn't stick to original quote item
  830. * It either has same configuration as some other quote item's product or completely new configuration
  831. */
  832. $this->removeItem($itemId);
  833. $items = $this->getAllItems();
  834. foreach ($items as $item) {
  835. if (($item->getProductId() == $productId) && ($item->getId() != $resultItem->getId())) {
  836. if ($resultItem->compare($item)) {
  837. // Product configuration is same as in other quote item
  838. $resultItem->setQty($resultItem->getQty() + $item->getQty());
  839. $this->removeItem($item->getId());
  840. break;
  841. }
  842. }
  843. }
  844. } else {
  845. $resultItem->setQty($buyRequest->getQty());
  846. }
  847. return $resultItem;
  848. }
  849. /**
  850. * Retrieve quote item by product id
  851. *
  852. * @param Mage_Catalog_Model_Product $product
  853. * @return Mage_Sales_Model_Quote_Item || false
  854. */
  855. public function getItemByProduct($product)
  856. {
  857. foreach ($this->getAllItems() as $item) {
  858. if ($item->representProduct($product)) {
  859. return $item;
  860. }
  861. }
  862. return false;
  863. }
  864. public function getItemsSummaryQty()
  865. {
  866. $qty = $this->getData('all_items_qty');
  867. if (is_null($qty)) {
  868. $qty = 0;
  869. foreach ($this->getAllItems() as $item) {
  870. if ($item->getParentItem()) {
  871. continue;
  872. }
  873. if (($children = $item->getChildren()) && $item->isShipSeparately()) {
  874. foreach ($children as $child) {
  875. $qty+= $child->getQty()*$item->getQty();
  876. }
  877. } else {
  878. $qty+= $item->getQty();
  879. }
  880. }
  881. $this->setData('all_items_qty', $qty);
  882. }
  883. return $qty;
  884. }
  885. public function getItemVirtualQty()
  886. {
  887. $qty = $this->getData('virtual_items_qty');
  888. if (is_null($qty)) {
  889. $qty = 0;
  890. foreach ($this->getAllItems() as $item) {
  891. if ($item->getParentItem()) {
  892. continue;
  893. }
  894. if (($children = $item->getChildren()) && $item->isShipSeparately()) {
  895. foreach ($children as $child) {
  896. if ($child->getProduct()->getIsVirtual()) {
  897. $qty+= $child->getQty();
  898. }
  899. }
  900. } else {
  901. if ($item->getProduct()->getIsVirtual()) {
  902. $qty+= $item->getQty();
  903. }
  904. }
  905. }
  906. $this->setData('virtual_items_qty', $qty);
  907. }
  908. return $qty;
  909. }
  910. /*********************** PAYMENTS ***************************/
  911. public function getPaymentsCollection()
  912. {
  913. if (is_null($this->_payments)) {
  914. $this->_payments = Mage::getModel('sales/quote_payment')->getCollection()
  915. ->setQuoteFilter($this->getId());
  916. if ($this->getId()) {
  917. foreach ($this->_payments as $payment) {
  918. $payment->setQuote($this);
  919. }
  920. }
  921. }
  922. return $this->_payments;
  923. }
  924. /**
  925. * @return Mage_Sales_Model_Quote_Payment
  926. */
  927. public function getPayment()
  928. {
  929. foreach ($this->getPaymentsCollection() as $payment) {
  930. if (!$payment->isDeleted()) {
  931. return $payment;
  932. }
  933. }
  934. $payment = Mage::getModel('sales/quote_payment');
  935. $this->addPayment($payment);
  936. return $payment;
  937. }
  938. public function getPaymentById($paymentId)
  939. {
  940. foreach ($this->getPaymentsCollection() as $payment) {
  941. if ($payment->getId()==$paymentId) {
  942. return $payment;
  943. }
  944. }
  945. return false;
  946. }
  947. public function addPayment(Mage_Sales_Model_Quote_Payment $payment)
  948. {
  949. $payment->setQuote($this);
  950. if (!$payment->getId()) {
  951. $this->getPaymentsCollection()->addItem($payment);
  952. }
  953. return $this;
  954. }
  955. public function setPayment(Mage_Sales_Model_Quote_Payment $payment)
  956. {
  957. if (!$this->getIsMultiPayment() && ($old = $this->getPayment())) {
  958. $payment->setId($old->getId());
  959. }
  960. $this->addPayment($payment);
  961. return $payment;
  962. }
  963. public function removePayment()
  964. {
  965. $this->getPayment()->isDeleted(true);
  966. return $this;
  967. }
  968. /**
  969. * Collect totals
  970. *
  971. * @return Mage_Sales_Model_Quote
  972. */
  973. public function collectTotals()
  974. {
  975. /**
  976. * Protect double totals collection
  977. */
  978. if ($this->getTotalsCollectedFlag()) {
  979. return $this;
  980. }
  981. Mage::dispatchEvent(
  982. $this->_eventPrefix . '_collect_totals_before',
  983. array(
  984. $this->_eventObject=>$this
  985. )
  986. );
  987. $this->setSubtotal(0);
  988. $this->setBaseSubtotal(0);
  989. $this->setSubtotalWithDiscount(0);
  990. $this->setBaseSubtotalWithDiscount(0);
  991. $this->setGrandTotal(0);
  992. $this->setBaseGrandTotal(0);
  993. foreach ($this->getAllAddresses() as $address) {
  994. $address->setSubtotal(0);
  995. $address->setBaseSubtotal(0);
  996. $address->setGrandTotal(0);
  997. $address->setBaseGrandTotal(0);
  998. $address->collectTotals();
  999. $this->setSubtotal((float) $this->getSubtotal() + $address->getSubtotal());
  1000. $this->setBaseSubtotal((float) $this->getBaseSubtotal() + $address->getBaseSubtotal());
  1001. $this->setSubtotalWithDiscount(
  1002. (float) $this->getSubtotalWithDiscount() + $address->getSubtotalWithDiscount()
  1003. );
  1004. $this->setBaseSubtotalWithDiscount(
  1005. (float) $this->getBaseSubtotalWithDiscount() + $address->getBaseSubtotalWithDiscount()
  1006. );
  1007. $this->setGrandTotal((float) $this->getGrandTotal() + $address->getGrandTotal());
  1008. $this->setBaseGrandTotal((float) $this->getBaseGrandTotal() + $address->getBaseGrandTotal());
  1009. }
  1010. Mage::helper('sales')->checkQuoteAmount($this, $this->getGrandTotal());
  1011. Mage::helper('sales')->checkQuoteAmount($this, $this->getBaseGrandTotal());
  1012. $this->setItemsCount(0);
  1013. $this->setItemsQty(0);
  1014. $this->setVirtualItemsQty(0);
  1015. foreach ($this->getAllVisibleItems() as $item) {
  1016. if ($item->getParentItem()) {
  1017. continue;
  1018. }
  1019. $children = $item->getChildren();
  1020. if ($children && $item->isShipSeparately()) {
  1021. foreach ($children as $child) {
  1022. if ($child->getProduct()->getIsVirtual()) {
  1023. $this->setVirtualItemsQty($this->getVirtualItemsQty() + $child->getQty()*$item->getQty());
  1024. }
  1025. }
  1026. }
  1027. if ($item->getProduct()->getIsVirtual()) {
  1028. $this->setVirtualItemsQty($this->getVirtualItemsQty() + $item->getQty());
  1029. }
  1030. $this->setItemsCount($this->getItemsCount()+1);
  1031. $this->setItemsQty((float) $this->getItemsQty()+$item->getQty());
  1032. }
  1033. $this->setData('trigger_recollect', 0);
  1034. $this->_validateCouponCode();
  1035. Mage::dispatchEvent(
  1036. $this->_eventPrefix . '_collect_totals_after',
  1037. array($this->_eventObject => $this)
  1038. );
  1039. $this->setTotalsCollectedFlag(true);
  1040. return $this;
  1041. }
  1042. /**
  1043. * Get all quote totals (sorted by priority)
  1044. * Method process quote states isVirtual and isMultiShipping
  1045. *
  1046. * @return array
  1047. */
  1048. public function getTotals()
  1049. {
  1050. /**
  1051. * If quote is virtual we are using totals of billing address because
  1052. * all items assigned to it
  1053. */
  1054. if ($this->isVirtual()) {
  1055. return $this->getBillingAddress()->getTotals();
  1056. }
  1057. $shippingAddress = $this->getShippingAddress();
  1058. $totals = $shippingAddress->getTotals();
  1059. // Going through all quote addresses and merge their totals
  1060. foreach ($this->getAddressesCollection() as $address) {
  1061. if ($address->isDeleted() || $address === $shippingAddress) {
  1062. continue;
  1063. }
  1064. foreach ($address->getTotals() as $code => $total) {
  1065. if (isset($totals[$code])) {
  1066. $totals[$code]->merge($total);
  1067. } else {
  1068. $totals[$code] = $total;
  1069. }
  1070. }
  1071. }
  1072. $sortedTotals = array();
  1073. foreach ($this->getBillingAddress()->getTotalModels() as $total) {
  1074. /* @var $total Mage_Sales_Model_Quote_Address_Total_Abstract */
  1075. if (isset($totals[$total->getCode()])) {
  1076. $sortedTotals[$total->getCode()] = $totals[$total->getCode()];
  1077. }
  1078. }
  1079. return $sortedTotals;
  1080. }
  1081. public function addMessage($message, $index='error')
  1082. {
  1083. $messages = $this->getData('messages');
  1084. if (is_null($messages)) {
  1085. $messages = array();
  1086. }
  1087. if (isset($messages[$index])) {
  1088. return $this;
  1089. }
  1090. if (is_string($message)) {
  1091. $message = Mage::getSingleton('core/message')->error($message);
  1092. }
  1093. $messages[$index] = $message;
  1094. $this->setData('messages', $messages);
  1095. return $this;
  1096. }
  1097. public function getMessages()
  1098. {
  1099. $messages = $this->getData('messages');
  1100. if (is_null($messages)) {
  1101. $messages = array();
  1102. $this->setData('messages', $messages);
  1103. }
  1104. return $messages;
  1105. }
  1106. /**
  1107. * Generate new increment order id and associate it with current quote
  1108. *
  1109. * @return Mage_Sales_Model_Quote
  1110. */
  1111. public function reserveOrderId()
  1112. {
  1113. if (!$this->getReservedOrderId()) {
  1114. $this->setReservedOrderId($this->_getResource()->getReservedOrderId($this));
  1115. } else {
  1116. //checking if reserved order id was already used for some order
  1117. //if yes reserving new one if not using old one
  1118. if ($this->_getResource()->isOrderIncrementIdUsed($this->getReservedOrderId())) {
  1119. $this->setReservedOrderId($this->_getResource()->getReservedOrderId($this));
  1120. }
  1121. }
  1122. return $this;
  1123. }
  1124. public function validateMinimumAmount($multishipping = false)
  1125. {
  1126. $storeId = $this->getStoreId();
  1127. $minOrderActive = Mage::getStoreConfigFlag('sales/minimum_order/active', $storeId);
  1128. $minOrderMulti = Mage::getStoreConfigFlag('sales/minimum_order/multi_address', $storeId);
  1129. if (!$minOrderActive) {
  1130. return true;
  1131. }
  1132. if ($multishipping) {
  1133. if ($minOrderMulti) {
  1134. $baseTotal = 0;
  1135. foreach ($this->getAllAddresses() as $address) {
  1136. /* @var $address Mage_Sales_Model_Quote_Address */
  1137. $baseTotal += $address->getBaseSubtotalWithDiscount();
  1138. }
  1139. if ($baseTotal < Mage::getStoreConfig('sales/minimum_order/amount', $storeId)) {
  1140. return false;
  1141. }
  1142. }
  1143. } else {
  1144. foreach ($this->getAllAddresses() as $address) {
  1145. /* @var $address Mage_Sales_Model_Quote_Address */
  1146. if (!$address->validateMinimumAmount()) {
  1147. return false;
  1148. }
  1149. }
  1150. }
  1151. return true;
  1152. }
  1153. /**
  1154. * Check quote for virtual product only
  1155. *
  1156. * @return bool
  1157. */
  1158. public function isVirtual()
  1159. {
  1160. $isVirtual = true;
  1161. $countItems = 0;
  1162. foreach ($this->getItemsCollection() as $_item) {
  1163. /* @var $_item Mage_Sales_Model_Quote_Item */
  1164. if ($_item->isDeleted() || $_item->getParentItemId()) {
  1165. continue;
  1166. }
  1167. $countItems ++;
  1168. if (!$_item->getProduct()->getIsVirtual()) {
  1169. $isVirtual = false;
  1170. break;
  1171. }
  1172. }
  1173. return $countItems == 0 ? false : $isVirtual;
  1174. }
  1175. /**
  1176. * Check quote for virtual product only
  1177. *
  1178. * @return bool
  1179. */
  1180. public function getIsVirtual()
  1181. {
  1182. return intval($this->isVirtual());
  1183. }
  1184. /**
  1185. * Has a virtual products on quote
  1186. *
  1187. * @return bool
  1188. */
  1189. public function hasVirtualItems()
  1190. {
  1191. $hasVirtual = false;
  1192. foreach ($this->getItemsCollection() as $_item) {
  1193. if ($_item->getParentItemId()) {
  1194. continue;
  1195. }
  1196. if ($_item->getProduct()->isVirtual()) {
  1197. $hasVirtual = true;
  1198. }
  1199. }
  1200. return $hasVirtual;
  1201. }
  1202. /**
  1203. * Merge quotes
  1204. *
  1205. * @param Mage_Sales_Model_Quote $quote
  1206. * @return Mage_Sales_Model_Quote
  1207. */
  1208. public function merge(Mage_Sales_Model_Quote $quote)
  1209. {
  1210. Mage::dispatchEvent(
  1211. $this->_eventPrefix . '_merge_before',
  1212. array(
  1213. $this->_eventObject=>$this,
  1214. 'source'=>$quote
  1215. )
  1216. );
  1217. foreach ($quote->getAllVisibleItems() as $item) {
  1218. $found = false;
  1219. foreach ($this->getAllItems() as $quoteItem) {
  1220. if ($quoteItem->compare($item)) {
  1221. $quoteItem->setQty($quoteItem->getQty() + $item->getQty());
  1222. $found = true;
  1223. break;
  1224. }
  1225. }
  1226. if (!$found) {
  1227. $newItem = clone $item;
  1228. $this->addItem($newItem);
  1229. if ($item->getHasChildren()) {
  1230. foreach ($item->getChildren() as $child) {
  1231. $newChild = clone $child;
  1232. $newChild->setParentItem($newItem);
  1233. $this->addItem($newChild);
  1234. }
  1235. }
  1236. }
  1237. }
  1238. /**
  1239. * Init shipping and billing address if quote is new
  1240. */
  1241. if (!$this->getId()) {
  1242. $this->getShippingAddress();
  1243. $this->getBillingAddress();
  1244. }
  1245. if ($quote->getCouponCode()) {
  1246. $this->setCouponCode($quote->getCouponCode());
  1247. }
  1248. Mage::dispatchEvent(
  1249. $this->_eventPrefix . '_merge_after',
  1250. array(
  1251. $this->_eventObject=>$this,
  1252. 'source'=>$quote
  1253. )
  1254. );
  1255. return $this;
  1256. }
  1257. /**
  1258. * Whether there are recurring items
  1259. *
  1260. * @return bool
  1261. */
  1262. public function hasRecurringItems()
  1263. {
  1264. foreach ($this->getAllVisibleItems() as $item) {
  1265. if ($item->getProduct() && $item->getProduct()->isRecurring()) {
  1266. return true;
  1267. }
  1268. }
  1269. return false;
  1270. }
  1271. /**
  1272. * Getter whether quote has nominal items
  1273. * Can bypass treating virtual items as nominal
  1274. *
  1275. * @param bool $countVirtual
  1276. * @return bool
  1277. */
  1278. public function hasNominalItems($countVirtual = true)
  1279. {
  1280. foreach ($this->getAllVisibleItems() as $item) {
  1281. if ($item->isNominal()) {
  1282. if ((!$countVirtual) && $item->getProduct()->isVirtual()) {
  1283. continue;
  1284. }
  1285. return true;
  1286. }
  1287. }
  1288. return false;
  1289. }
  1290. /**
  1291. * Whether quote has nominal items only
  1292. *
  1293. * @return bool
  1294. */
  1295. public function isNominal()
  1296. {
  1297. foreach ($this->getAllVisibleItems() as $item) {
  1298. if (!$item->isNominal()) {
  1299. return false;
  1300. }
  1301. }
  1302. return true;
  1303. }
  1304. /**
  1305. * Create recurring payment profiles basing on the current items
  1306. *
  1307. * @return array
  1308. */
  1309. public function prepareRecurringPaymentProfiles()
  1310. {
  1311. if (!$this->getTotalsCollectedFlag()) {
  1312. // Whoops! Make sure nominal totals must be calculated here.
  1313. throw new Exception('Quote totals must be collected before this operation.');
  1314. }
  1315. $result = array();
  1316. foreach ($this->getAllVisibleItems() as $item) {
  1317. $product = $item->getProduct();
  1318. if (is_object($product) && ($product->isRecurring())
  1319. && $profile = Mage::getModel('sales/recurring_profile')->importProduct($product)
  1320. ) {
  1321. $profile->importQuote($this);
  1322. $profile->importQuoteItem($item);
  1323. $result[] = $profile;
  1324. }
  1325. }
  1326. return $result;
  1327. }
  1328. protected function _validateCouponCode()
  1329. {
  1330. $code = $this->_getData('coupon_code');
  1331. if ($code) {
  1332. $addressHasCoupon = false;
  1333. $addresses = $this->getAllAddresses();
  1334. if (count($addresses)>0) {
  1335. foreach ($addresses as $address) {
  1336. if ($address->hasCouponCode()) {
  1337. $addressHasCoupon = true;
  1338. }
  1339. }
  1340. if (!$addressHasCoupon) {
  1341. $this->setCouponCode('');
  1342. }
  1343. }
  1344. }
  1345. return $this;
  1346. }
  1347. /**
  1348. * Trigger collect totals after loading, if required
  1349. *
  1350. * @return Mage_Sales_Model_Quote
  1351. */
  1352. protected function _afterLoad()
  1353. {
  1354. // collect totals and save me, if required
  1355. if (1 == $this->getData('trigger_recollect')) {
  1356. $this->collectTotals()->save();
  1357. }
  1358. return parent::_afterLoad();
  1359. }
  1360. /**
  1361. * @deprecated after 1.4 beta1 - one page checkout responsibility
  1362. */
  1363. const CHECKOUT_METHOD_REGISTER = 'register';
  1364. const CHECKOUT_METHOD_GUEST = 'guest';
  1365. const CHECKOUT_METHOD_LOGIN_IN = 'login_in';
  1366. /**
  1367. * Return quote checkout method code
  1368. *
  1369. * @deprecated after 1.4 beta1 it is checkout module responsibility
  1370. * @param boolean $originalMethod if true return defined method from begining
  1371. * @return string
  1372. */
  1373. public function getCheckoutMethod($originalMethod = false)
  1374. {
  1375. if ($this->getCustomerId() && !$originalMethod) {
  1376. return self::CHECKOUT_METHOD_LOGIN_IN;
  1377. }
  1378. return $this->_getData('checkout_method');
  1379. }
  1380. /**
  1381. * Check is allow Guest Checkout
  1382. *
  1383. * @deprecated after 1.4 beta1 it is checkout module responsibility
  1384. * @return bool
  1385. */
  1386. public function isAllowedGuestCheckout()
  1387. {
  1388. return Mage::helper('checkout')->isAllowedGuestCheckout($this, $this->getStoreId());
  1389. }
  1390. }