diff --git a/vendor/magento/module-inventory-sales/Model/GetBackorder.php b/vendor/magento/module-inventory-sales/Model/GetBackorder.php
new file mode 100644
index 000000000000..06d0ca64a3fb
--- /dev/null
+++ b/vendor/magento/module-inventory-sales/Model/GetBackorder.php
@@ -0,0 +1,187 @@
+objectFactory = $objectFactory;
+ $this->format = $format;
+ $this->getSkusByProductIds = $getSkusByProductIds;
+ $this->storeManager = $storeManager;
+ $this->stockResolver = $stockResolver;
+ $this->isProductSalableForRequestedQtyRequestInterfaceFactory = $isProductSalableForRequestedQtyRequestFactory;
+ $this->areProductsSalableForRequestedQty = $areProductsSalableForRequestedQty;
+ $this->backOrderNotifyCustomerCondition = $backOrderNotifyCustomerCondition;
+ $this->getBackorderQty = $getBackorderQty
+ ?? ObjectManager::getInstance()->get(GetBackorderQty::class);
+ }
+
+ /**
+ * Main execute function
+ *
+ * @param int $productId
+ * @param int|float $itemQty
+ * @param int|float $qtyToCheck
+ * @param int|null $scopeId
+ *
+ * @return DataObject
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute(int $productId, $itemQty, $qtyToCheck, $scopeId): object
+ {
+ $result = $this->objectFactory->create();
+ $result->setHasError(false);
+
+ $qty = max($this->getNumber($itemQty), $this->getNumber($qtyToCheck));
+
+ $skus = $this->getSkusByProductIds->execute([$productId]);
+ $productSku = $skus[$productId];
+
+ $websiteCode = $this->storeManager->getWebsite($scopeId)->getCode();
+ $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
+ $stockId = $stock->getStockId();
+
+ $request = $this->isProductSalableForRequestedQtyRequestInterfaceFactory->create(
+ [
+ 'sku' => $productSku,
+ 'qty' => $qty,
+ ]
+ );
+ $productsSalableResult = $this->areProductsSalableForRequestedQty->execute([$request], (int)$stockId);
+ $productsSalableResult = current($productsSalableResult);
+
+ if ($productsSalableResult->isSalable() === false) {
+ /** @var ProductSalabilityError $error */
+ foreach ($productsSalableResult->getErrors() as $error) {
+ $result->setHasError(true)->setMessage($error->getMessage())->setQuoteMessage($error->getMessage())
+ ->setQuoteMessageIndex('qty')->setErrorCode($error->getCode());
+ }
+ } else {
+ $productSalableResult = $this->backOrderNotifyCustomerCondition->execute($productSku, (int)$stockId, $qty);
+ if ($productSalableResult->getErrors()) {
+ /** @var ProductSalabilityError $error */
+ foreach ($productSalableResult->getErrors() as $error) {
+ $result->setMessage($error->getMessage());
+ }
+ }
+ $backorderQty = $this->getBackorderQty->execute($productSku, (int)$stockId, $qty);
+ if ($backorderQty > 0) {
+ $result->setItemBackorders($backorderQty);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Convert quantity to a valid float
+ *
+ * @param int|float $qty
+ *
+ * @return float
+ */
+ private function getNumber($qty): float
+ {
+ if (!is_numeric($qty)) {
+ return $this->format->getNumber($qty);
+ }
+
+ return $qty;
+ }
+}
\ No newline at end of file
diff --git a/vendor/magento/module-inventory-sales/Model/GetBackorderQty.php b/vendor/magento/module-inventory-sales/Model/GetBackorderQty.php
new file mode 100644
index 000000000000..f821288389a7
--- /dev/null
+++ b/vendor/magento/module-inventory-sales/Model/GetBackorderQty.php
@@ -0,0 +1,120 @@
+getStockItemConfiguration = $getStockItemConfig;
+ $this->getStockItemData = $getStockItemData;
+ $this->getProductSalableQty = $getProductSalableQty;
+ }
+
+ /**
+ * Main execute function
+ *
+ * Calculate the amount of a particular product that is to be backordered based on the
+ * salable quantity available, given its sku, stockId and the amount requested.
+ *
+ * @param string $sku
+ * @param integer $stockId
+ * @param float $requestedQty
+ * @return float
+ */
+ public function execute(string $sku, int $stockId, float $requestedQty): float
+ {
+ $stockItemConfiguration = $this->getStockItemConfiguration->execute($sku, $stockId);
+ $backOrderQty = 0;
+
+ if ($stockItemConfiguration->isManageStock()
+ && ($stockItemConfiguration->getBackorders() === StockItemConfigurationInterface::BACKORDERS_YES_NOTIFY ||
+ $stockItemConfiguration->getBackorders() === StockItemConfigurationInterface::BACKORDERS_YES_NONOTIFY)
+ ) {
+ $stockItemData = $this->getStockItemData->execute($sku, $stockId);
+ if (null === $stockItemData) {
+ return $backOrderQty;
+ }
+
+ $salableQty = $this->getProductSalableQty->execute($sku, $stockId);
+ /**
+ * Salable Quantity has had minQty subtracted from it. This means that when minqty is a negative value
+ * it is increasing the value of salable quantity to incorporate the fact that we are allowed to sell
+ * more items than in stock. When calculating backorder quantity to be displayed to customer and to be
+ * logged in sales_order_item, we don't want salable qty to include this, we want the backorder qty to
+ * indicate how many more items than we have in stock are being ordered including any existing
+ * reservations.
+ *
+ * Example calculation
+ *
+ * instock = 10
+ * minqty = -10
+ *
+ * Therefore $salableQty = 20
+ *
+ * $requestedQty = 15
+ *
+ * Therefore the amount to be backordered would be 15 - 20 - (-10) = 5
+ */
+ $minqty = $stockItemConfiguration->getMinQty();
+ $backOrderQty = $requestedQty - $salableQty - $minqty;
+ /**
+ * In cases of more stock available than being ordered, the above calculation returns a
+ * negative result indicating more stock available than requested. Set to zero in this case
+ */
+ if ($backOrderQty < 0) {
+ $backOrderQty = 0;
+ }
+ /**
+ * Catch the situation where we are already in backorders. e.g 0 in stock, reservations already
+ * in for 5 items on other orders, and we get a request for 5 more. Without this check, we would
+ * end up notifying the customer that 10 needed to be backorderd (10 backorderd items are required
+ * to satisfy this order plus all reservations), but only 5 of them are relevant to this
+ * request/customer/order so always cap the backOrderQty to the amount requested.
+ */
+ if (bccomp((string)$requestedQty, (string)$backOrderQty, 4) < 0) {
+ $backOrderQty = $requestedQty;
+ }
+ }
+ return $backOrderQty;
+ }
+}
diff --git a/vendor/magento/module-inventory-sales/Model/IsProductSalableCondition/BackOrderNotifyCustomerCondition.php b/vendor/magento/module-inventory-sales/Model/IsProductSalableCondition/BackOrderNotifyCustomerCondition.php
index ea24fb973ca4..45cb6dcddaf5 100644
--- a/vendor/magento/module-inventory-sales/Model/IsProductSalableCondition/BackOrderNotifyCustomerCondition.php
+++ b/vendor/magento/module-inventory-sales/Model/IsProductSalableCondition/BackOrderNotifyCustomerCondition.php
@@ -16,6 +16,7 @@
use Magento\InventorySalesApi\Api\IsProductSalableForRequestedQtyInterface;
use Magento\InventorySalesApi\Model\GetStockItemDataInterface;
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
+use Magento\InventorySales\Model\GetBackorderQty;
/**
* Get back order notify for customer condition
@@ -29,46 +30,43 @@ class BackOrderNotifyCustomerCondition implements IsProductSalableForRequestedQt
*/
private $getStockItemConfiguration;
- /**
- * @var GetStockItemDataInterface
- */
- private $getStockItemData;
-
/**
* @var ProductSalableResultInterfaceFactory
*/
private $productSalableResultFactory;
/**
- * @var GetProductSalableQtyInterface
+ * @var ProductSalabilityErrorInterfaceFactory
*/
- private $getProductSalableQty;
+ private $productSalabilityErrorFactory;
/**
- * @var ProductSalabilityErrorInterfaceFactory
+ * @var GetBackorderQty
*/
- private $productSalabilityErrorFactory;
+ private $getBackorderQty;
/**
* @param GetStockItemConfigurationInterface $getStockItemConfiguration
- * @param GetStockItemDataInterface $getStockItemData
+ * @param GetStockItemDataInterface $getStockItemData @deprecated
* @param ProductSalableResultInterfaceFactory $productSalableResultFactory
* @param ProductSalabilityErrorInterfaceFactory $productSalabilityErrorFactory
- * @param GetProductSalableQtyInterface|null $getProductSalableQty
+ * @param GetProductSalableQtyInterface|null $getProductSalableQty @deprecated
+ * @param GetBackorderQty|null $getBackorderQty
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
GetStockItemConfigurationInterface $getStockItemConfiguration,
GetStockItemDataInterface $getStockItemData,
ProductSalableResultInterfaceFactory $productSalableResultFactory,
ProductSalabilityErrorInterfaceFactory $productSalabilityErrorFactory,
- ?GetProductSalableQtyInterface $getProductSalableQty = null
+ ?GetProductSalableQtyInterface $getProductSalableQty = null,
+ ?GetBackorderQty $getBackorderQty = null
) {
$this->getStockItemConfiguration = $getStockItemConfiguration;
- $this->getStockItemData = $getStockItemData;
$this->productSalableResultFactory = $productSalableResultFactory;
$this->productSalabilityErrorFactory = $productSalabilityErrorFactory;
- $this->getProductSalableQty = $getProductSalableQty
- ?? ObjectManager::getInstance()->get(GetProductSalableQtyInterface::class);
+ $this->getBackorderQty = $getBackorderQty
+ ?? ObjectManager::getInstance()->get(GetBackorderQty::class);
}
/**
@@ -81,23 +79,17 @@ public function execute(string $sku, int $stockId, float $requestedQty): Product
if ($stockItemConfiguration->isManageStock()
&& $stockItemConfiguration->getBackorders() === StockItemConfigurationInterface::BACKORDERS_YES_NOTIFY
) {
- $stockItemData = $this->getStockItemData->execute($sku, $stockId);
- if (null === $stockItemData) {
- return $this->productSalableResultFactory->create(['errors' => []]);
- }
- $salableQty = $this->getProductSalableQty->execute($sku, $stockId);
- $remainingQty = ($salableQty > $requestedQty) ? $salableQty - $requestedQty : $requestedQty - $salableQty;
- $displayQty = $this->getDisplayQty($remainingQty, $salableQty, $requestedQty);
+ $backorderQty = $this->getBackorderQty->execute($sku, $stockId, $requestedQty);
- if ($requestedQty > $stockItemData[GetStockItemDataInterface::QUANTITY] && $displayQty >= 0) {
+ if ($backorderQty > 0) {
$errors = [
$this->productSalabilityErrorFactory->create([
- 'code' => 'back_order-not-enough',
- 'message' => __(
- 'We don\'t have as many quantity as you requested, '
- . 'but we\'ll back order the remaining %1.',
- $displayQty * 1
- )])
+ 'code' => 'back_order-not-enough',
+ 'message' => __(
+ 'We don\'t have as many quantity as you requested, '
+ . 'but we\'ll back order the remaining %1.',
+ $backorderQty * 1
+ )])
];
return $this->productSalableResultFactory->create(['errors' => $errors]);
}
@@ -105,23 +97,4 @@ public function execute(string $sku, int $stockId, float $requestedQty): Product
return $this->productSalableResultFactory->create(['errors' => []]);
}
-
- /**
- * Get display quantity to show the number of quantity customer can backorder
- *
- * @param float $remainingQty
- * @param float $salableQty
- * @param float $requestedQty
- * @return float
- */
- private function getDisplayQty(float $remainingQty, float $salableQty, float $requestedQty): float
- {
- $displayQty = 0;
- if ($remainingQty > 0 && $salableQty > 0) {
- $displayQty = $remainingQty;
- } elseif ($requestedQty > $salableQty) {
- $displayQty = $requestedQty;
- }
- return $displayQty;
- }
-}
+}
\ No newline at end of file
diff --git a/vendor/magento/module-inventory-sales/Plugin/StockState/CheckQuoteItemQtyPlugin.php b/vendor/magento/module-inventory-sales/Plugin/StockState/CheckQuoteItemQtyPlugin.php
index c4210e65b1db..7b105ca459af 100644
--- a/vendor/magento/module-inventory-sales/Plugin/StockState/CheckQuoteItemQtyPlugin.php
+++ b/vendor/magento/module-inventory-sales/Plugin/StockState/CheckQuoteItemQtyPlugin.php
@@ -7,20 +7,12 @@
namespace Magento\InventorySales\Plugin\StockState;
+use Magento\Framework\App\ObjectManager;
use Magento\CatalogInventory\Api\StockStateInterface;
use Magento\Framework\DataObject;
-use Magento\Framework\DataObject\Factory as ObjectFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
-use Magento\Framework\Locale\FormatInterface;
-use Magento\InventoryCatalogApi\Model\GetSkusByProductIdsInterface;
-use Magento\InventorySales\Model\IsProductSalableCondition\BackOrderNotifyCustomerCondition;
-use Magento\InventorySales\Model\IsProductSalableForRequestedQtyCondition\ProductSalabilityError;
-use Magento\InventorySalesApi\Api\AreProductsSalableForRequestedQtyInterface;
-use Magento\InventorySalesApi\Api\Data\IsProductSalableForRequestedQtyRequestInterfaceFactory;
-use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
-use Magento\InventorySalesApi\Api\StockResolverInterface;
-use Magento\Store\Model\StoreManagerInterface;
+use Magento\InventorySales\Model\GetBackorder;
/**
* Replace legacy quote item check
@@ -29,74 +21,18 @@
class CheckQuoteItemQtyPlugin
{
/**
- * @var ObjectFactory
+ * @var GetBackorder
*/
- private $objectFactory;
+ private $getBackorder;
/**
- * @var FormatInterface
- */
- private $format;
-
- /**
- * @var AreProductsSalableForRequestedQtyInterface
- */
- private $areProductsSalableForRequestedQty;
-
- /**
- * @var IsProductSalableForRequestedQtyRequestInterfaceFactory
- */
- private $isProductSalableForRequestedQtyRequestInterfaceFactory;
-
- /**
- * @var GetSkusByProductIdsInterface
- */
- private $getSkusByProductIds;
-
- /**
- * @var StockResolverInterface
- */
- private $stockResolver;
-
- /**
- * @var StoreManagerInterface
- */
- private $storeManager;
-
- /**
- * @var BackOrderNotifyCustomerCondition
- */
- private $backOrderNotifyCustomerCondition;
-
- /**
- * @param ObjectFactory $objectFactory
- * @param FormatInterface $format
- * @param AreProductsSalableForRequestedQtyInterface $areProductsSalableForRequestedQty
- * @param IsProductSalableForRequestedQtyRequestInterfaceFactory $isProductSalableForRequestedQtyRequestFactory
- * @param GetSkusByProductIdsInterface $getSkusByProductIds
- * @param StockResolverInterface $stockResolver
- * @param StoreManagerInterface $storeManager
- * @param BackOrderNotifyCustomerCondition $backOrderNotifyCustomerCondition
- * @SuppressWarnings(PHPMD.LongVariable)
+ * @param GetBackorder|null $getBackorder
*/
public function __construct(
- ObjectFactory $objectFactory,
- FormatInterface $format,
- AreProductsSalableForRequestedQtyInterface $areProductsSalableForRequestedQty,
- IsProductSalableForRequestedQtyRequestInterfaceFactory $isProductSalableForRequestedQtyRequestFactory,
- GetSkusByProductIdsInterface $getSkusByProductIds,
- StockResolverInterface $stockResolver,
- StoreManagerInterface $storeManager,
- BackOrderNotifyCustomerCondition $backOrderNotifyCustomerCondition
+ GetBackorder $getBackorder = null
) {
- $this->objectFactory = $objectFactory;
- $this->format = $format;
- $this->areProductsSalableForRequestedQty = $areProductsSalableForRequestedQty;
- $this->isProductSalableForRequestedQtyRequestInterfaceFactory = $isProductSalableForRequestedQtyRequestFactory;
- $this->getSkusByProductIds = $getSkusByProductIds;
- $this->stockResolver = $stockResolver;
- $this->storeManager = $storeManager;
- $this->backOrderNotifyCustomerCondition = $backOrderNotifyCustomerCondition;
+ $this->getBackorder = $getBackorder
+ ?? ObjectManager::getInstance()->get(GetBackorder::class);
}
/**
@@ -124,62 +60,6 @@ public function aroundCheckQuoteItemQty(
$origQty,
$scopeId = null
) {
- $result = $this->objectFactory->create();
- $result->setHasError(false);
-
- $qty = max($this->getNumber($itemQty), $this->getNumber($qtyToCheck));
-
- $skus = $this->getSkusByProductIds->execute([$productId]);
- $productSku = $skus[$productId];
-
- $websiteCode = $this->storeManager->getWebsite($scopeId)->getCode();
- $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
- $stockId = $stock->getStockId();
-
- $request = $this->isProductSalableForRequestedQtyRequestInterfaceFactory->create(
- [
- 'sku' => $productSku,
- 'qty' => $qty,
- ]
- );
- $productsSalableResult = $this->areProductsSalableForRequestedQty->execute([$request], (int)$stockId);
- $productsSalableResult = current($productsSalableResult);
-
- if ($productsSalableResult->isSalable() === false) {
- /** @var ProductSalabilityError $error */
- foreach ($productsSalableResult->getErrors() as $error) {
- $result->setHasError(true)
- ->setMessage($error->getMessage())
- ->setQuoteMessage($error->getMessage())
- ->setQuoteMessageIndex('qty')
- ->setErrorCode($error->getCode());
- }
- } else {
- $productSalableResult = $this->backOrderNotifyCustomerCondition->execute($productSku, (int)$stockId, $qty);
- if ($productSalableResult->getErrors()) {
- /** @var ProductSalabilityError $error */
- foreach ($productSalableResult->getErrors() as $error) {
- $result->setMessage($error->getMessage());
- }
- }
- }
-
- return $result;
- }
-
- /**
- * Convert quantity to a valid float
- *
- * @param string|float|int|null $qty
- *
- * @return float|null
- */
- private function getNumber($qty)
- {
- if (!is_numeric($qty)) {
- return $this->format->getNumber($qty);
- }
-
- return $qty;
+ return $this->getBackorder->execute((int) $productId, $itemQty, $qtyToCheck, $scopeId);
}
-}
+}
\ No newline at end of file
diff --git a/vendor/magento/module-inventory-sales/Plugin/StockStateProvider/CheckQuoteItemQtyPlugin.php b/vendor/magento/module-inventory-sales/Plugin/StockStateProvider/CheckQuoteItemQtyPlugin.php
new file mode 100644
index 000000000000..aee35b748005
--- /dev/null
+++ b/vendor/magento/module-inventory-sales/Plugin/StockStateProvider/CheckQuoteItemQtyPlugin.php
@@ -0,0 +1,104 @@
+objectFactory = $objectFactory
+ ?? ObjectManager::getInstance()->get(ObjectFactory::class);
+ $this->getProductTypesBySkus = $getProductTypesBySkus
+ ?? ObjectManager::getInstance()->get(GetProductTypesBySkusInterface::class);
+ $this->getSkusByProductIds = $getSkusByProductIds
+ ?? ObjectManager::getInstance()->get(GetSkusByProductIdsInterface::class);
+ $this->getBackorder = $getBackorder
+ ?? ObjectManager::getInstance()->get(GetBackorder::class);
+ }
+
+ /**
+ * Replace legacy quote item check
+ *
+ * @param StockStateProviderInterface $subject
+ * @param \Closure $proceed
+ * @param StockItemInterface $stockItem
+ * @param int|float $itemQty
+ * @param int|float $qtyToCheck
+ * @param int|float $origQty
+ *
+ * @return DataObject
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function aroundCheckQuoteItemQty(
+ StockStateProviderInterface $subject,
+ \Closure $proceed,
+ StockItemInterface $stockItem,
+ $itemQty,
+ $qtyToCheck,
+ $origQty = 0
+ ) {
+ $productId = $stockItem->getProductId();
+ $skus = $this->getSkusByProductIds->execute([$productId]);
+ $productSku = $skus[$productId];
+ $productType = $this->getProductTypesBySkus->execute([$productSku])[$productSku];
+ if ($productType !== Type::TYPE_SIMPLE) {
+ $result = $this->objectFactory->create();
+ $result->setHasError(false);
+ return $result;
+ }
+ return $this->getBackorder->execute((int) $productId, $itemQty, $qtyToCheck, null);
+ }
+}
diff --git a/vendor/magento/module-inventory-sales/etc/di.xml b/vendor/magento/module-inventory-sales/etc/di.xml
index 12b95a877435..7921fb927a2f 100644
--- a/vendor/magento/module-inventory-sales/etc/di.xml
+++ b/vendor/magento/module-inventory-sales/etc/di.xml
@@ -25,6 +25,9 @@
+
+
+
@@ -200,4 +203,4 @@
-
+
\ No newline at end of file