Skip to content

Commit

Permalink
MDEE-40:[Commerce Export] Implement stock_item_status feed (#11)
Browse files Browse the repository at this point in the history
* MDEE-40:[Commerce Export] Implement stock_item_status feed
- cover with tests: basic cases
  • Loading branch information
mslabko authored Oct 27, 2021
1 parent 2434187 commit 5531217
Show file tree
Hide file tree
Showing 16 changed files with 967 additions and 42 deletions.
6 changes: 4 additions & 2 deletions InventoryDataExporter/Model/Provider/InfiniteStock.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ private function getIsInfiniteStock(array $row, bool $configManageStock, bool $c
if (false === (bool)$row['useConfigManageStock'] && isset($row['manageStock'])) {
$isInfinite = !(bool)$row['manageStock'];
}
if (false === $isInfinite && false === (bool)$row['useConfigBackorders'] && isset($row['backorders'])) {
$isInfinite = (bool)$row['backorders'];
// With Backorders enabled, and Out-of-Stock Threshold = 0 allows for infinite backorders
if (false === $isInfinite && false === (bool)$row['useConfigBackorders']
&& false === (bool)$row['useConfigMinQty'] && isset($row['backorders'], $row['minQty'])) {
$isInfinite = (bool)$row['backorders'] && (float)$row['minQty'] === 0.0;
}
return $isInfinite;
}
Expand Down
21 changes: 14 additions & 7 deletions InventoryDataExporter/Model/Provider/StockStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\DB\Adapter\TableNotFoundException;
use Magento\InventoryDataExporter\Model\Query\InventoryStockQuery;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -79,22 +80,28 @@ public function get(array $values): array
$output = [];

try {
$select = $this->query->getQuery($skus);
$cursor = $connection->query($select);
$processedSkus = [];
while ($row = $cursor->fetch()) {
$processedSkus[] = $row['sku'];
$output[] = $this->fillWithDefaultValues($row);
$select = $this->query->getQuery($skus);
// $select can be null if no stocks exists except default
if ($select) {
$cursor = $connection->query($select);
while ($row = $cursor->fetch()) {
$processedSkus[] = $row['sku'];
$output[] = $this->fillWithDefaultValues($row);
}
}

$select = $this->query->getQueryForDefaultStock(\array_diff($skus, $processedSkus));
$cursor = $connection->query($select);
while ($row = $cursor->fetch()) {
$output[] = $this->fillWithDefaultValues($row);
}

} catch (TableNotFoundException $e) {
$this->logger->warning(
'StockStatus export warning. Inventory index should be run first. Error: ' . $e->getMessage(). ' '
);
} catch (\Throwable $e) {
$this->logger->error("StockStatus export error: " . $e->getMessage(), ['exception' => $e]);
$this->logger->error('StockStatus export error: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}

Expand Down
48 changes: 37 additions & 11 deletions InventoryDataExporter/Model/Query/InventoryStockQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ private function getTable(string $tableName): string
*
* @param array $skus
* @param bool $defaultStock
* @return Select
* @return Select|null
*/
public function getQuery(array $skus): Select
public function getQuery(array $skus): ?Select
{
$connection = $this->resourceConnection->getConnection();
$selects = [];
Expand All @@ -66,23 +66,49 @@ public function getQuery(array $skus): Select
}
$select = $connection->select()
->from(['isi' => $this->getTable(sprintf('inventory_stock_%s', $stockId))], [])
->where('isi.sku IN (?)', $skus)
->joinLeft(
[
'product' => $this->resourceConnection->getTableName('catalog_product_entity'),
],
'product.sku = isi.sku',
[]
)->joinLeft(
[
'stock_item' => $this->resourceConnection->getTableName('cataloginventory_stock_item'),
],
'stock_item.product_id = product.entity_id',
[]
)->where('isi.sku IN (?)', $skus)
->columns(
[
'qty' => "isi.quantity",
'isSalable' => "isi.is_salable",
'sku' => "isi.sku",
'stockId' => new Expression($stockId),
'manageStock' => new Expression(1),
'useConfigManageStock' => new Expression(1),
'backorders' => new Expression(0),
'useConfigBackorders' => new Expression(1),
'manageStock' => $connection->getCheckSql(
'stock_item.manage_stock IS NULL', 1, 'stock_item.manage_stock'
),
'useConfigManageStock' => $connection->getCheckSql(
'stock_item.use_config_manage_stock IS NULL', 1, 'stock_item.use_config_manage_stock'
),
'backorders' => $connection->getCheckSql(
'stock_item.backorders IS NULL', 0, 'stock_item.backorders'
),
'useConfigBackorders' => $connection->getCheckSql(
'stock_item.use_config_backorders IS NULL', 1, 'stock_item.use_config_backorders'
),
'useConfigMinQty' => $connection->getCheckSql(
'stock_item.use_config_min_qty IS NULL', 1, 'stock_item.use_config_min_qty'
),
'minQty' => $connection->getCheckSql(
'stock_item.min_qty IS NULL', 0, 'stock_item.min_qty'
),
]
);

$selects[] = $select;
}
return $connection->select()->union($selects, Select::SQL_UNION_ALL);
return $selects ? $connection->select()->union($selects, Select::SQL_UNION_ALL) : null;
}

/**
Expand All @@ -96,7 +122,7 @@ public function getQueryForDefaultStock(array $skus): Select
{
$connection = $this->resourceConnection->getConnection();
$stockId = $this->defaultStockProvider->getId();
$select = $connection->select()
return $connection->select()
->from(['isi' => $this->getTable(sprintf('inventory_stock_%s', $stockId))], [])
->where('isi.sku IN (?)', $skus)
->joinInner(
Expand All @@ -115,8 +141,9 @@ public function getQueryForDefaultStock(array $skus): Select
'useConfigManageStock' => 'stock_item.use_config_manage_stock',
'backorders' => 'stock_item.backorders',
'useConfigBackorders' => 'stock_item.use_config_backorders',
'useConfigMinQty' => 'stock_item.use_config_min_qty',
'minQty' => 'stock_item.min_qty',
]);
return $select;
}

/**
Expand All @@ -126,7 +153,6 @@ public function getQueryForDefaultStock(array $skus): Select
*/
private function getStocks(): array
{
// TODO: add batching
$connection = $this->resourceConnection->getConnection();
return $connection->fetchCol($connection->select()
->from(['stock' => $this->getTable('inventory_stock')], ['stock_id']));
Expand Down
21 changes: 9 additions & 12 deletions InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,18 @@ public function getStocksAssignedToSkus(array $skus): array
/**
* Mark stock statuses as deleted
*
* @param array $stocksToDelete
* @param array $idsToDelete
*/
public function markStockStatusesAsDeleted(array $stocksToDelete): void
public function markStockStatusesAsDeleted(array $idsToDelete): void
{
$connection = $this->resourceConnection->getConnection();
$feedTableName = $this->resourceConnection->getTableName($this->metadata->getFeedTableName());
foreach ($stocksToDelete as $stockId => $skus) {
$connection->update(
$feedTableName,
['is_deleted' => new \Zend_Db_Expr('1')],
[
'sku IN (?)' => $skus,
'stock_id = ?' => $stockId
]
);
}
$connection->update(
$feedTableName,
['is_deleted' => new \Zend_Db_Expr('1')],
[
'id IN (?)' => $idsToDelete
]
);
}
}
5 changes: 4 additions & 1 deletion InventoryDataExporter/Plugin/BulkSourceUnassign.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
namespace Magento\InventoryDataExporter\Plugin;

use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder;
use Magento\InventoryDataExporter\Model\Query\StockStatusDeleteQuery;

/**
Expand Down Expand Up @@ -60,7 +61,9 @@ private function getStocksToDelete(array $affectedSkus, array $deletedSources, $
foreach ($affectedSkus as $deletedItemSku) {
foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) {
if ($this->getContainsAllKeys($fetchedItemSources, $deletedSources)) {
$stocksToDelete[(string)$fetchedItemStockId][] = $deletedItemSku;
$stocksToDelete[] = StockStatusIdBuilder::build(
['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku]
);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion InventoryDataExporter/Plugin/MarkItemsAsDeleted.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use Magento\Inventory\Model\ResourceModel\SourceItem\DeleteMultiple;
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder;
use Magento\InventoryDataExporter\Model\Query\StockStatusDeleteQuery;

/**
Expand Down Expand Up @@ -65,7 +66,9 @@ private function getStocksToDelete(array $deletedSourceItems, $fetchedSourceItem
foreach ($deletedSourceItems as $deletedItemSku => $deletedItemSources) {
foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) {
if ($this->getContainsAllKeys($fetchedItemSources, $deletedItemSources)) {
$stocksToDelete[(string)$fetchedItemStockId][] = $deletedItemSku;
$stocksToDelete[] = StockStatusIdBuilder::build(
['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku]
);
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion InventoryDataExporter/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Release notes

*Magento_InventoryDataExporter* module
*Magento_InventoryDataExporter* module

https://docs.magento.com/user-guide/catalog/inventory-backorders.html?itm_source=devdocs&itm_medium=quick_search&itm_campaign=federated_search&itm_term=backorer


Zero
With Backorders enabled, entering 0 allows for infinite backorders.
163 changes: 163 additions & 0 deletions InventoryDataExporter/Test/Integration/ExportStockStatusTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\WebsiteDataExporter\Test\Integration;

use Magento\DataExporter\Export\Processor;
use Magento\TestFramework\Helper\Bootstrap;

/**
* @magentoDbIsolation disabled
* @magentoAppIsolation enabled
*/
class ExportStockStatusTest extends \PHPUnit\Framework\TestCase
{
/**
* @var Processor
*/
private $processor;

/**
* Setup tests
*/
protected function setUp(): void
{
parent::setUp();
$this->processor = Bootstrap::getObjectManager()->create(Processor::class);
}

/**
* @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php
*/
public function testExportStockStatuses()
{
$actualStockStatus = $this->processor->process(
'stock_statuses',
[
['sku' => 'product_in_EU_stock_with_2_sources'],
['sku' => 'product_in_Global_stock_with_3_sources'],
['sku' => 'product_with_default_stock_only'],
['sku' => 'product_with_disabled_manage_stock'],
['sku' => 'product_with_enabled_backorders'],
['sku' => 'product_in_US_stock_with_disabled_source'],
]
);

$actualStockStatusFormatted = [];
foreach ($actualStockStatus as $stockStatus) {
$actualStockStatusFormatted[$stockStatus['stockId']][$stockStatus['sku']] = $stockStatus;
}
foreach ($this->getExpectedStockStatus() as $stockId => $stockStatuses) {
foreach ($stockStatuses as $sku => $stockStatus) {
if (!isset($actualStockStatusFormatted[$stockId][$sku])) {
self::fail("Cannot find stock status for stock $stockId & sku $sku");
}
$actualStockStatus = $actualStockStatusFormatted[$stockId][$sku];
// ignore fields for now
unset($actualStockStatus['id'], $actualStockStatus['lowStock'], $actualStockStatus['updatedAt']);
self::assertEquals(
$stockStatus,
$actualStockStatus,
"Wrong stock status for stock $stockId & sku $sku"
);
}
}
}

/**
* @return \array[][]
*/
private function getExpectedStockStatus(): array
{
return [
// default stock
'1' => [
'product_with_default_stock_only' => [
'stockId' => '1',
'sku' => 'product_with_default_stock_only',
'qty' => 8.5,
'qtyForSale' => 8.5,
'infiniteStock' => false,
'isSalable' => true,
],
'product_with_disabled_manage_stock' => [
'stockId' => '1',
'sku' => 'product_with_disabled_manage_stock',
'qty' => 0,
'qtyForSale' => 0,
'infiniteStock' => true,
'isSalable' => true,
],
'product_with_enabled_backorders' => [
'stockId' => '1',
'sku' => 'product_with_enabled_backorders',
'qty' => 5,
'qtyForSale' => 5,
'infiniteStock' => true,
'isSalable' => true,
],
],
// EU Stock
'10' => [
'product_in_EU_stock_with_2_sources' => [
'stockId' => '10',
'sku' => 'product_in_EU_stock_with_2_sources',
'qty' => 9.5, // 5.5 (eu-1) + 4 (eu-2)
'qtyForSale' => 9.5,
'infiniteStock' => false,
'isSalable' => true,
],
'product_in_Global_stock_with_3_sources' => [
'stockId' => '10',
'sku' => 'product_in_Global_stock_with_3_sources',
'qty' => 3, // eu1 + eu2
'qtyForSale' => 3,
'infiniteStock' => false,
'isSalable' => true,
],
],
// US Stock
'20' => [
'product_in_Global_stock_with_3_sources' => [
'stockId' => '20',
'sku' => 'product_in_Global_stock_with_3_sources',
'qty' => 4, // us-1 source assigned to both stocks: US & Global
'qtyForSale' => 4,
'infiniteStock' => false,
'isSalable' => true,
],
'product_in_US_stock_with_disabled_source' => [
'stockId' => '20',
'sku' => 'product_in_US_stock_with_disabled_source',
'qty' => 0,
'qtyForSale' => 0,
'infiniteStock' => false,
'isSalable' => false,
],
],
// Global Stock
'30' => [
'product_in_Global_stock_with_3_sources' => [
'stockId' => '30',
'sku' => 'product_in_Global_stock_with_3_sources',
'qty' => 5, // 1 (eu-1) + 4 (us-1)
'qtyForSale' => 5,
'infiniteStock' => false,
'isSalable' => true,
],
'product_in_EU_stock_with_2_sources' => [
'stockId' => '30',
'sku' => 'product_in_EU_stock_with_2_sources',
'qty' => 5.5, // eu-1 only
'qtyForSale' => 5.5,
'infiniteStock' => false,
'isSalable' => true,
],
],
];
}
}
Loading

0 comments on commit 5531217

Please sign in to comment.