123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- <?php
- /**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
- namespace Magento\CatalogInventory\Model;
- use Magento\Catalog\Model\ProductFactory;
- use Magento\CatalogInventory\Api\Data\StockItemInterface;
- use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface;
- use Magento\Framework\DataObject\Factory as ObjectFactory;
- use Magento\Framework\Locale\FormatInterface;
- use Magento\Framework\Math\Division as MathDivision;
- /**
- * Interface StockStateProvider
- */
- class StockStateProvider implements StockStateProviderInterface
- {
- /**
- * @var MathDivision
- */
- protected $mathDivision;
- /**
- * @var FormatInterface
- */
- protected $localeFormat;
- /**
- * @var ObjectFactory
- */
- protected $objectFactory;
- /**
- * @var ProductFactory
- */
- protected $productFactory;
- /**
- * @var bool
- */
- protected $qtyCheckApplicable;
- /**
- * @param MathDivision $mathDivision
- * @param FormatInterface $localeFormat
- * @param ObjectFactory $objectFactory
- * @param ProductFactory $productFactory
- * @param bool $qtyCheckApplicable
- */
- public function __construct(
- MathDivision $mathDivision,
- FormatInterface $localeFormat,
- ObjectFactory $objectFactory,
- ProductFactory $productFactory,
- $qtyCheckApplicable = true
- ) {
- $this->mathDivision = $mathDivision;
- $this->localeFormat = $localeFormat;
- $this->objectFactory = $objectFactory;
- $this->productFactory = $productFactory;
- $this->qtyCheckApplicable = $qtyCheckApplicable;
- }
- /**
- * Validate stock
- *
- * @param StockItemInterface $stockItem
- * @return bool
- */
- public function verifyStock(StockItemInterface $stockItem)
- {
- if ($stockItem->getQty() === null && $stockItem->getManageStock()) {
- return false;
- }
- if ($stockItem->getBackorders() == StockItemInterface::BACKORDERS_NO
- && $stockItem->getQty() <= $stockItem->getMinQty()
- ) {
- return false;
- }
- return true;
- }
- /**
- * Verify notification
- *
- * @param StockItemInterface $stockItem
- * @return bool
- */
- public function verifyNotification(StockItemInterface $stockItem)
- {
- return (float)$stockItem->getQty() < $stockItem->getNotifyStockQty();
- }
- /**
- * Validate quote qty
- *
- * @param StockItemInterface $stockItem
- * @param int|float $qty
- * @param int|float $summaryQty
- * @param int|float $origQty
- * @return \Magento\Framework\DataObject
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- * @SuppressWarnings(PHPMD.NPathComplexity)
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQty, $origQty = 0)
- {
- $result = $this->objectFactory->create();
- $result->setHasError(false);
- $qty = $this->getNumber($qty);
- /**
- * Check quantity type
- */
- $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal());
- if (!$stockItem->getIsQtyDecimal()) {
- $result->setHasQtyOptionUpdate(true);
- $qty = (int) $qty;
- /**
- * Adding stock data to quote item
- */
- $result->setItemQty($qty);
- $qty = $this->getNumber($qty);
- $origQty = (int) $origQty;
- $result->setOrigQty($origQty);
- }
- if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) {
- $result->setHasError(true)
- ->setMessage(__('The fewest you may purchase is %1.', $stockItem->getMinSaleQty() * 1))
- ->setErrorCode('qty_min')
- ->setQuoteMessage(__('Please correct the quantity for some products.'))
- ->setQuoteMessageIndex('qty');
- return $result;
- }
- if ($stockItem->getMaxSaleQty() && $qty > $stockItem->getMaxSaleQty()) {
- $result->setHasError(true)
- ->setMessage(__('The most you may purchase is %1.', $stockItem->getMaxSaleQty() * 1))
- ->setErrorCode('qty_max')
- ->setQuoteMessage(__('Please correct the quantity for some products.'))
- ->setQuoteMessageIndex('qty');
- return $result;
- }
- $result->addData($this->checkQtyIncrements($stockItem, $qty)->getData());
- if ($result->getHasError()) {
- return $result;
- }
- if (!$stockItem->getManageStock()) {
- return $result;
- }
- if (!$stockItem->getIsInStock()) {
- $result->setHasError(true)
- ->setMessage(__('This product is out of stock.'))
- ->setQuoteMessage(__('Some of the products are out of stock.'))
- ->setQuoteMessageIndex('stock');
- $result->setItemUseOldQty(true);
- return $result;
- }
- if (!$this->checkQty($stockItem, $summaryQty) || !$this->checkQty($stockItem, $qty)) {
- $message = __('The requested qty is not available');
- $result->setHasError(true)->setMessage($message)->setQuoteMessage($message)->setQuoteMessageIndex('qty');
- return $result;
- } else {
- if ($stockItem->getQty() - $summaryQty < 0) {
- if ($stockItem->getProductName()) {
- if ($stockItem->getIsChildItem()) {
- $backOrderQty = $stockItem->getQty() > 0 ? ($summaryQty - $stockItem->getQty()) * 1 : $qty * 1;
- if ($backOrderQty > $qty) {
- $backOrderQty = $qty;
- }
- $result->setItemBackorders($backOrderQty);
- } else {
- $orderedItems = (int)$stockItem->getOrderedItems();
- // Available item qty in stock excluding item qty in other quotes
- $qtyAvailable = ($stockItem->getQty() - ($summaryQty - $qty)) * 1;
- if ($qtyAvailable > 0) {
- $backOrderQty = $qty * 1 - $qtyAvailable;
- } else {
- $backOrderQty = $qty * 1;
- }
- if ($backOrderQty > 0) {
- $result->setItemBackorders($backOrderQty);
- }
- $stockItem->setOrderedItems($orderedItems + $qty);
- }
- if ($stockItem->getBackorders() == \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NOTIFY) {
- if (!$stockItem->getIsChildItem()) {
- $result->setMessage(
- __(
- 'We don\'t have as many "%1" as you requested, '
- . 'but we\'ll back order the remaining %2.',
- $stockItem->getProductName(),
- $backOrderQty * 1
- )
- );
- } else {
- $result->setMessage(
- __(
- 'We don\'t have "%1" in the requested quantity, '
- . 'so we\'ll back order the remaining %2.',
- $stockItem->getProductName(),
- $backOrderQty * 1
- )
- );
- }
- } elseif ($stockItem->getShowDefaultNotificationMessage()) {
- $result->setMessage(
- __('The requested qty is not available')
- );
- }
- }
- } else {
- if (!$stockItem->getIsChildItem()) {
- $stockItem->setOrderedItems($qty + (int)$stockItem->getOrderedItems());
- }
- }
- }
- return $result;
- }
- /**
- * Check quantity
- *
- * @param StockItemInterface $stockItem
- * @param int|float $qty
- * @exception \Magento\Framework\Exception\LocalizedException
- * @return bool
- */
- public function checkQty(StockItemInterface $stockItem, $qty)
- {
- if (!$this->qtyCheckApplicable) {
- return true;
- }
- if (!$stockItem->getManageStock()) {
- return true;
- }
- if ($stockItem->getQty() - $stockItem->getMinQty() - $qty < 0) {
- switch ($stockItem->getBackorders()) {
- case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NONOTIFY:
- case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NOTIFY:
- break;
- default:
- return false;
- }
- }
- return true;
- }
- /**
- * Returns suggested qty
- *
- * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions
- * or original qty if such value does not exist
- *
- * @param StockItemInterface $stockItem
- * @param int|float $qty
- * @return int|float
- */
- public function suggestQty(StockItemInterface $stockItem, $qty)
- {
- // We do not manage stock
- if ($qty <= 0 || !$stockItem->getManageStock()) {
- return $qty;
- }
- $qtyIncrements = (int)$stockItem->getQtyIncrements();
- // Currently only integer increments supported
- if ($qtyIncrements < 2) {
- return $qty;
- }
- $minQty = max($stockItem->getMinSaleQty(), $qtyIncrements);
- $divisibleMin = ceil($minQty / $qtyIncrements) * $qtyIncrements;
- $maxQty = min($stockItem->getQty() - $stockItem->getMinQty(), $stockItem->getMaxSaleQty());
- $divisibleMax = floor($maxQty / $qtyIncrements) * $qtyIncrements;
- if ($qty < $minQty || $qty > $maxQty || $divisibleMin > $divisibleMax) {
- // Do not perform rounding for qty that does not satisfy min/max conditions to not confuse customer
- return $qty;
- }
- // Suggest value closest to given qty
- $closestDivisibleLeft = floor($qty / $qtyIncrements) * $qtyIncrements;
- $closestDivisibleRight = $closestDivisibleLeft + $qtyIncrements;
- $acceptableLeft = min(max($divisibleMin, $closestDivisibleLeft), $divisibleMax);
- $acceptableRight = max(min($divisibleMax, $closestDivisibleRight), $divisibleMin);
- return abs($acceptableLeft - $qty) < abs($acceptableRight - $qty) ? $acceptableLeft : $acceptableRight;
- }
- /**
- * Check Qty Increments
- *
- * @param StockItemInterface $stockItem
- * @param float|int $qty
- * @return \Magento\Framework\DataObject
- */
- public function checkQtyIncrements(StockItemInterface $stockItem, $qty)
- {
- $result = new \Magento\Framework\DataObject();
- if ($stockItem->getSuppressCheckQtyIncrements()) {
- return $result;
- }
- $qtyIncrements = $stockItem->getQtyIncrements() * 1;
- if ($qtyIncrements && $this->mathDivision->getExactDivision($qty, $qtyIncrements) != 0) {
- $result->setHasError(true)
- ->setQuoteMessage(__('Please correct the quantity for some products.'))
- ->setErrorCode('qty_increments')
- ->setQuoteMessageIndex('qty');
- if ($stockItem->getIsChildItem()) {
- $result->setMessage(
- __(
- 'You can buy %1 only in quantities of %2 at a time.',
- $stockItem->getProductName(),
- $qtyIncrements
- )
- );
- } else {
- $result->setMessage(__('You can buy this product only in quantities of %1 at a time.', $qtyIncrements));
- }
- }
- return $result;
- }
- /**
- * Retrieve stock qty whether product is composite or no
- *
- * @param StockItemInterface $stockItem
- * @return float
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- * @SuppressWarnings(PHPMD.UnusedLocalVariable)
- */
- public function getStockQty(StockItemInterface $stockItem)
- {
- if (!$stockItem->hasStockQty()) {
- $stockItem->setStockQty(0);
- $product = $this->productFactory->create();
- $product->load($stockItem->getProductId());
- // prevent possible recursive loop
- if (!$product->isComposite()) {
- $stockQty = $stockItem->getQty();
- } else {
- $stockQty = null;
- $productsByGroups = $product->getTypeInstance()->getProductsToPurchaseByReqGroups($product);
- foreach ($productsByGroups as $productsInGroup) {
- $qty = 0;
- foreach ($productsInGroup as $childProduct) {
- $qty += $this->getStockQty($stockItem);
- }
- if (null === $stockQty || $qty < $stockQty) {
- $stockQty = $qty;
- }
- }
- }
- $stockQty = (float)$stockQty;
- if ($stockQty < 0 || !$stockItem->getManageStock() || !$stockItem->getIsInStock()
- || !$product->isSaleable()
- ) {
- $stockQty = 0;
- }
- $stockItem->setStockQty($stockQty);
- }
- return (float)$stockItem->getData('stock_qty');
- }
- /**
- * Get numeric qty
- *
- * @param string|float|int|null $qty
- * @return float|null
- */
- protected function getNumber($qty)
- {
- if (!is_numeric($qty)) {
- $qty = $this->localeFormat->getNumber($qty);
- return $qty;
- }
- return $qty;
- }
- }
|