Calculator.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Bundle\Pricing\Adjustment;
  7. use Magento\Bundle\Model\Product\Price;
  8. use Magento\Bundle\Pricing\Price\BundleSelectionFactory;
  9. use Magento\Catalog\Model\Product;
  10. use Magento\Framework\Pricing\Adjustment\Calculator as CalculatorBase;
  11. use Magento\Framework\Pricing\Amount\AmountFactory;
  12. use Magento\Framework\Pricing\SaleableInterface;
  13. use Magento\Framework\Pricing\PriceCurrencyInterface;
  14. use Magento\Store\Model\Store;
  15. use Magento\Tax\Api\TaxCalculationInterface;
  16. use Magento\Tax\Helper\Data as TaxHelper;
  17. /**
  18. * Bundle price calculator
  19. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  20. */
  21. class Calculator implements BundleCalculatorInterface
  22. {
  23. /**
  24. * @var CalculatorBase
  25. */
  26. protected $calculator;
  27. /**
  28. * @var AmountFactory
  29. */
  30. protected $amountFactory;
  31. /**
  32. * @var BundleSelectionFactory
  33. */
  34. protected $selectionFactory;
  35. /**
  36. * Tax helper, needed to get rounding setting
  37. *
  38. * @var TaxHelper
  39. */
  40. protected $taxHelper;
  41. /**
  42. * @var PriceCurrencyInterface
  43. */
  44. protected $priceCurrency;
  45. /**
  46. * @var \Magento\Framework\Pricing\Amount\AmountInterface[]
  47. */
  48. private $optionAmount = [];
  49. /**
  50. * @var SelectionPriceListProviderInterface
  51. */
  52. private $selectionPriceListProvider;
  53. /**
  54. * @param CalculatorBase $calculator
  55. * @param AmountFactory $amountFactory
  56. * @param BundleSelectionFactory $bundleSelectionFactory
  57. * @param TaxHelper $taxHelper
  58. * @param PriceCurrencyInterface $priceCurrency
  59. * @param SelectionPriceListProviderInterface|null $selectionPriceListProvider
  60. */
  61. public function __construct(
  62. CalculatorBase $calculator,
  63. AmountFactory $amountFactory,
  64. BundleSelectionFactory $bundleSelectionFactory,
  65. TaxHelper $taxHelper,
  66. PriceCurrencyInterface $priceCurrency,
  67. SelectionPriceListProviderInterface $selectionPriceListProvider = null
  68. ) {
  69. $this->calculator = $calculator;
  70. $this->amountFactory = $amountFactory;
  71. $this->selectionFactory = $bundleSelectionFactory;
  72. $this->taxHelper = $taxHelper;
  73. $this->priceCurrency = $priceCurrency;
  74. $this->selectionPriceListProvider = $selectionPriceListProvider;
  75. }
  76. /**
  77. * Get amount for current product which is included price of existing options with minimal price
  78. *
  79. * @param float|string $amount
  80. * @param SaleableInterface $saleableItem
  81. * @param null|bool|string|array $exclude
  82. * @param null|array $context
  83. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  84. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  85. */
  86. public function getAmount($amount, SaleableInterface $saleableItem, $exclude = null, $context = [])
  87. {
  88. return $this->getOptionsAmount($saleableItem, $exclude, true, $amount);
  89. }
  90. /**
  91. * Get amount for current product which is included price of existing options with maximal price
  92. *
  93. * @param float $amount
  94. * @param Product $saleableItem
  95. * @param null|bool|string|array $exclude
  96. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  97. */
  98. public function getMinRegularAmount($amount, Product $saleableItem, $exclude = null)
  99. {
  100. return $this->getOptionsAmount($saleableItem, $exclude, true, $amount, true);
  101. }
  102. /**
  103. * Get amount for current product which is included price of existing options with maximal price
  104. *
  105. * @param float $amount
  106. * @param Product $saleableItem
  107. * @param null|bool|string|array $exclude
  108. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  109. */
  110. public function getMaxAmount($amount, Product $saleableItem, $exclude = null)
  111. {
  112. return $this->getOptionsAmount($saleableItem, $exclude, false, $amount);
  113. }
  114. /**
  115. * Get amount for current product which is included price of existing options with maximal price
  116. *
  117. * @param float $amount
  118. * @param Product $saleableItem
  119. * @param null|bool|string|array $exclude
  120. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  121. */
  122. public function getMaxRegularAmount($amount, Product $saleableItem, $exclude = null)
  123. {
  124. return $this->getOptionsAmount($saleableItem, $exclude, false, $amount, true);
  125. }
  126. /**
  127. * Option amount calculation for bundle product
  128. *
  129. * @param Product $saleableItem
  130. * @param null|bool|string|array $exclude
  131. * @param bool $searchMin
  132. * @param float $baseAmount
  133. * @param bool $useRegularPrice
  134. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  135. */
  136. public function getOptionsAmount(
  137. Product $saleableItem,
  138. $exclude = null,
  139. $searchMin = true,
  140. $baseAmount = 0.,
  141. $useRegularPrice = false
  142. ) {
  143. $cacheKey = implode('-', [$saleableItem->getId(), $exclude, $searchMin, $baseAmount, $useRegularPrice]);
  144. if (!isset($this->optionAmount[$cacheKey])) {
  145. $this->optionAmount[$cacheKey] = $this->calculateBundleAmount(
  146. $baseAmount,
  147. $saleableItem,
  148. $this->getSelectionAmounts($saleableItem, $searchMin, $useRegularPrice),
  149. $exclude
  150. );
  151. }
  152. return $this->optionAmount[$cacheKey];
  153. }
  154. /**
  155. * Get base amount without option
  156. *
  157. * @param float $amount
  158. * @param Product $saleableItem
  159. * @return \Magento\Framework\Pricing\Amount\AmountInterface|void
  160. */
  161. public function getAmountWithoutOption($amount, Product $saleableItem)
  162. {
  163. return $this->calculateBundleAmount(
  164. $amount,
  165. $saleableItem,
  166. []
  167. );
  168. }
  169. /**
  170. * Filter all options for bundle product
  171. *
  172. * @param Product $bundleProduct
  173. * @param bool $searchMin
  174. * @param bool $useRegularPrice
  175. * @return array
  176. */
  177. protected function getSelectionAmounts(Product $bundleProduct, $searchMin, $useRegularPrice = false)
  178. {
  179. return $this->getSelectionPriceListProvider()->getPriceList($bundleProduct, $searchMin, $useRegularPrice);
  180. }
  181. /**
  182. * Get selection price list provider.
  183. *
  184. * @return SelectionPriceListProviderInterface
  185. * @deprecated 100.2.0
  186. */
  187. private function getSelectionPriceListProvider()
  188. {
  189. if (null === $this->selectionPriceListProvider) {
  190. $this->selectionPriceListProvider = \Magento\Framework\App\ObjectManager::getInstance()
  191. ->get(SelectionPriceListProviderInterface::class);
  192. }
  193. return $this->selectionPriceListProvider;
  194. }
  195. /**
  196. * Check this option if it should be skipped
  197. *
  198. * @param \Magento\Bundle\Model\Option $option
  199. * @param bool $canSkipRequiredOption
  200. * @return bool
  201. * @deprecated 100.2.0
  202. */
  203. protected function canSkipOption($option, $canSkipRequiredOption)
  204. {
  205. return !$option->getSelections() || ($canSkipRequiredOption && !$option->getRequired());
  206. }
  207. /**
  208. * Check the bundle product for availability of required options
  209. *
  210. * @param Product $bundleProduct
  211. * @return bool
  212. * @deprecated 100.2.0
  213. */
  214. protected function hasRequiredOption($bundleProduct)
  215. {
  216. $options = array_filter(
  217. $this->getBundleOptions($bundleProduct),
  218. function ($item) {
  219. return $item->getRequired();
  220. }
  221. );
  222. return !empty($options);
  223. }
  224. /**
  225. * Get bundle options
  226. *
  227. * @param Product $saleableItem
  228. * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
  229. * @deprecated 100.2.0
  230. */
  231. protected function getBundleOptions(Product $saleableItem)
  232. {
  233. /** @var \Magento\Bundle\Pricing\Price\BundleOptionPrice $bundlePrice */
  234. $bundlePrice = $saleableItem->getPriceInfo()->getPrice(
  235. \Magento\Bundle\Pricing\Price\BundleOptionPrice::PRICE_CODE
  236. );
  237. return $bundlePrice->getOptions();
  238. }
  239. /**
  240. * Calculate amount for bundle product with all selection prices
  241. *
  242. * @param float $basePriceValue
  243. * @param Product $bundleProduct
  244. * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList
  245. * @param null|bool|string|array $exclude
  246. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  247. */
  248. public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude = null)
  249. {
  250. if ($bundleProduct->getPriceType() == Price::PRICE_TYPE_FIXED) {
  251. return $this->calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude);
  252. }
  253. return $this->calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude);
  254. }
  255. /**
  256. * Calculate amount for fixed bundle product
  257. *
  258. * @param float $basePriceValue
  259. * @param Product $bundleProduct
  260. * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList
  261. * @param null|bool|string|array $exclude
  262. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  263. */
  264. protected function calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude)
  265. {
  266. $fullAmount = $basePriceValue;
  267. /** @var $option \Magento\Bundle\Model\Option */
  268. foreach ($selectionPriceList as $selectionPrice) {
  269. $fullAmount += ($selectionPrice->getValue() * $selectionPrice->getQuantity());
  270. }
  271. return $this->calculator->getAmount($fullAmount, $bundleProduct, $exclude);
  272. }
  273. /**
  274. * Calculate amount for dynamic bundle product
  275. *
  276. * @param float $basePriceValue
  277. * @param Product $bundleProduct
  278. * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList
  279. * @param null|bool|string|array $exclude
  280. * @return \Magento\Framework\Pricing\Amount\AmountInterface
  281. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  282. */
  283. protected function calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude)
  284. {
  285. $fullAmount = 0.;
  286. $adjustments = [];
  287. $i = 0;
  288. $amountList[$i]['amount'] = $this->calculator->getAmount($basePriceValue, $bundleProduct, $exclude);
  289. $amountList[$i]['quantity'] = 1;
  290. foreach ($selectionPriceList as $selectionPrice) {
  291. ++$i;
  292. if ($selectionPrice) {
  293. $amountList[$i]['amount'] = $selectionPrice->getAmount();
  294. // always honor the quantity given
  295. $amountList[$i]['quantity'] = $selectionPrice->getQuantity();
  296. }
  297. }
  298. /** @var Store $store */
  299. $store = $bundleProduct->getStore();
  300. $roundingMethod = $this->taxHelper->getCalculationAlgorithm($store);
  301. foreach ($amountList as $amountInfo) {
  302. /** @var \Magento\Framework\Pricing\Amount\AmountInterface $itemAmount */
  303. $itemAmount = $amountInfo['amount'];
  304. $qty = $amountInfo['quantity'];
  305. if ($roundingMethod != TaxCalculationInterface::CALC_TOTAL_BASE) {
  306. //We need to round the individual selection first
  307. $fullAmount += ($this->priceCurrency->round($itemAmount->getValue()) * $qty);
  308. foreach ($itemAmount->getAdjustmentAmounts() as $code => $adjustment) {
  309. $adjustment = $this->priceCurrency->round($adjustment) * $qty;
  310. $adjustments[$code] = isset($adjustments[$code]) ? $adjustments[$code] + $adjustment : $adjustment;
  311. }
  312. } else {
  313. $fullAmount += ($itemAmount->getValue() * $qty);
  314. foreach ($itemAmount->getAdjustmentAmounts() as $code => $adjustment) {
  315. $adjustment = $adjustment * $qty;
  316. $adjustments[$code] = isset($adjustments[$code]) ? $adjustments[$code] + $adjustment : $adjustment;
  317. }
  318. }
  319. }
  320. if (is_array($exclude) == false) {
  321. if ($exclude && isset($adjustments[$exclude])) {
  322. $fullAmount -= $adjustments[$exclude];
  323. unset($adjustments[$exclude]);
  324. }
  325. } else {
  326. foreach ($exclude as $oneExclusion) {
  327. if ($oneExclusion && isset($adjustments[$oneExclusion])) {
  328. $fullAmount -= $adjustments[$oneExclusion];
  329. unset($adjustments[$oneExclusion]);
  330. }
  331. }
  332. }
  333. return $this->amountFactory->create($fullAmount, $adjustments);
  334. }
  335. /**
  336. * Create selection price list for the retrieved options
  337. *
  338. * @param \Magento\Bundle\Model\Option $option
  339. * @param Product $bundleProduct
  340. * @param bool $useRegularPrice
  341. * @return \Magento\Bundle\Pricing\Price\BundleSelectionPrice[]
  342. */
  343. public function createSelectionPriceList($option, $bundleProduct, $useRegularPrice = false)
  344. {
  345. $priceList = [];
  346. $selections = $option->getSelections();
  347. if ($selections === null) {
  348. return $priceList;
  349. }
  350. /* @var $selection \Magento\Bundle\Model\Selection|\Magento\Catalog\Model\Product */
  351. foreach ($selections as $selection) {
  352. if (!$selection->isSalable()) {
  353. // @todo CatalogInventory Show out of stock Products
  354. continue;
  355. }
  356. $priceList[] = $this->selectionFactory->create(
  357. $bundleProduct,
  358. $selection,
  359. $selection->getSelectionQty(),
  360. [
  361. 'useRegularPrice' => $useRegularPrice,
  362. ]
  363. );
  364. }
  365. return $priceList;
  366. }
  367. /**
  368. * Find minimal or maximal price for existing options
  369. *
  370. * @param \Magento\Bundle\Model\Option $option
  371. * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList
  372. * @param bool $searchMin
  373. * @return \Magento\Bundle\Pricing\Price\BundleSelectionPrice[]
  374. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  375. */
  376. public function processOptions($option, $selectionPriceList, $searchMin = true)
  377. {
  378. $result = [];
  379. foreach ($selectionPriceList as $current) {
  380. $qty = $current->getQuantity();
  381. $currentValue = $current->getAmount()->getValue() * $qty;
  382. if (empty($result)) {
  383. $result = [$current];
  384. } else {
  385. $lastSelectionPrice = end($result);
  386. $lastValue = $lastSelectionPrice->getAmount()->getValue() * $lastSelectionPrice->getQuantity();
  387. if ($searchMin && $lastValue > $currentValue) {
  388. $result = [$current];
  389. } elseif (!$searchMin && $option->isMultiSelection()) {
  390. $result[] = $current;
  391. } elseif (!$searchMin
  392. && !$option->isMultiSelection()
  393. && $lastValue < $currentValue
  394. ) {
  395. $result = [$current];
  396. }
  397. }
  398. }
  399. return $result;
  400. }
  401. }