Programatically Modifying Products’ Stock Item Objects

Recently we had a project to make all products within a number of different categories available as backorder products. The client did not want to go through and change all of the products individually, so we embarked on attempting to do this programatically.

Our first idea was to do this using observers, so hypothetically whenever a product or product collection loaded we would be able to modify the accompanying stock item object if the product was in one of the categories in question.

The observers we identified to perform this were:

catalog_product_is_salable_after
We would use the above event to detect whether the product was in a particular category and set isSalable on the product object to true if so. This would make the product appear purchasable from the listing and detail pages instead of displaying the Out of Stock message.

catalog_product_collection_load_after
catalog_product_load_after
sales_quote_item_collection_products_after_load
We used the above three events to modify the following values:

  $stockItem->setIsInStock(true)
    ->setData('backorders', Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY)
    ->setUseConfigBackorders(false)
    ->setData('is_in_stock', true);

This method worked to a certain extent, however the process fell down when on the One Page Checkout; the Mage_CatalogInventory_Model_Stock object’s registerProductsSale creates a stock item object and populates it manually without firing any observers to hook into. This means that the final check in this method will always fail, and the user will be denied purchasing the product in question.

An attempted workaround was to override the Mage_CatalogInventory_Model_Stock class’ registerProductsSale method and perform our category check there. This, however caused another issue in that after registering the sale, the stock object gets saved in order to decrement the stock available; this also saves the values we’ve set meaning that if the client ever wanted to disable this functionality they would be left with any products which had been ordered in a backorder state.

Attempt 2 – Overriding

Against our better judgement, we opted instead to override the *Mage_CatalogInventory_Model_Stock_Item** class rather than changing its values with observers; this got around the issue of the stock item being saved and the values within it being crystallised. Here’s the class in question:

<?php

class Namespace_Module_Model_CatalogInventory_Stock_Item extends Mage_CatalogInventory_Model_Stock_Item
{
    public function getIsInStock()
    {
        if($this->isPurchaseableViaCategory()){
            return true;
        }

        return parent::getIsInStock(); 
    }

    public function getBackorders()
    {
        if($this->isPurchaseableViaCategory()){
            return Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY;
        }

        return parent::getBackorders();
    }

    public function isPurchaseableViaCategory()
    {
        $product = $this->getProduct();
        if(!$product && $this->getProductId()){
            $product = Mage::getModel('catalog/product')->load($this->getProductId());
        }

        if(!$product){
            return false;
        }

        return Mage::helper('module')->isPurchaseableViaCategory($product);
    }
}

And in our helper, we do the work of checking whether the product is in one of our selected categories (or a subcategory):

class Namespace_Module_Helper_Data extends Mage_Core_Helper_Abstract{
    protected $_allowBackOrderCategories = array("gold");

    public function isPurchaseableViaCategory(Mage_Catalog_Model_Product $product){
        $keys = array();

        $cats = $product->getCategoryCollection()
            ->addAttributeToSelect('url_key');

        foreach($cats as $cat){
            $this->getParentUrlKeys($cat, $keys);
        }

        foreach($this->_allowBackOrderCategories as $cat){
            if(in_array($cat, $keys)){
                return true;
            }
        }
    }

    public function getParentUrlKeys(Mage_Catalog_Model_Category $cat, &$urlKeys) {
        if(!in_array($cat->getUrlKey(), $urlKeys)) {
            $urlKeys[] = $cat->getUrlKey();
        }

        if($cat->getParentCategory()->getId()){
            return $this->getParentUrlKeys($cat->getParentCategory(), $urlKeys);
        }
    }
}

We were still able to complete our saleable check (for the display on the product listing and detail page) using an observer:

<catalog_product_is_salable_after>
    <observers>
        <modify_saleable>
            <type>singleton</type>
            <class>module/observer</class>
            <method>checkIsSaleable</method>
        </modify_saleable>
    </observers>
