6

I've been looking at programmatically creating bundle products, and got stuck on adding a selection to the options.

The problem is that when you create a new selection (Mage_Bundle_Model_Selection) object (whatever method is used to generate that object), it does not initially have a selection_id. It can't, because the id is generated through the auto_increment column.

The problem comes when you go to save your new object. The model class has a _beforeSave function, which calls getResource()->saveSelectionPrice, which inserts a row into bundle/selection_price table... where one of the values is the selection id!

So, you get a catch 22 - you can't save the object, because it needs a selection_id to insert the price, which must happen before it saves. But you can't get the selection_id, because that is not generated until the save completes.

So how are new selection objects created?

  • Useful Clarification: What steps in the UI are you taking to create a bundle/selection_price. A simply configured bundle doesn't appear to add any information to this table. – Alan Storm Jul 4 '14 at 17:18
  • @AlanStorm. It does add info to that table if you set the price scope to Website and you edit the product on a store view level. (not default values.) – Marius Jul 4 '14 at 21:20
8
+50

Short Version: If you save your product in a Magento environment where the store ID is set to 0 you shouldn't run into this problem.

Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);

Magento blocks saving products when the store ID isn't 0 already, so you'll need to do this anyway. If that doesn't work read the rest of this and come back with the context we'll need to answer the question.

Long Version: At the risk of blowing my cover, I'm not super familiar with bundled products or what a selection price actually is. It would help if someone could describe the admin process they're using to create bundled products and have the selection price saved. The reason I ask is, from a cursory examination of things, it doesn't look like the code path from the question is ever called by Magento. I'm also looking at the latest Magento CE 1.9 — so things might be different in a different version.

Taking a look at the _beforeSave method

protected function _beforeSave()
{
    $storeId = Mage::registry('product')->getStoreId();
    if (!Mage::helper('catalog')->isPriceGlobal() && $storeId) {
        $this->setWebsiteId(Mage::app()->getStore($storeId)->getWebsiteId());
        $this->getResource()->saveSelectionPrice($this);

        if (!$this->getDefaultPriceScope()) {
            $this->unsSelectionPriceValue();
            $this->unsSelectionPriceType();
        }
    }
    parent::_beforeSave();
}

You can see the call to saveSelectionPrice mentioned in the question. However, it's wrapped in a conditional.

if (!Mage::helper('catalog')->isPriceGlobal() && $storeId) {
    //...
    $this->getResource()->saveSelectionPrice($this);
    //...
}

The first part of the conditional checks the global price scope, set at System -> Configuration -> Catalog -> Price. The second, is the $storeId. This is set in the line above

$storeId = Mage::registry('product')->getStoreId();

Here's the thing — a product doesn't have a store id. It has website ids that let you know which websites a product is a member of. A product does not, however, have one true store id. Sometimes a store ID is set on a product, which impacts the behavior of other methods (ex. "save this extra specific information"), but products themselves don't have a store id.

If you pop to the getStoreId method in the product class

#File: app/code/core/Mage/Catalog/Model/Product.php
public function getStoreId()
{
    if ($this->hasData('store_id')) {
        return $this->getData('store_id');
    }
    return Mage::app()->getStore()->getId();
}

You can see all it does is return the currently set ID of the store object.

When Magento creates a product programmatically in the admin, the store_id is always 0. The only other time Magento creates a product would be in an API context -- and the API doesn't support bundled products. My guess is if there are places where Magento executes this code path, it's in places where a selection already exists, and the problem of no selection_id goes away.

So, if you make sure to set the store ID to Mage_Core_Model_App::ADMIN_STORE_ID (in other words, 0) when you save the product you should be set.

There's also probably a rant in here about the less-than-complete implementation of bundled products, and the loosely defined idea of a "store" and "store id" in Magento, but I've already gone on long enough.

Update: Thanks to Marius's reminder about the store switcher I've been able to dig into this further and (unfortunately) reproduce the bug in the original question using only UI actions. If you

  1. Create a new Bundled Product with a fixed (vs. dynamic) price

  2. Click "Save and Continue" to save the product

  3. Select a store view from the switcher (which you can only do once the product is saved)

  4. Click on the Bundled Items Tab

  5. Attempt to add a new product to the bundle

