It turned out that this is a bug in Mage_Sales_Model_Quote_Item::compare()
that was introduced in Magento CE 1.9.2 / EE 1.14.2. The method is used to compare items to decide if they are the same product and can be merged (during login and when adding products to the cart).
When comparing all custom options, it should skip the options that are not represantative (_notRepresentOptions
), namely the info_buyRequest option.
In previous Magento versions, it looked like this:
foreach ($this->getOptions() as $option) {
if (in_array($option->getCode(), $this->_notRepresentOptions)) {
continue;
}
and worked correctly. Now it looks like this:
foreach ($this->getOptions() as $option) {
if (in_array($option->getCode(), $this->_notRepresentOptions)
&& !$item->getProduct()->hasCustomOptions()
) {
continue;
}
and the additional check for hasCustomOptions()
causes the described bug. Why? It looks like the check has been added to always keep products with custom options separate. I don't think it makes sense, at least not in the way it is implemented, but there will be some reason for it that I am not aware of.
However, $item->getProduct()->hasCustomOptions()
always returns true for quote items!
This is the method:
public function hasCustomOptions()
{
if (count($this->_customOptions)) {
return true;
} else {
return false;
}
}
But $this->_customOptions
also contains the info_buyRequest
option from the quote item.
For an unobstrusive solution, I tried to remove the info_buyRequest
option from all products in an observer on sales_quote_merge_before
, with no success.
The reason lies in Mage_Sales_Model_Quote_Item_Abstract::getProduct()
where the option is copied again from the quote item itself:
public function getProduct()
{
$product = $this->_getData('product');
[...]
if (is_array($this->_optionsByCode)) {
$product->setCustomOptions($this->_optionsByCode);
}
return $product;
}
Solution
I created a rewrite for Mage_Sales_Model_Quote_Item
with an override for getProduct()
to not include the info_buyRequest
option at this point:
public function getProduct()
{
$product = parent::getProduct();
$options = $product->getCustomOptions();
if (isset($options['info_buyRequest'])) {
unset($options['info_buyRequest']);
$product->setCustomOptions($options);
}
return $product;
}
This caused trouble with bundle products, the alternative below or the official patch as described by @AnnaVölkl is a better solution
Alternative
You could also remove the offending && !$item->getProduct()->hasCustomOptions()
in the compare()
method if you are rewriting the item model anyways. I don't know what problem it tried to solve, but it created more...
Update Jan 29 2016
I reported this to Magento and got the response that they could not reproduce the issue, so the patch will not make it into the community edition (Submission APPSEC-1321).
This means, if you have the problem, you need to apply the enterprise patch SUPEE-6190 after each update or use a class rewrite instead.