</catalog_product_is_salable_after>
public function checkIsSaleable($observer) {
    $product = $observer->getProduct();

    if($product->getIgnoreSaleableCategory()){
        return;
    }

    if(Mage::helper('module')->isPurchaseableViaCategory($product)){
        $observer->getSalable()->setIsSalable(true);
    }
}

For completeness, we added a property to the product object to be able to identify whether this product exists in a category and should have been listed as out of stock. We can then identify these products and display a different message (Such as delivery times) on the listing and detail pages. Here’s where it’s used in our Helper:

public function isOutOfStockButPurchasable(Mage_Catalog_Model_Product $product){
    if(!$product){
        return false;
    }

    $salableCheckOne = $product->isSalable();
    $product->setIgnoreSaleableCategory(true);

    $salableCheckTwo = $product->isSalable();
    $product->setIgnoreSaleableCategory(false);

    return $salableCheckOne !== $salableCheckTwo;
}

Our modified products will then be identifiable because the value from the two checks will not match.

Caveats

The only problem from this method is that if the client saves one of the out of stock products within one of these categories, then the values for backorder and is_in_stock will be saved to the database, meaning that if the code is disabled those saved products will still be available via backorder.

Fixing Issues With Magento’s Massaction PDF Tax Values

There appears to be an issue in Magento’s Massaction Invoice PDF. The issue presents itself when outputting the total tax for the invoice, which will be completely different to the amount displayed when the PDF is created from the view invoice page.

The issue exists in the Mage_Tax_Helper_Data class’ getCalculatedTaxes method. The first few lines decide what the variable $current will be;

if ($this->_getFromRegistry('current_invoice')) {
    $current = $this->_getFromRegistry('current_invoice');
} elseif ($this->_getFromRegistry('current_creditmemo')) {
    $current = $this->_getFromRegistry('current_creditmemo');
} else {
    $current = $source;
}

In the single version of PDF creation, the current_invoice registry key is set and is used going forward. This is omitted from the massaction update, so the method falls back to the $source parameter, which happens to be an instance of an order object.

The fix is to override Mage_Sales_Model_Order_Pdf_Invoice and create a new getPdf method, with just a two line addition inside of the foreach:

public function getPdf($invoices = array())
{
    $this->_beforeGetPdf();
    $this->_initRenderer('invoice');

    $pdf = new Zend_Pdf();
    $this->_setPdf($pdf);
    $style = new Zend_Pdf_Style();
    $this->_setFontBold($style, 10);

    foreach ($invoices as $invoice) {
        // Set the invoice here, as not doing this (E.g. default Magento) causes the wrong value to be calculated
        // when using the massaction pdf action. Unregister first, otherwise only the first invoice will be set
        Mage::unregister('current_invoice');
        Mage::register('current_invoice', $invoice);
        
        if ($invoice->getStoreId()) {
            Mage::app()->getLocale()->emulate($invoice->getStoreId());
            Mage::app()->setCurrentStore($invoice->getStoreId());
        }
        $page  = $this->newPage();
        $order = $invoice->getOrder();
        /* Add image */
        $this->insertLogo($page, $invoice->getStore());
        /* Add address */
        $this->insertAddress($page, $invoice->getStore());
        /* Add head */
        $this->insertOrder(
            $page,
            $order,
            Mage::getStoreConfigFlag(self::XML_PATH_SALES_PDF_INVOICE_PUT_ORDER_ID, $order->getStoreId())
        );
        /* Add document text and number */
        $this->insertDocumentNumber(
            $page,
            Mage::helper('sales')->__('Invoice # ') . $invoice->getIncrementId()
        );
        /* Add table */
        $this->_drawHeader($page);
        /* Add body */
        foreach ($invoice->getAllItems() as $item){
            if ($item->getOrderItem()->getParentItem()) {
                continue;
            }
            /* Draw item */
            $this->_drawItem($item, $page, $order);
            $page = end($pdf->pages);
        }
        /* Add totals */
        $this->insertTotals($page, $invoice);
        if ($invoice->getStoreId()) {
            Mage::app()->getLocale()->revert();
        }
    }
    $this->_afterGetPdf();
    return $pdf;
}