You'll end up with the following error.

Integrity constraint violation: 1048 Column 'selection_id' cannot be null

enter image description here

This is exactly the situation described in the question — there's no selection object yet, so any attempt to insert the price fails.

The key here appears to be the design decision that it's only the price that's broken out by store id. That is, if you modify the above steps to add your products to the bundle while the admin is editing the default store view (store id 0), you'll be able to hop down to a specific store view and set your prices after you save the selections at store_id 0.

As implemented, it does not appear possible for a bundled product to contain different products dependent on store view.

So, I'd take a similar approach when you're creating your product programmatically. Create the products and initial selection objects when the system's in "Store ID 0 Mode", and then add any store specific specific price changes in a separate operation/transaction.

  • "Holy Smokes Batman". You really dug deep on this one. You have my +1 on this, but starting with the line "Here's the thing — a product doesn't have a store id" I think it's wrong. the product has a store id in the backend when editing it on the store view level. For example in Mage_Adminhtml_Catalog_ProductController::_initProduct you have at the beginning: $product = Mage::getModel('catalog/product')->setStoreId($this->getRequest()->getParam('store', 0)); – Marius Jul 4 '14 at 21:30
  • @Marius Thanks for the switcher reminder -- I added an update digging in a bit further, explaining how the admin code handles (or doesn't handle) this. – Alan Storm Jul 5 '14 at 1:44
  • And now to figure out why this is done in _beforeSave to begin with...When dealing with foreign objects, beforeSave should only deal with validation, nothing else. @sohel-rana already moved it to _afterSave, so now I wonder what breaks and if nothing, it's worthy of a bug report. – Melvyn Jul 5 '14 at 10:43
  • [-50 points] for "Magneto" usage. – benmarks Jul 5 '14 at 12:24
  • [+50 points] for steps to reproduce the bug, want me to log it or you? magentocommerce.com/bug-tracking – benmarks Jul 5 '14 at 12:26
2
+100

I think it's known issue of magento team. I already post a similar issue

When you save before there is no selection_id, so you should save after table "catalog_product_bundle_selection". So your function name change from "_beforeSave" to "_afterSave". This code run without error. You can try this one.

[EDIT by Marius. Code from the link above].

Solution is simple :) . Overwrite Mage_Bundle_Model_Selection model class and codes are look like

class Package_MyModule_Model_Selection extends Mage_Bundle_Model_Selection
{
    /**
     * Processing object before save data
     *
     * @return Mage_Bundle_Model_Selection
     */
    protected function _beforeSave()
    {
        // No code please
    }

    /**
     * Processing object after save data
     *
     * @return Mage_Bundle_Model_Selection
     */
    protected function _afterSave()
    {
        $storeId = Mage::registry('product')->getStoreId();
        if (!Mage::helper('catalog')->isPriceGlobal() && $storeId) {
            $this->setWebsiteId(Mage::app()->getStore($storeId)->getWebsiteId());

            $this->getResource()->saveSelectionPrice($this);

            if (!$this->getDefaultPriceScope()) {
                $this->unsSelectionPriceValue();
                $this->unsSelectionPriceType();
            }
        }
        parent::_afterSave();
    }
}
  • Thanks for this, but I think you don't understand the question. or maybe I didn't understand it and offered a bounty for nothing. This has nothing to do with any custom code. It is about how the core code works. – Marius Jul 4 '14 at 12:37
  • Sorry man...I first misunderstood what you meant. Yep you are right, with the error and with the fix. But since single link answers are not the best, please post what's at the end of that URL in the answer so it will remain for posterity and I will gladly award you the "50 fake internets" from the bounty. – Marius Jul 4 '14 at 21:22
  • I started an other bounty as promised. You will get the points in 23 hours when I'm allowed to give it. – Marius Jul 5 '14 at 18:06
  • This solution will introduce another bug: If you change the bundle selection price in another website, it will get saved into default scope additionally. This is because the second if statement is supposed to unset it but since it is afterSave event it won't do anything. – Domeglic Jan 11 '17 at 15:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.