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