And massaction PDFs will once again have the correct tax amount.

Curling a URL in PHP

Assume the form object contains an array in the form of params

 $fieldString = http_build_query($form->getParams());

//open connection
$ch = curl_init();

//set the url, number of POST vars, POST data
curl_setopt($ch,CURLOPT_URL, $this->_dotMailerUri);
curl_setopt($ch,CURLOPT_POST, count($form->getParams()));
curl_setopt($ch,CURLOPT_POSTFIELDS, $fieldString);
// Set this so the result isn't added to stdout and gets returned instead
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

//execute post
$result = curl_exec($ch);

//close connection
curl_close($ch);

Programatically add a Layout Update File in Magento

We sometimes need logic to decide whether an update file should be loaded or not, and need to do this outside of a module’s config.xml area/layout/update node. Instead, we can use an observer and programatically decide whether to add the layout file or not.

config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Namespace_Module>
            <version>1.0.0</version>
        </Namespace_Module>
    </modules>
    <global>
        <models>
            <mymodule>
                <class>Loewenstark_Layout_Model</class>
            </mymodule>
        </models>
        <events>
            <core_layout_update_updates_get_after>
                <observers>
                    <mymodule_add_layout>
                        <type>singleton</type>
                        <class>mymodule/observer</class>
                        <method>addLayoutXml</method>
                    </mymodule_add_layout>
                </observers>
            </core_layout_update_updates_get_after>     
        </events>
    </global>
</config>

The Observer

Because the updates object extends a SimpleXML Object, we can add our file to it here

<?php
class Namespace_Module_Model_Observer
{
    public function loadXML(Varien_Event_Observer $observer)
    {
        // Conditional to decide whether to load the XML file
        if(Mage::helper('mymodule')->isEnabled()) {
            $xml = $observer->getUpdates()
                ->addChild('mymodule');
            /* @var $xml SimpleXMLElement */
            $xml->addAttribute('module', 'Namespace_Module');
            $xml->addChild('file', 'mymodule/layoutfile.xml');
        }
    }
}

Retrieve a list of all parent category url keys in Magento

In this example, we only want to show a block for a product which resides within a particular category. We check all of the category’s parent categories and show the block if any of them match our $_allowedCategories array. Using URL keys instead of category IDs makes this code more portable between environments such as development and production where IDs may not be the same.

<?php
class Namespace_Module_Block_Catalog_Product_Request_Form extends Mage_Core_Block_Text_List
{
    protected $_allowedCateogies = array('shelters');

    protected function _toHtml()
    {
        $allCatUrls = array();

        // get an array of all parent category url keys
        if($product = Mage::registry('current_product')){
            $cats = $product->getCategoryCollection()->addAttributeToSelect('url_key');

            foreach($cats as $cat){
                $this->_getParentUrlKeys($cat, $allCatUrls);
            }
        }

        // Check whether the category is in the array
        foreach($this->_allowedCateogies as $category){
            if(in_array($category, $allCatUrls)){
                return parent::_toHtml();
            }
        }

        return '';
    }

    // Recursively get all of the parents url keys
    protected function _getParentUrlKeys(Mage_Catalog_Model_Category $cat, &$urlKeys)
    {
        if(!in_array($cat->getUrlKey(), $urlKeys)) {
            $urlKeys[] = $cat->getUrlKey();
        }

        if($cat->getParentCategory()->getId()){
            return $this->_getParentUrlKeys($cat->getParentCategory(), $urlKeys);
        }
    }
}

Fixing Magento’s Configurable Attribute Ordering

Somewhere along the way in a 1.9.x update, configurable attribute sorting stopped working on the fronted. Magento should use an attribute’s position parameter (in the options tab when editing a product attribute), to order the dropdown on a product detail page.

