StockStateProvider.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\CatalogInventory\Model;
  7. use Magento\Catalog\Model\ProductFactory;
  8. use Magento\CatalogInventory\Api\Data\StockItemInterface;
  9. use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface;
  10. use Magento\Framework\DataObject\Factory as ObjectFactory;
  11. use Magento\Framework\Locale\FormatInterface;
  12. use Magento\Framework\Math\Division as MathDivision;
  13. /**
  14. * Interface StockStateProvider
  15. */
  16. class StockStateProvider implements StockStateProviderInterface
  17. {
  18. /**
  19. * @var MathDivision
  20. */
  21. protected $mathDivision;
  22. /**
  23. * @var FormatInterface
  24. */
  25. protected $localeFormat;
  26. /**
  27. * @var ObjectFactory
  28. */
  29. protected $objectFactory;
  30. /**
  31. * @var ProductFactory
  32. */
  33. protected $productFactory;
  34. /**
  35. * @var bool
  36. */
  37. protected $qtyCheckApplicable;
  38. /**
  39. * @param MathDivision $mathDivision
  40. * @param FormatInterface $localeFormat
  41. * @param ObjectFactory $objectFactory
  42. * @param ProductFactory $productFactory
  43. * @param bool $qtyCheckApplicable
  44. */
  45. public function __construct(
  46. MathDivision $mathDivision,
  47. FormatInterface $localeFormat,
  48. ObjectFactory $objectFactory,
  49. ProductFactory $productFactory,
  50. $qtyCheckApplicable = true
  51. ) {
  52. $this->mathDivision = $mathDivision;
  53. $this->localeFormat = $localeFormat;
  54. $this->objectFactory = $objectFactory;
  55. $this->productFactory = $productFactory;
  56. $this->qtyCheckApplicable = $qtyCheckApplicable;
  57. }
  58. /**
  59. * Validate stock
  60. *
  61. * @param StockItemInterface $stockItem
  62. * @return bool
  63. */
  64. public function verifyStock(StockItemInterface $stockItem)
  65. {
  66. if ($stockItem->getQty() === null && $stockItem->getManageStock()) {
  67. return false;
  68. }
  69. if ($stockItem->getBackorders() == StockItemInterface::BACKORDERS_NO
  70. && $stockItem->getQty() <= $stockItem->getMinQty()
  71. ) {
  72. return false;
  73. }
  74. return true;
  75. }
  76. /**
  77. * Verify notification
  78. *
  79. * @param StockItemInterface $stockItem
  80. * @return bool
  81. */
  82. public function verifyNotification(StockItemInterface $stockItem)
  83. {
  84. return (float)$stockItem->getQty() < $stockItem->getNotifyStockQty();
  85. }
  86. /**
  87. * Validate quote qty
  88. *
  89. * @param StockItemInterface $stockItem
  90. * @param int|float $qty
  91. * @param int|float $summaryQty
  92. * @param int|float $origQty
  93. * @return \Magento\Framework\DataObject
  94. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  95. * @SuppressWarnings(PHPMD.NPathComplexity)
  96. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  97. */
  98. public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQty, $origQty = 0)
  99. {
  100. $result = $this->objectFactory->create();
  101. $result->setHasError(false);
  102. $qty = $this->getNumber($qty);
  103. /**
  104. * Check quantity type
  105. */
  106. $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal());
  107. if (!$stockItem->getIsQtyDecimal()) {
  108. $result->setHasQtyOptionUpdate(true);
  109. $qty = (int) $qty;
  110. /**
  111. * Adding stock data to quote item
  112. */
  113. $result->setItemQty($qty);
  114. $qty = $this->getNumber($qty);
  115. $origQty = (int) $origQty;
  116. $result->setOrigQty($origQty);
  117. }
  118. if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) {
  119. $result->setHasError(true)
  120. ->setMessage(__('The fewest you may purchase is %1.', $stockItem->getMinSaleQty() * 1))
  121. ->setErrorCode('qty_min')
  122. ->setQuoteMessage(__('Please correct the quantity for some products.'))
  123. ->setQuoteMessageIndex('qty');
  124. return $result;
  125. }
  126. if ($stockItem->getMaxSaleQty() && $qty > $stockItem->getMaxSaleQty()) {
  127. $result->setHasError(true)
  128. ->setMessage(__('The most you may purchase is %1.', $stockItem->getMaxSaleQty() * 1))
  129. ->setErrorCode('qty_max')
  130. ->setQuoteMessage(__('Please correct the quantity for some products.'))
  131. ->setQuoteMessageIndex('qty');
  132. return $result;
  133. }
  134. $result->addData($this->checkQtyIncrements($stockItem, $qty)->getData());
  135. if ($result->getHasError()) {
  136. return $result;
  137. }
  138. if (!$stockItem->getManageStock()) {
  139. return $result;
  140. }
  141. if (!$stockItem->getIsInStock()) {
  142. $result->setHasError(true)
  143. ->setMessage(__('This product is out of stock.'))
  144. ->setQuoteMessage(__('Some of the products are out of stock.'))
  145. ->setQuoteMessageIndex('stock');
  146. $result->setItemUseOldQty(true);
  147. return $result;
  148. }
  149. if (!$this->checkQty($stockItem, $summaryQty) || !$this->checkQty($stockItem, $qty)) {
  150. $message = __('The requested qty is not available');
  151. $result->setHasError(true)->setMessage($message)->setQuoteMessage($message)->setQuoteMessageIndex('qty');
  152. return $result;
  153. } else {
  154. if ($stockItem->getQty() - $summaryQty < 0) {
  155. if ($stockItem->getProductName()) {
  156. if ($stockItem->getIsChildItem()) {
  157. $backOrderQty = $stockItem->getQty() > 0 ? ($summaryQty - $stockItem->getQty()) * 1 : $qty * 1;
  158. if ($backOrderQty > $qty) {
  159. $backOrderQty = $qty;
  160. }
  161. $result->setItemBackorders($backOrderQty);
  162. } else {
  163. $orderedItems = (int)$stockItem->getOrderedItems();
  164. // Available item qty in stock excluding item qty in other quotes
  165. $qtyAvailable = ($stockItem->getQty() - ($summaryQty - $qty)) * 1;
  166. if ($qtyAvailable > 0) {
  167. $backOrderQty = $qty * 1 - $qtyAvailable;
  168. } else {
  169. $backOrderQty = $qty * 1;
  170. }
  171. if ($backOrderQty > 0) {
  172. $result->setItemBackorders($backOrderQty);
  173. }
  174. $stockItem->setOrderedItems($orderedItems + $qty);
  175. }
  176. if ($stockItem->getBackorders() == \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NOTIFY) {
  177. if (!$stockItem->getIsChildItem()) {
  178. $result->setMessage(
  179. __(
  180. 'We don\'t have as many "%1" as you requested, '
  181. . 'but we\'ll back order the remaining %2.',
  182. $stockItem->getProductName(),
  183. $backOrderQty * 1
  184. )
  185. );
  186. } else {
  187. $result->setMessage(
  188. __(
  189. 'We don\'t have "%1" in the requested quantity, '
  190. . 'so we\'ll back order the remaining %2.',
  191. $stockItem->getProductName(),
  192. $backOrderQty * 1
  193. )
  194. );
  195. }
  196. } elseif ($stockItem->getShowDefaultNotificationMessage()) {
  197. $result->setMessage(
  198. __('The requested qty is not available')
  199. );
  200. }
  201. }
  202. } else {
  203. if (!$stockItem->getIsChildItem()) {
  204. $stockItem->setOrderedItems($qty + (int)$stockItem->getOrderedItems());
  205. }
  206. }
  207. }
  208. return $result;
  209. }
  210. /**
  211. * Check quantity
  212. *
  213. * @param StockItemInterface $stockItem
  214. * @param int|float $qty
  215. * @exception \Magento\Framework\Exception\LocalizedException
  216. * @return bool
  217. */
  218. public function checkQty(StockItemInterface $stockItem, $qty)
  219. {
  220. if (!$this->qtyCheckApplicable) {
  221. return true;
  222. }
  223. if (!$stockItem->getManageStock()) {
  224. return true;
  225. }
  226. if ($stockItem->getQty() - $stockItem->getMinQty() - $qty < 0) {
  227. switch ($stockItem->getBackorders()) {
  228. case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NONOTIFY:
  229. case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NOTIFY:
  230. break;
  231. default:
  232. return false;
  233. }
  234. }
  235. return true;
  236. }
  237. /**
  238. * Returns suggested qty
  239. *
  240. * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions
  241. * or original qty if such value does not exist
  242. *
  243. * @param StockItemInterface $stockItem
  244. * @param int|float $qty
  245. * @return int|float
  246. */
  247. public function suggestQty(StockItemInterface $stockItem, $qty)
  248. {
  249. // We do not manage stock
  250. if ($qty <= 0 || !$stockItem->getManageStock()) {
  251. return $qty;
  252. }
  253. $qtyIncrements = (int)$stockItem->getQtyIncrements();
  254. // Currently only integer increments supported
  255. if ($qtyIncrements < 2) {
  256. return $qty;
  257. }
  258. $minQty = max($stockItem->getMinSaleQty(), $qtyIncrements);
  259. $divisibleMin = ceil($minQty / $qtyIncrements) * $qtyIncrements;
  260. $maxQty = min($stockItem->getQty() - $stockItem->getMinQty(), $stockItem->getMaxSaleQty());
  261. $divisibleMax = floor($maxQty / $qtyIncrements) * $qtyIncrements;
  262. if ($qty < $minQty || $qty > $maxQty || $divisibleMin > $divisibleMax) {
  263. // Do not perform rounding for qty that does not satisfy min/max conditions to not confuse customer
  264. return $qty;
  265. }
  266. // Suggest value closest to given qty
  267. $closestDivisibleLeft = floor($qty / $qtyIncrements) * $qtyIncrements;
  268. $closestDivisibleRight = $closestDivisibleLeft + $qtyIncrements;
  269. $acceptableLeft = min(max($divisibleMin, $closestDivisibleLeft), $divisibleMax);
  270. $acceptableRight = max(min($divisibleMax, $closestDivisibleRight), $divisibleMin);
  271. return abs($acceptableLeft - $qty) < abs($acceptableRight - $qty) ? $acceptableLeft : $acceptableRight;
  272. }
  273. /**
  274. * Check Qty Increments
  275. *
  276. * @param StockItemInterface $stockItem
  277. * @param float|int $qty
  278. * @return \Magento\Framework\DataObject
  279. */
  280. public function checkQtyIncrements(StockItemInterface $stockItem, $qty)
  281. {
  282. $result = new \Magento\Framework\DataObject();
  283. if ($stockItem->getSuppressCheckQtyIncrements()) {
  284. return $result;
  285. }
  286. $qtyIncrements = $stockItem->getQtyIncrements() * 1;
  287. if ($qtyIncrements && $this->mathDivision->getExactDivision($qty, $qtyIncrements) != 0) {
  288. $result->setHasError(true)
  289. ->setQuoteMessage(__('Please correct the quantity for some products.'))
  290. ->setErrorCode('qty_increments')
  291. ->setQuoteMessageIndex('qty');
  292. if ($stockItem->getIsChildItem()) {
  293. $result->setMessage(
  294. __(
  295. 'You can buy %1 only in quantities of %2 at a time.',
  296. $stockItem->getProductName(),
  297. $qtyIncrements
  298. )
  299. );
  300. } else {
  301. $result->setMessage(__('You can buy this product only in quantities of %1 at a time.', $qtyIncrements));
  302. }
  303. }
  304. return $result;
  305. }
  306. /**
  307. * Retrieve stock qty whether product is composite or no
  308. *
  309. * @param StockItemInterface $stockItem
  310. * @return float
  311. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  312. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  313. */
  314. public function getStockQty(StockItemInterface $stockItem)
  315. {
  316. if (!$stockItem->hasStockQty()) {
  317. $stockItem->setStockQty(0);
  318. $product = $this->productFactory->create();
  319. $product->load($stockItem->getProductId());
  320. // prevent possible recursive loop
  321. if (!$product->isComposite()) {
  322. $stockQty = $stockItem->getQty();
  323. } else {
  324. $stockQty = null;
  325. $productsByGroups = $product->getTypeInstance()->getProductsToPurchaseByReqGroups($product);
  326. foreach ($productsByGroups as $productsInGroup) {
  327. $qty = 0;
  328. foreach ($productsInGroup as $childProduct) {
  329. $qty += $this->getStockQty($stockItem);
  330. }
  331. if (null === $stockQty || $qty < $stockQty) {
  332. $stockQty = $qty;
  333. }
  334. }
  335. }
  336. $stockQty = (float)$stockQty;
  337. if ($stockQty < 0 || !$stockItem->getManageStock() || !$stockItem->getIsInStock()
  338. || !$product->isSaleable()
  339. ) {
  340. $stockQty = 0;
  341. }
  342. $stockItem->setStockQty($stockQty);
  343. }
  344. return (float)$stockItem->getData('stock_qty');
  345. }
  346. /**
  347. * Get numeric qty
  348. *
  349. * @param string|float|int|null $qty
  350. * @return float|null
  351. */
  352. protected function getNumber($qty)
  353. {
  354. if (!is_numeric($qty)) {
  355. $qty = $this->localeFormat->getNumber($qty);
  356. return $qty;
  357. }
  358. return $qty;
  359. }
  360. }