Preconditions (*)
- Magento Open Source
2.4-develop (measured at 3a6b9667), also reproducible on 2.4.8-p5 and 2.4.9
- Any composite product type: configurable, bundle or grouped
Summary
A composite product created before its children are linked is born with
is_in_stock = 0 and stock_status_changed_auto = 0, and can then never be
brought back into stock automatically. This is the ordinary order an ERP or PIM
integration writes in: a structure feed creates the parent, a later feed attaches
the children, and a stock feed fills in quantities last.
ChangeParentStockStatus::isNeedToUpdateParent() — identical in
Magento\ConfigurableProduct, Magento\Bundle and Magento\GroupedProduct —
lets a parent go out of stock unconditionally, but only lets it come back when
stock_status_changed_auto is set:
return $parentStockItem->getIsInStock() !== $childrenIsInStock &&
($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto());
The implicitly created stock item records no merchant decision, but a 0 in that
column is indistinguishable from a merchant having taken the product off sale, so
the parent is latched out of stock permanently. Reindexing does not help: the flag
is stored, not derived.
Steps to reproduce (*)
POST /rest/V1/products — create a configurable, bundle or grouped parent with
no stock_item extension attribute and no children linked yet.
- Read the parent's row:
SELECT is_in_stock, stock_status_changed_auto
FROM cataloginventory_stock_item si
JOIN catalog_product_entity e ON e.entity_id = si.product_id
WHERE e.sku = '<parent sku>';
It is (0, 0).
- Create the children and attach them to the parent in a second call.
- Send stock for the children in a third call.
- Re-read the parent's row and its storefront salability.
Expected result (*)
- The parent follows its children back into stock once they are salable.
Actual result (*)
- The parent stays
(is_in_stock = 0, stock_status_changed_auto = 0) and is not
salable, permanently. Measured for all three composite types.
Important boundary
The parent must be created before its children are linked. If one call creates
the parent already carrying its links or options, ChangeParentStockStatus runs
inside that same save, the parent is born (0, 1), and it recovers normally. That
is why this looks intermittent between integrations: one that sends structure and
links together never sees it.
Additional information
The asymmetry is visible in StockItemRepository::save(). The qty branch maps the
"changed automatically" marker onto the column; the non-qty (composite) branch
never touches the column at all:
if ($isQty) {
…
if ($stockItem->hasStockStatusChangedAutomaticallyFlag()) {
$stockItem->setStockStatusChangedAuto((int)$stockItem->getStockStatusChangedAutomaticallyFlag());
}
} else {
$stockItem->setQty(0);
}
StockConfigurationInterface::isQty() is false for exactly configurable,
bundle and grouped (only simple, virtual and downloadable declare
isQty="true"), so that branch already isolates the composite types.
Upstream already applies the same rule elsewhere:
InventoryConfigurableProduct\Model\StockStatusManagement::update() derives the
flag for a newly created stock item — but only for configurables, only in single
source mode, and it computes 0 for the born-out-of-stock case.
Related, all describing the same latch from different entry points:
#36154, #37960, #32192.
Preconditions (*)
2.4-develop(measured at3a6b9667), also reproducible on 2.4.8-p5 and 2.4.9Summary
A composite product created before its children are linked is born with
is_in_stock = 0andstock_status_changed_auto = 0, and can then never bebrought back into stock automatically. This is the ordinary order an ERP or PIM
integration writes in: a structure feed creates the parent, a later feed attaches
the children, and a stock feed fills in quantities last.
ChangeParentStockStatus::isNeedToUpdateParent()— identical inMagento\ConfigurableProduct,Magento\BundleandMagento\GroupedProduct—lets a parent go out of stock unconditionally, but only lets it come back when
stock_status_changed_autois set:The implicitly created stock item records no merchant decision, but a
0in thatcolumn is indistinguishable from a merchant having taken the product off sale, so
the parent is latched out of stock permanently. Reindexing does not help: the flag
is stored, not derived.
Steps to reproduce (*)
POST /rest/V1/products— create a configurable, bundle or grouped parent withno
stock_itemextension attribute and no children linked yet.(0, 0).Expected result (*)
Actual result (*)
(is_in_stock = 0, stock_status_changed_auto = 0)and is notsalable, permanently. Measured for all three composite types.
Important boundary
The parent must be created before its children are linked. If one call creates
the parent already carrying its links or options,
ChangeParentStockStatusrunsinside that same save, the parent is born
(0, 1), and it recovers normally. Thatis why this looks intermittent between integrations: one that sends structure and
links together never sees it.
Additional information
The asymmetry is visible in
StockItemRepository::save(). The qty branch maps the"changed automatically" marker onto the column; the non-qty (composite) branch
never touches the column at all:
StockConfigurationInterface::isQty()is false for exactlyconfigurable,bundleandgrouped(onlysimple,virtualanddownloadabledeclareisQty="true"), so that branch already isolates the composite types.Upstream already applies the same rule elsewhere:
InventoryConfigurableProduct\Model\StockStatusManagement::update()derives theflag for a newly created stock item — but only for configurables, only in single
source mode, and it computes
0for the born-out-of-stock case.Related, all describing the same latch from different entry points:
#36154, #37960, #32192.