The issue appears to be where Magento assigns prices to an attribute collection in Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection

Magento first gets the options for the attribute here:

 $options = $productAttribute->getFrontend()->getSelectOptions();

At this point, all is good. In Magento’s SQL to get the options, sort_order is used correctly and the array of options is in the correct order.

The problem seems to occur in the loop on line 257:

 foreach ($this->getProduct()->getTypeInstance(true)
->getUsedProducts(array($productAttribute->getAttributeCode()), $this->getProduct())
   as $associatedProduct) {
...
}

The order of the attributes will now be determined by the order of items in this loop, which is the order of associated products.

What we need to do is re-order the $values array which is populated in the above loop, we can do this using the order of the $options array:

$order = 0;
$optionsByValue = array();
foreach ($options as $option) {
    $optionsByValue[$option['value']] = $option['label'];
    $orders[$option['value']] = $order;
    $order++;
}

Here we utilise the existing loop which populates the $optionsByValue array to log the ordering of each option. We can then use those values by passing them into our custom sort method just before Magento adds the prices to each item:

/* Resort the values array using the order we've logged against options */
usort($values, array($this, 'sortValues'));

foreach ($values as $data) {
    $this->getItemById($data['product_super_attribute_id'])->addPrice($data);
}

Here’s the sort method which puts the array back to the correct sort order:

public function sortValues($value1, $value2){
    if(isset($value1['sort_order']) && isset($value2['sort_order'])){
        if($value1['sort_order'] > $value2['sort_order']){
            return 1;
        }

        if($value1['sort_order'] < $value2['sort_order']) {
            return -1;
        }

        return 0;
    }
}

Paypal Express – Skip Order Review Step in Magento 1

Magento has the option to skip the review step upon returning from Paypal (System > Configuration > Payment Methods > Paypal Express Checkout > Advanced Settings > Skip Order Review Step). There are a couple of caveats to the way this functionality works:

1. Using the Paypal button from the cart page

If the button from the checkout/cart page is used rather than following the flow through the one step checkout then the review step becomes compulsory.

We can see this by the URL which the buttons links to the paypal express controller, which has the parameter button/1 on it. In Mage_Paypal_Controller_Express_Abstract, the following is passed into the start method of the checkout (Mage_Paypal_Model_Express_Checkout) object:

<?php
$button = (bool)$this->getRequest()->getParam(Mage_Paypal_Model_Express_Checkout::PAYMENT_INFO_BUTTON);
$token = $this->_checkout->start(Mage::getUrl('*/*/return'), Mage::getUrl('*/*/cancel'), $button);

This is then set and saved on the payment object:

<?php
$this->_quote->getPayment()->setAdditionalInformation(self::PAYMENT_INFO_BUTTON, 1);

This option is used when the user returns from Paypal in Mage_Paypal_Controller_Express_Abstract‘s returnAction method:

<?php
if ($this->_checkout->canSkipOrderReviewStep()) {
    $this->_forward('placeOrder');
} else {
    $this->_redirect('*/*/review');
}

Where the canSkipOrderReviewStep() method uses the PAYMENT_INFO_BUTTON option which was set earlier to decide wither the user should be sent to the review page or have their order placed straight away.

2. Using Paypal’s Incontext Module

If this module from Magento Connect has been installed, then the function referenced above which sets the button parameter when starting the checkout process is commented out:

<?php
//$button = (bool)$this->getRequest()->getParam(Mage_Paypal_Model_Express_Checkout::PAYMENT_INFO_BUTTON);
...
$token = $this->_checkout->start(Mage::getUrl($type . '/return'), Mage::getUrl($type . '/cancel'), true);

This means that Magento will always assume the user came from clicking one of the cart buttons in this scenario, and never from the one page checkout. This line is found in Paypal_InContext_Controller_Express_Action‘s startAction method, which supersedes the one found in Core Magento’s Mage_Paypal_Controller_Express_Abstract.

