From 93d7a696ab10a07344e72f2ac4686e849dfb74dd Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Mon, 27 Jan 2025 14:37:26 +0100 Subject: [PATCH] Bundle Stock Calculation #210 --- Api/Config/System/FeedInterface.php | 27 +++ Api/Config/System/ItemupdateInterface.php | 2 +- Helper/Product.php | 2 +- Model/Config/System/FeedRepository.php | 25 +++ Model/Config/System/ItemupdateRepository.php | 2 +- Service/Product/InventoryData.php | 190 +++++++++++++++---- etc/adminhtml/system/feed.xml | 10 + 7 files changed, 214 insertions(+), 44 deletions(-) create mode 100644 Api/Config/System/FeedInterface.php create mode 100644 Model/Config/System/FeedRepository.php diff --git a/Api/Config/System/FeedInterface.php b/Api/Config/System/FeedInterface.php new file mode 100644 index 0000000..fd039aa --- /dev/null +++ b/Api/Config/System/FeedInterface.php @@ -0,0 +1,27 @@ +getTypeId(), ['bundle','configurable','grouped'])) { + if (in_array($product->getTypeId(), ['configurable','grouped'])) { return null; } diff --git a/Model/Config/System/FeedRepository.php b/Model/Config/System/FeedRepository.php new file mode 100644 index 0000000..3571720 --- /dev/null +++ b/Model/Config/System/FeedRepository.php @@ -0,0 +1,25 @@ +isSetFlag(self::XML_PATH_BUNDLE_STOCK_CALCULATION, $storeId); + } +} diff --git a/Model/Config/System/ItemupdateRepository.php b/Model/Config/System/ItemupdateRepository.php index af9cd71..2999855 100644 --- a/Model/Config/System/ItemupdateRepository.php +++ b/Model/Config/System/ItemupdateRepository.php @@ -12,7 +12,7 @@ /** * Item Update provider class */ -class ItemupdateRepository extends BaseRepository implements ItemupdateInterface +class ItemupdateRepository extends FeedRepository implements ItemupdateInterface { /** diff --git a/Service/Product/InventoryData.php b/Service/Product/InventoryData.php index ccfe7bd..856a6e7 100644 --- a/Service/Product/InventoryData.php +++ b/Service/Product/InventoryData.php @@ -7,40 +7,27 @@ namespace Magmodules\Channable\Service\Product; +use Magmodules\Channable\Api\Config\RepositoryInterface as ConfigProvider; use Magento\Framework\App\ResourceConnection; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; class InventoryData { - /** - * @var ResourceConnection - */ - private $resourceConnection; + private ResourceConnection $resourceConnection; + private ConfigProvider $configProvider; - /** - * @var array - */ - private $inventory; - /** - * @var array - */ - private $inventorySourceItems; - /** - * @var array - */ - private $reservation; + private array $inventory; + private array $inventorySourceItems; + private array $reservation; + private array $bundleParentSimpleRelation; - /** - * InventoryData constructor. - * - * @param ResourceConnection $resourceConnection - */ public function __construct( - ResourceConnection $resourceConnection + ResourceConnection $resourceConnection, + ConfigProvider $configProvider ) { $this->resourceConnection = $resourceConnection; + $this->configProvider = $configProvider; } /** @@ -123,6 +110,51 @@ private function getReservations(array $skus, int $stockId): void } } + /** + * Get all linked simple products from a list of bundle product SKUs. + * + * @param array $skus Array of product SKUs + */ + public function getLinkedSimpleProductsFromBundle(array $skus): void + { + $connection = $this->resourceConnection->getConnection(); + + // Retrieve product IDs for the given SKUs + $productTable = $this->resourceConnection->getTableName('catalog_product_entity'); + $bundleProductIds = $connection->fetchPairs( + $connection->select() + ->from($productTable, ['sku', 'entity_id']) + ->where('sku IN (?)', $skus) + ->where('type_id = ?', 'bundle') + ); + + if (empty($bundleProductIds)) { + return; + } + + // Retrieve linked simple products for the bundle products + $selectionTable = $this->resourceConnection->getTableName('catalog_product_bundle_selection'); + $linkedProducts = $connection->fetchAll( + $connection->select() + ->from(['s' => $selectionTable], ['parent_product_id', 'product_id', 'selection_qty']) + ->join( + ['p' => $productTable], + 's.product_id = p.entity_id', + ['sku', 'type_id'] + ) + ->where('s.parent_product_id IN (?)', $bundleProductIds) + ); + + foreach ($linkedProducts as $linkedProduct) { + $bundleSku = array_search($linkedProduct['parent_product_id'], $bundleProductIds, true); + $this->bundleParentSimpleRelation[$bundleSku][] = [ + 'sku' => $linkedProduct['sku'], + 'product_id' => $linkedProduct['product_id'], + 'quantity' => $linkedProduct['selection_qty'], + ]; + } + } + /** * Loads all stock information into memory * @@ -135,6 +167,11 @@ public function load(array $skus, array $config): void if (isset($config['inventory']['stock_id'])) { $this->getInventoryData($skus, (int)$config['inventory']['stock_id']); $this->getReservations($skus, (int)$config['inventory']['stock_id']); + + if ($this->configProvider->isBundleStockCalculationEnabled((int)$config['store_id'])) { + $this->getLinkedSimpleProductsFromBundle($skus); + } + if (!empty($config['inventory']['inventory_source_items'])) { $this->getInventorySourceItems($skus); } @@ -142,31 +179,102 @@ public function load(array $skus, array $config): void } /** - * Add stock data to product object - * - * @param Product $product - * @param array $config + * Add stock data to a product object. * - * @return Product + * @param Product $product The product object to which stock data will be added. + * @param array $config Configuration data, including inventory information. + * @return Product The product object with added stock data. */ public function addDataToProduct(Product $product, array $config): Product { - if (empty($config['inventory']['stock_id']) - || $product->getTypeId() != 'simple' - ) { + if (empty($config['inventory']['stock_id'])) { + return $product; + } + + if (!$this->isSupportedProductType($product)) { return $product; } - $inventoryData = $this->inventory[$config['inventory']['stock_id']][$product->getSku()] ?? []; - $reservations = $this->reservation[$config['inventory']['stock_id']][$product->getSku()] ?? 0; - $sourceItems = $this->inventorySourceItems[$product->getSku()] ?? []; + $stockData = $this->getStockData($product, $config); + + return $product + ->setQty($stockData['qty'] ?? 0) + ->setIsSalable($stockData['is_salable'] ?? 0) + ->setIsInStock($stockData['is_salable'] ?? 0) + ->setInventorySourceItems($stockData['source_item'] ?? null); + } + + /** + * Determine if the product type is supported for stock data processing. + * + * @param Product $product The product object. + * @return bool True if the product type is supported, false otherwise. + */ + private function isSupportedProductType(Product $product): bool + { + return in_array($product->getTypeId(), ['simple', 'bundle'], true); + } + + /** + * Retrieve stock data for a product, including bundle-specific logic. + * + * @param Product $product The product object. + * @param array $config Configuration data, including inventory information. + * @return array Stock data for the product. + */ + private function getStockData(Product $product, array $config): array + { + if ($product->getTypeId() == 'bundle' && isset($this->bundleParentSimpleRelation[$product->getSku()])) { + return $this->getBundleStockData($product, $config); + } + + return $this->getStockDataBySku($product->getSku(), $config); + } + + /** + * Retrieve stock data for a bundle product based on its associated simple products. + * + * @param Product $product The bundle product object. + * @param array $config Configuration data, including inventory information. + * @return array Stock data for the bundle product. + */ + private function getBundleStockData(Product $product, array $config): array + { + $simples = $this->bundleParentSimpleRelation[$product->getSku()] ?? []; + $minStockData = ['qty' => 0, 'is_salable' => 0, 'source_item' => null]; + + foreach ($simples as $simple) { + $simpleStockData = $this->getStockDataBySku($simple['sku'], $config); + $realStock = $simple['quantity'] ? $simpleStockData['qty'] / $simple['quantity'] : $simpleStockData['qty']; + + if ($realStock > $minStockData['qty']) { + $minStockData = $simpleStockData; + $minStockData['qty'] = $realStock; + } + } + + return $minStockData; + } + + /** + * Retrieve stock data for a product SKU. + * + * @param string $sku The SKU of the product. + * @param array $config Configuration data, including inventory information. + * @return array Stock data for the product. + */ + private function getStockDataBySku(string $sku, array $config): array + { + $stockId = $config['inventory']['stock_id'] ?? null; - $qty = isset($inventoryData['quantity']) ? $inventoryData['quantity'] - $reservations : 0; - $isSalable = $inventoryData['is_salable'] ?? 0; + $inventoryData = $this->inventory[$stockId][$sku] ?? []; + $reservations = $this->reservation[$stockId][$sku] ?? 0; + $sourceItems = $this->inventorySourceItems[$sku] ?? []; - return $product->setQty($qty) - ->setIsSalable($isSalable) - ->setIsInStock($isSalable) - ->setInventorySourceItems($sourceItems); + return [ + 'qty' => isset($inventoryData['quantity']) ? $inventoryData['quantity'] - $reservations : 0, + 'is_salable' => $inventoryData['is_salable'] ?? 0, + 'source_item' => $sourceItems, + ]; } -} +} \ No newline at end of file diff --git a/etc/adminhtml/system/feed.xml b/etc/adminhtml/system/feed.xml index 9094ca7..bb9547e 100644 --- a/etc/adminhtml/system/feed.xml +++ b/etc/adminhtml/system/feed.xml @@ -216,6 +216,16 @@ simple,both + + + + Magento\Config\Model\Config\Source\Yesno + magmodules_channable/types/bundle_stock_calculation + Recommended: Yes. This option calculates the stock of the parent bundle product based on the lowest available stock of associated simple products, adjusted by the selection quantity (selection_qty). Useful for ensuring the availability of bundles reflects the actual stock of their components.]]> + + parent,both + + Magmodules\Channable\Block\Adminhtml\Design\Heading