Skip to content

Commit

Permalink
Bundle Stock Calculation #210
Browse files Browse the repository at this point in the history
  • Loading branch information
Marvin-Magmodules committed Jan 27, 2025
1 parent 8f78d2e commit 93d7a69
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 44 deletions.
27 changes: 27 additions & 0 deletions Api/Config/System/FeedInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* Copyright © Magmodules.eu. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magmodules\Channable\Api\Config\System;

interface FeedInterface
{

public const XML_PATH_BUNDLE_STOCK_CALCULATION = 'magmodules_channable/types/bundle_stock_calculation';

/**
* Check if bundle stock calculation is enabled.
*
* This setting determines whether the parent bundle product stock
* should be calculated based on the lowest stock of associated simple products.
*
* @param int|null $storeId Store ID for which the configuration should be checked.
* If null, uses the default store configuration.
* @return bool True if bundle stock calculation is enabled, false otherwise.
*/
public function isBundleStockCalculationEnabled(?int $storeId = null): bool;
}
2 changes: 1 addition & 1 deletion Api/Config/System/ItemupdateInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/**
* Itemupdate group interface
*/
interface ItemupdateInterface
interface ItemupdateInterface extends FeedInterface
{

/** General Group */
Expand Down
2 changes: 1 addition & 1 deletion Helper/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ public function getAttributeSetName($product)
*/
private function getQtyValue($product)
{
if (in_array($product->getTypeId(), ['bundle','configurable','grouped'])) {
if (in_array($product->getTypeId(), ['configurable','grouped'])) {
return null;
}

Expand Down
25 changes: 25 additions & 0 deletions Model/Config/System/FeedRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
* Copyright © Magmodules.eu. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magmodules\Channable\Model\Config\System;

use Magmodules\Channable\Api\Config\System\FeedInterface;

/**
* Feed provider class
*/
class FeedRepository extends BaseRepository implements FeedInterface
{

/**
* @inheritDoc
*/
public function isBundleStockCalculationEnabled(?int $storeId = null): bool
{
return $this->isSetFlag(self::XML_PATH_BUNDLE_STOCK_CALCULATION, $storeId);
}
}
2 changes: 1 addition & 1 deletion Model/Config/System/ItemupdateRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* Item Update provider class
*/
class ItemupdateRepository extends BaseRepository implements ItemupdateInterface
class ItemupdateRepository extends FeedRepository implements ItemupdateInterface
{

/**
Expand Down
190 changes: 149 additions & 41 deletions Service/Product/InventoryData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
*
Expand All @@ -135,38 +167,114 @@ 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);
}
}
}

/**
* 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,
];
}
}
}
10 changes: 10 additions & 0 deletions etc/adminhtml/system/feed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@
<field id="bundle" separator=",">simple,both</field>
</depends>
</field>

<field id="bundle_stock_calculation" translate="label comment" type="select" sortOrder="36" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable Bundle Stock Calculation</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<config_path>magmodules_channable/types/bundle_stock_calculation</config_path>
<comment><![CDATA[<strong>Recommended:</strong> 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.]]></comment>
<depends>
<field id="bundle" separator=",">parent,both</field>
</depends>
</field>
<field id="heading_grouped" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Grouped Products</label>
<frontend_model>Magmodules\Channable\Block\Adminhtml\Design\Heading</frontend_model>
Expand Down

0 comments on commit 93d7a69

Please sign in to comment.