The reason the InContext module requires the review step is for the user experience. When using the InContext module, the user is not redirected to Paypal on a device where it’s enabled, so would never get chance to review their order from the point they chose to pay either from the cart or checkout page.

Retreiving Configurable Products Whose Children Have a Particular Attribute

A title as catchy as a loose tablecloth. This will give you a list of all configurable products whose children have a particular attribute.

SELECT config_product.entity_id, prod_name.value AS product_name, config_product.sku
     FROM  catalog_product_entity config_product
JOIN eav_attribute
    ON eav_attribute.entity_type_id = 4 
    AND eav_attribute.attribute_code = &quot;name&quot;
JOIN catalog_product_entity_varchar prod_name
    ON prod_name.`attribute_id` = eav_attribute.`attribute_id`
    AND prod_name.`entity_id` = config_product.`entity_id`
WHERE config_product.entity_id IN (
    SELECT parent_id 
        FROM catalog_product_entity_int att_value 
    JOIN `catalog_product_super_link` link
        ON link.product_id = att_value.entity_id
    AND att_value.`value` = attribute_value
);
  • As Magento only allows the creation of product links on dropdowns, catalog_product_entity_int should be fine to remain hardcoded.
  • Change attribute_value to the option value id. An easy way to retrieve this from Magento’s admin is to look at the name of the input box when looking at the attribute’s Manage Label/Options tab; it will be something like “option[value][959][0]” where 959 is the option ID.

The SQL firstly uses a subquery to get all products which are being used as simples (if their product_id appears in the super_link table), and gets their configurable counterparts from this. We need to do this as a subquery their may be multiple configurables using the same simple product. It then uses the parent IDs in looking up more details from the catalog_product_entity and catalog_product_entity_varchar tables.

Magento’s Configurable Swatches

Create the Attribute

Go to Catalog > Attributes and create the attribute to be used to create the configurable product. This would be typically something like “Colour”. Make sure “Use to create configurable product” is set to “Yes”.

In “Manage Label / Options” add the options for the swatch, E.g. “Red”, “Yellow”, “Blue”. Magento will use these values when looking for the image files for the swatches.

Setting Up The Swatches Config

Go to System > Configuration > Configurable Swatches and set “Enabled” to “Yes”, and select the newly created attribute to be used as a swatch. Set any of the swatch dimensions required if required.

Create the Products

Create the configurable product, and add associated products to it using the attribute we’ve created above. You should now find that when you view your product you get text links output as the colour swatches. This is because Magento currently can’t find any images to use for the colour swatch itself.

Add the Images

There are two ways to get Magento to show images as swatches. The first is to add global swatches which are simply images which are placed in Magento’s **Media/wysiwyg/swatches/” directory using the option label as the filename. So, for example, you may have “blue.png”, “red.png” etc. The extension for image files is PNG.

The second, and this will supersede the first, will allow the setting of a configurable swatch on the product level. To do this, upload images to the configurable product, and then set image’s label field to the attribute’s option value with the suffix “-swatch”, E.g. “blue-swatch”. This image will now be resized and used as the image for that option.

Using Magento’s Configurable Attribute Renderers

Magento decides whether to use a renderer instead of simply outputting configurable options as a select element in catalog/product/view/type/configurable.phtml:

  • All renderers are loaded from the text/list block product.info.options.configurable.renderers
  • Each product attribute is looped over, and each of the renderers are looped over within and checked whether it should be rendered for that attribute.
  • The shouldRender of the renderer object is passed in the attribute and jsonConfig. In the case of swatches, Magento returns true if the attribute in question has been defined as a swatch type in System > Configuration > Configurable Swatches > General Settings.

Using this method, we can add other renderers to the product.info.options.configurable.renderers block, implement the shouldRender method, and decide whether the use that renderer based on the attribute we’re passed in.