AbstractItem.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Quote\Model\Quote\Item;
  7. use Magento\Quote\Model\Quote\Item;
  8. use Magento\Framework\Api\AttributeValueFactory;
  9. /**
  10. * Quote item abstract model
  11. *
  12. * Price attributes:
  13. * - price - initial item price, declared during product association
  14. * - original_price - product price before any calculations
  15. * - calculation_price - prices for item totals calculation
  16. * - custom_price - new price that can be declared by user and recalculated during calculation process
  17. * - original_custom_price - original defined value of custom price without any conversion
  18. *
  19. * @api
  20. * @method float getDiscountAmount()
  21. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setDiscountAmount(float $amount)
  22. * @method float getBaseDiscountAmount()
  23. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setBaseDiscountAmount(float $amount)
  24. * @method float getDiscountPercent()
  25. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setDiscountPercent()
  26. * @method float getOriginalDiscountAmount()
  27. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setOriginalDiscountAmount()
  28. * @method float getBaseOriginalDiscountAmount()
  29. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setBaseOriginalDiscountAmount()
  30. * @method float getDiscountCalculationPrice()
  31. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setDiscountCalculationPrice()
  32. * @method float getBaseDiscountCalculationPrice()
  33. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setBaseDiscountCalculationPrice($price)
  34. * @method int[] getAppliedRuleIds()
  35. * @method \Magento\Quote\Model\Quote\Item\AbstractItem setAppliedRuleIds(array $ruleIds)
  36. * @method float getBaseTaxAmount()
  37. * @method float getBaseDiscountTaxCompensation()
  38. * @method float getBaseRowTotal()
  39. * @method float getQtyOrdered()
  40. * @method float getRowTotalInclTax()
  41. * @method float getTaxAmount()
  42. * @method float getDiscountTaxCompensation()
  43. * @method float getRowTotal()
  44. * @method float getPriceInclTax()
  45. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  46. * @since 100.0.2
  47. */
  48. abstract class AbstractItem extends \Magento\Framework\Model\AbstractExtensibleModel implements
  49. \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface
  50. {
  51. /**
  52. * @var Item|null
  53. */
  54. protected $_parentItem = null;
  55. /**
  56. * @var \Magento\Quote\Model\Quote\Item\AbstractItem[]
  57. */
  58. protected $_children = [];
  59. /**
  60. * @var array
  61. */
  62. protected $_messages = [];
  63. /**
  64. * List of custom options
  65. *
  66. * @var array
  67. */
  68. protected $_optionsByCode;
  69. /**
  70. * @var \Magento\Catalog\Api\ProductRepositoryInterface
  71. */
  72. protected $productRepository;
  73. /**
  74. * @var \Magento\Framework\Pricing\PriceCurrencyInterface
  75. */
  76. protected $priceCurrency;
  77. /**
  78. * @param \Magento\Framework\Model\Context $context
  79. * @param \Magento\Framework\Registry $registry
  80. * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory
  81. * @param AttributeValueFactory $customAttributeFactory
  82. * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
  83. * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
  84. * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
  85. * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
  86. * @param array $data
  87. */
  88. public function __construct(
  89. \Magento\Framework\Model\Context $context,
  90. \Magento\Framework\Registry $registry,
  91. \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
  92. AttributeValueFactory $customAttributeFactory,
  93. \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
  94. \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
  95. \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
  96. \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
  97. array $data = []
  98. ) {
  99. parent::__construct(
  100. $context,
  101. $registry,
  102. $extensionFactory,
  103. $customAttributeFactory,
  104. $resource,
  105. $resourceCollection,
  106. $data
  107. );
  108. $this->productRepository = $productRepository;
  109. $this->priceCurrency = $priceCurrency;
  110. }
  111. /**
  112. * Retrieve Quote instance
  113. *
  114. * @return \Magento\Quote\Model\Quote
  115. */
  116. abstract public function getQuote();
  117. /**
  118. * Retrieve address model
  119. *
  120. * @return \Magento\Quote\Model\Quote\Address
  121. */
  122. abstract public function getAddress();
  123. /**
  124. * Retrieve product model object associated with item
  125. *
  126. * @return \Magento\Catalog\Model\Product
  127. */
  128. public function getProduct()
  129. {
  130. $product = $this->_getData('product');
  131. if ($product === null && $this->getProductId()) {
  132. $product = clone $this->productRepository->getById(
  133. $this->getProductId(),
  134. false,
  135. $this->getQuote()->getStoreId()
  136. );
  137. $this->setProduct($product);
  138. }
  139. /**
  140. * Reset product final price because it related to custom options
  141. */
  142. $product->setFinalPrice(null);
  143. if (is_array($this->_optionsByCode)) {
  144. $product->setCustomOptions($this->_optionsByCode);
  145. }
  146. return $product;
  147. }
  148. /**
  149. * Returns special download params (if needed) for custom option with type = 'file'
  150. * Needed to implement \Magento\Catalog\Model\Product\Configuration\Item\Interface.
  151. * Return null, as quote item needs no additional configuration.
  152. *
  153. * @return null|\Magento\Framework\DataObject
  154. */
  155. public function getFileDownloadParams()
  156. {
  157. return null;
  158. }
  159. /**
  160. * Specify parent item id before saving data
  161. *
  162. * @return $this
  163. */
  164. public function beforeSave()
  165. {
  166. parent::beforeSave();
  167. if ($this->getParentItem()) {
  168. $this->setParentItemId($this->getParentItem()->getId());
  169. }
  170. return $this;
  171. }
  172. /**
  173. * Set parent item
  174. *
  175. * @param Item $parentItem
  176. * @return $this
  177. */
  178. public function setParentItem($parentItem)
  179. {
  180. if ($parentItem) {
  181. $this->_parentItem = $parentItem;
  182. $parentItem->addChild($this);
  183. }
  184. return $this;
  185. }
  186. /**
  187. * Get parent item
  188. *
  189. * @return Item
  190. */
  191. public function getParentItem()
  192. {
  193. return $this->_parentItem;
  194. }
  195. /**
  196. * Get child items
  197. *
  198. * @return \Magento\Quote\Model\Quote\Item\AbstractItem[]
  199. */
  200. public function getChildren()
  201. {
  202. return $this->_children;
  203. }
  204. /**
  205. * Add child item
  206. *
  207. * @param \Magento\Quote\Model\Quote\Item\AbstractItem $child
  208. * @return $this
  209. */
  210. public function addChild($child)
  211. {
  212. $this->setHasChildren(true);
  213. $this->_children[] = $child;
  214. return $this;
  215. }
  216. /**
  217. * Adds message(s) for quote item. Duplicated messages are not added.
  218. *
  219. * @param mixed $messages
  220. * @return $this
  221. */
  222. public function setMessage($messages)
  223. {
  224. $messagesExists = $this->getMessage(false);
  225. if (!is_array($messages)) {
  226. $messages = [$messages];
  227. }
  228. foreach ($messages as $message) {
  229. if (!in_array($message, $messagesExists)) {
  230. $this->addMessage($message);
  231. }
  232. }
  233. return $this;
  234. }
  235. /**
  236. * Add message of quote item to array of messages
  237. *
  238. * @param string $message
  239. * @return $this
  240. */
  241. public function addMessage($message)
  242. {
  243. $this->_messages[] = $message;
  244. return $this;
  245. }
  246. /**
  247. * Get messages array of quote item
  248. *
  249. * @param bool $string flag for converting messages to string
  250. * @return array|string
  251. */
  252. public function getMessage($string = true)
  253. {
  254. if ($string) {
  255. return join("\n", $this->_messages);
  256. }
  257. return $this->_messages;
  258. }
  259. /**
  260. * Removes message by text
  261. *
  262. * @param string $text
  263. * @return $this
  264. */
  265. public function removeMessageByText($text)
  266. {
  267. foreach ($this->_messages as $key => $message) {
  268. if ($message == $text) {
  269. unset($this->_messages[$key]);
  270. }
  271. }
  272. return $this;
  273. }
  274. /**
  275. * Clears all messages
  276. *
  277. * @return $this
  278. */
  279. public function clearMessage()
  280. {
  281. $this->unsMessage();
  282. // For older compatibility, when we kept message inside data array
  283. $this->_messages = [];
  284. return $this;
  285. }
  286. /**
  287. * Retrieve store model object
  288. *
  289. * @return \Magento\Store\Model\Store
  290. */
  291. public function getStore()
  292. {
  293. return $this->getQuote()->getStore();
  294. }
  295. /**
  296. * Checking item data
  297. *
  298. * @return $this
  299. */
  300. public function checkData()
  301. {
  302. $this->setHasError(false);
  303. $this->clearMessage();
  304. $qty = $this->_getData('qty');
  305. try {
  306. $this->setQty($qty);
  307. } catch (\Magento\Framework\Exception\LocalizedException $e) {
  308. $this->setHasError(true);
  309. $this->setMessage($e->getMessage());
  310. } catch (\Exception $e) {
  311. $this->setHasError(true);
  312. $this->setMessage(__('Item qty declaration error'));
  313. }
  314. try {
  315. $this->getProduct()->getTypeInstance()->checkProductBuyState($this->getProduct());
  316. } catch (\Magento\Framework\Exception\LocalizedException $e) {
  317. $this->setHasError(true)->setMessage($e->getMessage());
  318. $this->getQuote()->setHasError(
  319. true
  320. )->addMessage(
  321. __('Some of the products below do not have all the required options.')
  322. );
  323. } catch (\Exception $e) {
  324. $this->setHasError(true)->setMessage(__('Something went wrong during the item options declaration.'));
  325. $this->getQuote()->setHasError(true)->addMessage(__('We found an item options declaration error.'));
  326. }
  327. if ($this->getProduct()->getHasError()) {
  328. $this->setHasError(true)->setMessage(__('Some of the selected options are not currently available.'));
  329. $this->getQuote()->setHasError(true)->addMessage($this->getProduct()->getMessage(), 'options');
  330. }
  331. if ($this->getHasConfigurationUnavailableError()) {
  332. $this->setHasError(
  333. true
  334. )->setMessage(
  335. __('Selected option(s) or their combination is not currently available.')
  336. );
  337. $this->getQuote()->setHasError(
  338. true
  339. )->addMessage(
  340. __('Some item options or their combination are not currently available.'),
  341. 'unavailable-configuration'
  342. );
  343. $this->unsHasConfigurationUnavailableError();
  344. }
  345. return $this;
  346. }
  347. /**
  348. * Get original (not related with parent item) item quantity
  349. *
  350. * @return int|float
  351. */
  352. public function getQty()
  353. {
  354. return $this->_getData('qty');
  355. }
  356. /**
  357. * Get total item quantity (include parent item relation)
  358. *
  359. * @return int|float
  360. */
  361. public function getTotalQty()
  362. {
  363. if ($this->getParentItem()) {
  364. return $this->getQty() * $this->getParentItem()->getQty();
  365. }
  366. return $this->getQty();
  367. }
  368. /**
  369. * Calculate item row total price
  370. *
  371. * @return $this
  372. */
  373. public function calcRowTotal()
  374. {
  375. $qty = $this->getTotalQty();
  376. // Round unit price before multiplying to prevent losing 1 cent on subtotal
  377. $total = $this->priceCurrency->round($this->getCalculationPriceOriginal()) * $qty;
  378. $baseTotal = $this->priceCurrency->round($this->getBaseCalculationPriceOriginal()) * $qty;
  379. $this->setRowTotal($this->priceCurrency->round($total));
  380. $this->setBaseRowTotal($this->priceCurrency->round($baseTotal));
  381. return $this;
  382. }
  383. /**
  384. * Get item price used for quote calculation process.
  385. *
  386. * This method get custom price (if it is defined) or original product final price
  387. *
  388. * @return float
  389. */
  390. public function getCalculationPrice()
  391. {
  392. $price = $this->_getData('calculation_price');
  393. if ($price === null) {
  394. if ($this->hasCustomPrice()) {
  395. $price = $this->getCustomPrice();
  396. } else {
  397. $price = $this->getConvertedPrice();
  398. }
  399. $this->setData('calculation_price', $price);
  400. }
  401. return $price;
  402. }
  403. /**
  404. * Get item price used for quote calculation process.
  405. *
  406. * This method get original custom price applied before tax calculation
  407. *
  408. * @return float
  409. */
  410. public function getCalculationPriceOriginal()
  411. {
  412. $price = $this->_getData('calculation_price');
  413. if ($price === null) {
  414. if ($this->hasOriginalCustomPrice()) {
  415. $price = $this->getOriginalCustomPrice();
  416. } else {
  417. $price = $this->getConvertedPrice();
  418. }
  419. $this->setData('calculation_price', $price);
  420. }
  421. return $price;
  422. }
  423. /**
  424. * Get calculation price used for quote calculation in base currency.
  425. *
  426. * @return float
  427. */
  428. public function getBaseCalculationPrice()
  429. {
  430. if (!$this->hasBaseCalculationPrice()) {
  431. if ($this->hasCustomPrice()) {
  432. $price = (double)$this->getCustomPrice();
  433. if ($price) {
  434. $rate = $this->priceCurrency->convert($price, $this->getStore()) / $price;
  435. $price = $price / $rate;
  436. }
  437. } else {
  438. $price = $this->getPrice();
  439. }
  440. $this->setBaseCalculationPrice($price);
  441. }
  442. return $this->_getData('base_calculation_price');
  443. }
  444. /**
  445. * Get original calculation price used for quote calculation in base currency.
  446. *
  447. * @return float
  448. */
  449. public function getBaseCalculationPriceOriginal()
  450. {
  451. if (!$this->hasBaseCalculationPrice()) {
  452. if ($this->hasOriginalCustomPrice()) {
  453. $price = (double)$this->getOriginalCustomPrice();
  454. if ($price) {
  455. $rate = $this->priceCurrency->convert($price, $this->getStore()) / $price;
  456. $price = $price / $rate;
  457. }
  458. } else {
  459. $price = $this->getPrice();
  460. }
  461. $this->setBaseCalculationPrice($price);
  462. }
  463. return $this->_getData('base_calculation_price');
  464. }
  465. /**
  466. * Get original price (retrieved from product) for item.
  467. *
  468. * Original price value is in quote selected currency
  469. *
  470. * @return float
  471. */
  472. public function getOriginalPrice()
  473. {
  474. $price = $this->_getData('original_price');
  475. if ($price === null) {
  476. $price = $this->priceCurrency->convert($this->getBaseOriginalPrice(), $this->getStore());
  477. $this->setData('original_price', $price);
  478. }
  479. return $price;
  480. }
  481. /**
  482. * Set original price to item (calculation price will be refreshed too)
  483. *
  484. * @param float $price
  485. * @return \Magento\Quote\Model\Quote\Item\AbstractItem
  486. */
  487. public function setOriginalPrice($price)
  488. {
  489. return $this->setData('original_price', $price);
  490. }
  491. /**
  492. * Get Original item price (got from product) in base website currency
  493. *
  494. * @return float
  495. */
  496. public function getBaseOriginalPrice()
  497. {
  498. return $this->_getData('base_original_price');
  499. }
  500. /**
  501. * Specify custom item price (used in case when we have apply not product price to item)
  502. *
  503. * @param float $value
  504. * @return \Magento\Quote\Model\Quote\Item\AbstractItem
  505. */
  506. public function setCustomPrice($value)
  507. {
  508. $this->setCalculationPrice($value);
  509. $this->setBaseCalculationPrice(null);
  510. return $this->setData('custom_price', $value);
  511. }
  512. /**
  513. * Get item price. Item price currency is website base currency.
  514. *
  515. * @return float
  516. */
  517. public function getPrice()
  518. {
  519. return $this->_getData('price');
  520. }
  521. /**
  522. * Specify item price (base calculation price and converted price will be refreshed too)
  523. *
  524. * @param float $value
  525. * @return $this
  526. */
  527. public function setPrice($value)
  528. {
  529. $this->setBaseCalculationPrice(null);
  530. $this->setConvertedPrice(null);
  531. return $this->setData('price', $value);
  532. }
  533. /**
  534. * Get item price converted to quote currency
  535. *
  536. * @return float
  537. */
  538. public function getConvertedPrice()
  539. {
  540. $price = $this->_getData('converted_price');
  541. if ($price === null) {
  542. $price = $this->priceCurrency->convert($this->getPrice(), $this->getStore());
  543. $this->setData('converted_price', $price);
  544. }
  545. return $price;
  546. }
  547. /**
  548. * Set new value for converted price
  549. *
  550. * @param float $value
  551. * @return $this
  552. */
  553. public function setConvertedPrice($value)
  554. {
  555. $this->setCalculationPrice(null);
  556. $this->setData('converted_price', $value);
  557. return $this;
  558. }
  559. /**
  560. * Clone quote item
  561. *
  562. * @return $this
  563. */
  564. public function __clone()
  565. {
  566. $this->setId(null);
  567. $this->_parentItem = null;
  568. $this->_children = [];
  569. $this->_messages = [];
  570. return $this;
  571. }
  572. /**
  573. * Checking if there children calculated or parent item when we have parent quote item and its children
  574. *
  575. * @return bool
  576. */
  577. public function isChildrenCalculated()
  578. {
  579. if ($this->getParentItem()) {
  580. $calculate = $this->getParentItem()->getProduct()->getPriceType();
  581. } else {
  582. $calculate = $this->getProduct()->getPriceType();
  583. }
  584. if (null !== $calculate &&
  585. (int)$calculate === \Magento\Catalog\Model\Product\Type\AbstractType::CALCULATE_CHILD
  586. ) {
  587. return true;
  588. }
  589. return false;
  590. }
  591. /**
  592. * Checking can we ship product separately
  593. *
  594. * Checking can we ship product separately (each child separately)
  595. * or each parent product item can be shipped only like one item
  596. *
  597. * @return bool
  598. */
  599. public function isShipSeparately()
  600. {
  601. if ($this->getParentItem()) {
  602. $shipmentType = $this->getParentItem()->getProduct()->getShipmentType();
  603. } else {
  604. $shipmentType = $this->getProduct()->getShipmentType();
  605. }
  606. if (null !== $shipmentType &&
  607. (int)$shipmentType === \Magento\Catalog\Model\Product\Type\AbstractType::SHIPMENT_SEPARATELY
  608. ) {
  609. return true;
  610. }
  611. return false;
  612. }
  613. /**
  614. * Returns the total discount amounts of all the child items.
  615. *
  616. * If there are no children, returns the discount amount of this item.
  617. *
  618. * @return float
  619. */
  620. public function getTotalDiscountAmount()
  621. {
  622. $totalDiscountAmount = 0;
  623. /* \Magento\Quote\Model\Quote\Item\AbstractItem[] */
  624. $children = $this->getChildren();
  625. if (!empty($children) && $this->isChildrenCalculated()) {
  626. foreach ($children as $child) {
  627. $totalDiscountAmount += $child->getDiscountAmount();
  628. }
  629. } else {
  630. $totalDiscountAmount = $this->getDiscountAmount();
  631. }
  632. return $totalDiscountAmount;
  633. }
  634. }