Price.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Bundle\Model\Product;
  7. use Magento\Customer\Api\GroupManagementInterface;
  8. use Magento\Framework\Pricing\PriceCurrencyInterface;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
  11. /**
  12. * @api
  13. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  14. * @since 100.0.2
  15. */
  16. class Price extends \Magento\Catalog\Model\Product\Type\Price
  17. {
  18. /**
  19. * Fixed bundle price type
  20. */
  21. const PRICE_TYPE_FIXED = 1;
  22. /**
  23. * Dynamic bundle price type
  24. */
  25. const PRICE_TYPE_DYNAMIC = 0;
  26. /**
  27. * Flag which indicates - is min/max prices have been calculated by index
  28. *
  29. * @var bool
  30. */
  31. protected $_isPricesCalculatedByIndex;
  32. /**
  33. * Catalog data
  34. *
  35. * @var \Magento\Catalog\Helper\Data
  36. */
  37. protected $_catalogData = null;
  38. /**
  39. * Serializer interface instance.
  40. *
  41. * @var \Magento\Framework\Serialize\Serializer\Json
  42. */
  43. private $serializer;
  44. /**
  45. * Constructor
  46. *
  47. * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory
  48. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  49. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
  50. * @param \Magento\Customer\Model\Session $customerSession
  51. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  52. * @param PriceCurrencyInterface $priceCurrency
  53. * @param GroupManagementInterface $groupManagement
  54. * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory
  55. * @param \Magento\Framework\App\Config\ScopeConfigInterface $config
  56. * @param \Magento\Catalog\Helper\Data $catalogData
  57. * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
  58. * @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory
  59. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  60. */
  61. public function __construct(
  62. \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory,
  63. \Magento\Store\Model\StoreManagerInterface $storeManager,
  64. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
  65. \Magento\Customer\Model\Session $customerSession,
  66. \Magento\Framework\Event\ManagerInterface $eventManager,
  67. PriceCurrencyInterface $priceCurrency,
  68. GroupManagementInterface $groupManagement,
  69. \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory,
  70. \Magento\Framework\App\Config\ScopeConfigInterface $config,
  71. \Magento\Catalog\Helper\Data $catalogData,
  72. \Magento\Framework\Serialize\Serializer\Json $serializer = null,
  73. ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null
  74. ) {
  75. $this->_catalogData = $catalogData;
  76. $this->serializer = $serializer ?: ObjectManager::getInstance()
  77. ->get(\Magento\Framework\Serialize\Serializer\Json::class);
  78. parent::__construct(
  79. $ruleFactory,
  80. $storeManager,
  81. $localeDate,
  82. $customerSession,
  83. $eventManager,
  84. $priceCurrency,
  85. $groupManagement,
  86. $tierPriceFactory,
  87. $config,
  88. $tierPriceExtensionFactory
  89. );
  90. }
  91. /**
  92. * Is min/max prices have been calculated by index
  93. *
  94. * @return bool
  95. * @SuppressWarnings(PHPMD.BooleanGetMethodName)
  96. */
  97. public function getIsPricesCalculatedByIndex()
  98. {
  99. return $this->_isPricesCalculatedByIndex;
  100. }
  101. /**
  102. * Return product base price
  103. *
  104. * @param \Magento\Catalog\Model\Product $product
  105. * @return float
  106. */
  107. public function getPrice($product)
  108. {
  109. if ($product->getPriceType() == self::PRICE_TYPE_FIXED) {
  110. return $product->getData('price');
  111. } else {
  112. return 0;
  113. }
  114. }
  115. /**
  116. * Get Total price for Bundle items
  117. *
  118. * @param \Magento\Catalog\Model\Product $product
  119. * @param null|float $qty
  120. * @return float
  121. */
  122. public function getTotalBundleItemsPrice($product, $qty = null)
  123. {
  124. $price = 0.0;
  125. if ($product->hasCustomOptions()) {
  126. $selectionIds = $this->getBundleSelectionIds($product);
  127. if ($selectionIds) {
  128. $selections = $product->getTypeInstance()->getSelectionsByIds($selectionIds, $product);
  129. $selections->addTierPriceData();
  130. $this->_eventManager->dispatch(
  131. 'prepare_catalog_product_collection_prices',
  132. ['collection' => $selections, 'store_id' => $product->getStoreId()]
  133. );
  134. foreach ($selections->getItems() as $selection) {
  135. if ($selection->isSalable()) {
  136. $selectionQty = $product->getCustomOption('selection_qty_' . $selection->getSelectionId());
  137. if ($selectionQty) {
  138. $price += $this->getSelectionFinalTotalPrice(
  139. $product,
  140. $selection,
  141. $qty,
  142. $selectionQty->getValue()
  143. );
  144. }
  145. }
  146. }
  147. }
  148. }
  149. return $price;
  150. }
  151. /**
  152. * Retrieve array of bundle selection IDs
  153. *
  154. * @param \Magento\Catalog\Model\Product $product
  155. * @return array
  156. */
  157. protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product)
  158. {
  159. $customOption = $product->getCustomOption('bundle_selection_ids');
  160. if ($customOption) {
  161. $selectionIds = $this->serializer->unserialize($customOption->getValue());
  162. if (is_array($selectionIds) && !empty($selectionIds)) {
  163. return $selectionIds;
  164. }
  165. }
  166. return [];
  167. }
  168. /**
  169. * Get product final price
  170. *
  171. * @param float $qty
  172. * @param \Magento\Catalog\Model\Product $product
  173. * @return float
  174. */
  175. public function getFinalPrice($qty, $product)
  176. {
  177. if ($qty === null && $product->getCalculatedFinalPrice() !== null) {
  178. return $product->getCalculatedFinalPrice();
  179. }
  180. $finalPrice = $this->getBasePrice($product, $qty);
  181. $product->setFinalPrice($finalPrice);
  182. $this->_eventManager->dispatch('catalog_product_get_final_price', ['product' => $product, 'qty' => $qty]);
  183. $finalPrice = $product->getData('final_price');
  184. $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice);
  185. $finalPrice += $this->getTotalBundleItemsPrice($product, $qty);
  186. $finalPrice = max(0, $finalPrice);
  187. $product->setFinalPrice($finalPrice);
  188. return $finalPrice;
  189. }
  190. /**
  191. * Returns final price of a child product
  192. *
  193. * @param \Magento\Catalog\Model\Product $product
  194. * @param float $productQty
  195. * @param \Magento\Catalog\Model\Product $childProduct
  196. * @param float $childProductQty
  197. * @return float
  198. */
  199. public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty)
  200. {
  201. return $this->getSelectionFinalTotalPrice($product, $childProduct, $productQty, $childProductQty, false);
  202. }
  203. /**
  204. * Retrieve Price considering tier price
  205. *
  206. * @param \Magento\Catalog\Model\Product $product
  207. * @param string|null $which
  208. * @param bool|null $includeTax
  209. * @param bool $takeTierPrice
  210. * @return float|array
  211. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  212. * @SuppressWarnings(PHPMD.NPathComplexity)
  213. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  214. */
  215. public function getTotalPrices($product, $which = null, $includeTax = null, $takeTierPrice = true)
  216. {
  217. // check calculated price index
  218. if ($product->getData('min_price') && $product->getData('max_price')) {
  219. $minimalPrice = $this->_catalogData->getTaxPrice($product, $product->getData('min_price'), $includeTax);
  220. $maximalPrice = $this->_catalogData->getTaxPrice($product, $product->getData('max_price'), $includeTax);
  221. $this->_isPricesCalculatedByIndex = true;
  222. } else {
  223. /**
  224. * Check if product price is fixed
  225. */
  226. $finalPrice = $product->getFinalPrice();
  227. if ($product->getPriceType() == self::PRICE_TYPE_FIXED) {
  228. $minimalPrice = $maximalPrice = $this->_catalogData->getTaxPrice($product, $finalPrice, $includeTax);
  229. } else {
  230. // PRICE_TYPE_DYNAMIC
  231. $minimalPrice = $maximalPrice = 0;
  232. }
  233. $options = $this->getOptions($product);
  234. $minPriceFounded = false;
  235. if ($options) {
  236. foreach ($options as $option) {
  237. /* @var $option \Magento\Bundle\Model\Option */
  238. $selections = $option->getSelections();
  239. if ($selections) {
  240. $selectionMinimalPrices = [];
  241. $selectionMaximalPrices = [];
  242. foreach ($option->getSelections() as $selection) {
  243. /* @var $selection \Magento\Bundle\Model\Selection */
  244. if (!$selection->isSalable()) {
  245. /**
  246. * @todo CatalogInventory Show out of stock Products
  247. */
  248. continue;
  249. }
  250. $qty = $selection->getSelectionQty();
  251. $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection;
  252. $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice(
  253. $item,
  254. $this->getSelectionFinalTotalPrice(
  255. $product,
  256. $selection,
  257. 1,
  258. $qty,
  259. true,
  260. $takeTierPrice
  261. ),
  262. $includeTax
  263. );
  264. $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice(
  265. $item,
  266. $this->getSelectionFinalTotalPrice(
  267. $product,
  268. $selection,
  269. 1,
  270. null,
  271. true,
  272. $takeTierPrice
  273. ),
  274. $includeTax
  275. );
  276. }
  277. if (count($selectionMinimalPrices)) {
  278. $selMinPrice = min($selectionMinimalPrices);
  279. if ($option->getRequired()) {
  280. $minimalPrice += $selMinPrice;
  281. $minPriceFounded = true;
  282. } elseif (true !== $minPriceFounded) {
  283. $selMinPrice += $minimalPrice;
  284. $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min(
  285. $minPriceFounded,
  286. $selMinPrice
  287. );
  288. }
  289. if ($option->isMultiSelection()) {
  290. $maximalPrice += array_sum($selectionMaximalPrices);
  291. } else {
  292. $maximalPrice += max($selectionMaximalPrices);
  293. }
  294. }
  295. }
  296. }
  297. }
  298. // condition is TRUE when all product options are NOT required
  299. if (!is_bool($minPriceFounded)) {
  300. $minimalPrice = $minPriceFounded;
  301. }
  302. $customOptions = $product->getOptions();
  303. if ($product->getPriceType() == self::PRICE_TYPE_FIXED && $customOptions) {
  304. foreach ($customOptions as $customOption) {
  305. /* @var $customOption \Magento\Catalog\Model\Product\Option */
  306. $values = $customOption->getValues();
  307. if ($values) {
  308. $prices = [];
  309. foreach ($values as $value) {
  310. /* @var $value \Magento\Catalog\Model\Product\Option\Value */
  311. $valuePrice = $value->getPrice(true);
  312. $prices[] = $valuePrice;
  313. }
  314. if (count($prices)) {
  315. if ($customOption->getIsRequire()) {
  316. $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax);
  317. }
  318. $multiTypes = [
  319. \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX,
  320. \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE,
  321. ];
  322. if (in_array($customOption->getType(), $multiTypes)) {
  323. $maximalValue = array_sum($prices);
  324. } else {
  325. $maximalValue = max($prices);
  326. }
  327. $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax);
  328. }
  329. } else {
  330. $valuePrice = $customOption->getPrice(true);
  331. if ($customOption->getIsRequire()) {
  332. $minimalPrice += $this->_catalogData->getTaxPrice($product, $valuePrice, $includeTax);
  333. }
  334. $maximalPrice += $this->_catalogData->getTaxPrice($product, $valuePrice, $includeTax);
  335. }
  336. }
  337. }
  338. $this->_isPricesCalculatedByIndex = false;
  339. }
  340. if ($which == 'max') {
  341. return $maximalPrice;
  342. } elseif ($which == 'min') {
  343. return $minimalPrice;
  344. }
  345. return [$minimalPrice, $maximalPrice];
  346. }
  347. /**
  348. * Get Options with attached Selections collection
  349. *
  350. * @param \Magento\Catalog\Model\Product $product
  351. * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
  352. */
  353. public function getOptions($product)
  354. {
  355. $product->getTypeInstance()->setStoreFilter($product->getStoreId(), $product);
  356. $optionCollection = $product->getTypeInstance()->getOptionsCollection($product);
  357. $selectionCollection = $product->getTypeInstance()->getSelectionsCollection(
  358. $product->getTypeInstance()->getOptionsIds($product),
  359. $product
  360. );
  361. return $optionCollection->appendSelections($selectionCollection, false, false);
  362. }
  363. /**
  364. * Calculate price of selection
  365. *
  366. * @param \Magento\Catalog\Model\Product $bundleProduct
  367. * @param \Magento\Catalog\Model\Product $selectionProduct
  368. * @param float|null $selectionQty
  369. * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity
  370. * @return float
  371. *
  372. * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice()
  373. */
  374. public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQty = null, $multiplyQty = true)
  375. {
  376. return $this->getSelectionFinalTotalPrice($bundleProduct, $selectionProduct, 0, $selectionQty, $multiplyQty);
  377. }
  378. /**
  379. * Calculate selection price for front view (with applied special of bundle)
  380. *
  381. * @param \Magento\Catalog\Model\Product $bundleProduct
  382. * @param \Magento\Catalog\Model\Product $selectionProduct
  383. * @param float $qty
  384. * @return float
  385. */
  386. public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null)
  387. {
  388. return $this->getSelectionPrice($bundleProduct, $selectionProduct, $qty);
  389. }
  390. /**
  391. * Calculate final price of selection
  392. * with take into account tier price
  393. *
  394. * @param \Magento\Catalog\Model\Product $bundleProduct
  395. * @param \Magento\Catalog\Model\Product $selectionProduct
  396. * @param float $bundleQty
  397. * @param float $selectionQty
  398. * @param bool $multiplyQty
  399. * @param bool $takeTierPrice
  400. * @return float
  401. */
  402. public function getSelectionFinalTotalPrice(
  403. $bundleProduct,
  404. $selectionProduct,
  405. $bundleQty,
  406. $selectionQty,
  407. $multiplyQty = true,
  408. $takeTierPrice = true
  409. ) {
  410. if (null === $bundleQty) {
  411. $bundleQty = 1.;
  412. }
  413. if ($selectionQty === null) {
  414. $selectionQty = $selectionProduct->getSelectionQty();
  415. }
  416. if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) {
  417. $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1);
  418. } else {
  419. if ($selectionProduct->getSelectionPriceType()) {
  420. // percent
  421. $product = clone $bundleProduct;
  422. $product->setFinalPrice($this->getPrice($product));
  423. $this->_eventManager->dispatch(
  424. 'catalog_product_get_final_price',
  425. ['product' => $product, 'qty' => $bundleQty]
  426. );
  427. $price = $product->getData('final_price') * ($selectionProduct->getSelectionPriceValue() / 100);
  428. } else {
  429. // fixed
  430. $price = $selectionProduct->getSelectionPriceValue();
  431. }
  432. }
  433. if ($multiplyQty) {
  434. $price *= $selectionQty;
  435. }
  436. return min(
  437. $price,
  438. $this->_applyTierPrice($bundleProduct, $bundleQty, $price),
  439. $this->_applySpecialPrice($bundleProduct, $price)
  440. );
  441. }
  442. /**
  443. * Apply tier price for bundle
  444. *
  445. * @param \Magento\Catalog\Model\Product $product
  446. * @param float $qty
  447. * @param float $finalPrice
  448. * @return float
  449. */
  450. protected function _applyTierPrice($product, $qty, $finalPrice)
  451. {
  452. if ($qty === null) {
  453. return $finalPrice;
  454. }
  455. $tierPrice = $product->getTierPrice($qty);
  456. if (is_numeric($tierPrice)) {
  457. $tierPrice = $finalPrice - $finalPrice * ($tierPrice / 100);
  458. $finalPrice = min($finalPrice, $tierPrice);
  459. }
  460. return $finalPrice;
  461. }
  462. /**
  463. * Get product tier price by qty
  464. *
  465. * @param float $qty
  466. * @param \Magento\Catalog\Model\Product $product
  467. * @return float|array
  468. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  469. * @SuppressWarnings(PHPMD.NPathComplexity)
  470. */
  471. public function getTierPrice($qty, $product)
  472. {
  473. $allCustomersGroupId = $this->_groupManagement->getAllCustomersGroup()->getId();
  474. $prices = $product->getData('tier_price');
  475. if ($prices === null) {
  476. if ($attribute = $product->getResource()->getAttribute('tier_price')) {
  477. $attribute->getBackend()->afterLoad($product);
  478. $prices = $product->getData('tier_price');
  479. }
  480. }
  481. if ($prices === null || !is_array($prices)) {
  482. if ($qty !== null) {
  483. return $product->getPrice();
  484. }
  485. return [
  486. [
  487. 'price' => $product->getPrice(),
  488. 'website_price' => $product->getPrice(),
  489. 'price_qty' => 1,
  490. 'cust_group' => $allCustomersGroupId,
  491. ]
  492. ];
  493. }
  494. $custGroup = $this->_getCustomerGroupId($product);
  495. if ($qty) {
  496. $prevQty = 1;
  497. $prevPrice = 0;
  498. $prevGroup = $allCustomersGroupId;
  499. foreach ($prices as $price) {
  500. if (empty($price['percentage_value'])) {
  501. // can use only percentage tier price
  502. continue;
  503. }
  504. if ($price['cust_group'] != $custGroup && $price['cust_group'] != $allCustomersGroupId) {
  505. // tier not for current customer group nor is for all groups
  506. continue;
  507. }
  508. if ($qty < $price['price_qty']) {
  509. // tier is higher than product qty
  510. continue;
  511. }
  512. if ($price['price_qty'] < $prevQty) {
  513. // higher tier qty already found
  514. continue;
  515. }
  516. if ($price['price_qty'] == $prevQty
  517. && $prevGroup != $allCustomersGroupId
  518. && $price['cust_group'] == $allCustomersGroupId
  519. ) {
  520. // found tier qty is same as current tier qty but current tier group is ALL_GROUPS
  521. continue;
  522. }
  523. if ($price['percentage_value'] > $prevPrice) {
  524. $prevPrice = $price['percentage_value'];
  525. $prevQty = $price['price_qty'];
  526. $prevGroup = $price['cust_group'];
  527. }
  528. }
  529. return $prevPrice;
  530. } else {
  531. $qtyCache = [];
  532. foreach ($prices as $i => $price) {
  533. if ($price['cust_group'] != $custGroup && $price['cust_group'] != $allCustomersGroupId) {
  534. unset($prices[$i]);
  535. } elseif (isset($qtyCache[$price['price_qty']])) {
  536. $j = $qtyCache[$price['price_qty']];
  537. if ($prices[$j]['website_price'] < $price['website_price']) {
  538. unset($prices[$j]);
  539. $qtyCache[$price['price_qty']] = $i;
  540. } else {
  541. unset($prices[$i]);
  542. }
  543. } else {
  544. $qtyCache[$price['price_qty']] = $i;
  545. }
  546. }
  547. }
  548. return $prices ? $prices : [];
  549. }
  550. /**
  551. * Calculate and apply special price
  552. *
  553. * @param float $finalPrice
  554. * @param float $specialPrice
  555. * @param string $specialPriceFrom
  556. * @param string $specialPriceTo
  557. * @param mixed $store
  558. * @return float
  559. */
  560. public function calculateSpecialPrice(
  561. $finalPrice,
  562. $specialPrice,
  563. $specialPriceFrom,
  564. $specialPriceTo,
  565. $store = null
  566. ) {
  567. if ($specialPrice !== null && $specialPrice != false) {
  568. if ($this->_localeDate->isScopeDateInInterval($store, $specialPriceFrom, $specialPriceTo)) {
  569. $specialPrice = $finalPrice * ($specialPrice / 100);
  570. $finalPrice = min($finalPrice, $specialPrice);
  571. }
  572. }
  573. return $finalPrice;
  574. }
  575. /**
  576. * Returns the lowest price after applying any applicable bundle discounts
  577. *
  578. * @param /Magento/Catalog/Model/Product $bundleProduct
  579. * @param float|string $price
  580. * @param int $bundleQty
  581. * @return float
  582. */
  583. public function getLowestPrice($bundleProduct, $price, $bundleQty = 1)
  584. {
  585. $price = (float)$price;
  586. return min(
  587. $price,
  588. $this->_applyTierPrice($bundleProduct, $bundleQty, $price),
  589. $this->_applySpecialPrice($bundleProduct, $price)
  590. );
  591. }
  592. }