Sorry, we can’t ship there

Sorry, we can’t ship there

Magento offers a fair amount of shipping options out of the box, but we often find ourselves customizing this particular core functionality due to specific needs of our clients.

In this post we will show you how to disallow shipping to specific countries and we will do it on a product level. Let’s get started!

Adding product attribute

Since the configuration will be done per product, first thing we need to do is programmatically create new product attribute. In our custom module we will add following setup script:

<?php
 
/* @var $installer Mage_Eav_Model_Entity_Setup */
$installer = $this;
$installer->startSetup();
 
$installer->addAttribute(
    Mage_Catalog_Model_Product::ENTITY,
    'shipping_restriction',
    array(
        'type'      => 'varchar',
        'group'     => 'General',
        'input'     => 'multiselect',
        'label'     => 'Disallow Shipping to',
        'source'    => 'inchoo_shippingrestriction/attribute_source_country',
        'backend'   => 'inchoo_shippingrestriction/attribute_backend_country',
        'global'    => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE,
        'apply_to'  => 'simple,configurable,bundle,grouped',
        'required'  => false
    )
);
 
$installer->endSetup();

You will notice that our attribute has custom backend and source models and that we apply it only to product types which require shipping method.

First let’s take a look at our source model. For those that are not familiar with source models, their purpose is to fetch data for attribute. In our case it is a list of countries.

<?php
 
class Inchoo_ShippingRestriction_Model_Attribute_Source_Country
    extends Mage_Eav_Model_Entity_Attribute_Source_Abstract
    implements Mage_Eav_Model_Entity_Attribute_Source_Interface
{
    /**
     * Get list of all available countries
     *
     * @return array|mixed
     */
    public function getAllOptions()
    {
        $cacheKey = 'DIRECTORY_COUNTRY_SELECT_STORE_' . Mage::app()->getStore()->getCode();
        if (Mage::app()->useCache('config') && $cache = Mage::app()->loadCache($cacheKey)) {
            $options = unserialize($cache);
        } else {
            $collection = Mage::getModel('directory/country')->getResourceCollection();
            $options = $collection->toOptionArray();
            if (Mage::app()->useCache('config')) {
                Mage::app()->saveCache(serialize($options), $cacheKey, array('config'));
            }
        }
        return $options;
    }
}

Next we will look at our backend model. Purpose of backend model is to perform various actions on data that is stored in our attribute. For example we can validate data, modify before saving/after loading, etc. In our case we will use it to convert data from array (because we get array on input) to string before it is saved and other way around after our data is loaded.

<?php
 
class Inchoo_ShippingRestriction_Model_Attribute_Backend_Country
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract
    implements Mage_Eav_Model_Entity_Attribute_Backend_Interface
{
    public function beforeSave($object)
    {
        $attrCode = $this->getAttribute()->getAttributeCode();
        $object->setData($attrCode, implode(',', $object->getData($attrCode)));
 
        return $this;
    }
 
    public function afterLoad($object)
    {
        $attrCode = $this->getAttribute()->getAttributeCode();
        $object->setData($attrCode, explode(',', $object->getData($attrCode)));
 
        return $this;
    }
}

Once the attribute has been created, it will appear on the product edit page under General tab.
Here is what it looks like:

shipping_restriction

Validating shipping address

Now that we can configure our products to block shipping for specific countries, we have to implement logic which takes customer’s cart items, compares them against entered shipping address and decides if customer is eligible to select shipping method and complete the checkout process.

We cannot run our code immediately after the product was added to cart because there is no guarantee that we will have shipping address at this point. Instead, we have to wait for customer to start the checkout process.

With that in mind, we will implement our code by class rewrite method. We will rewrite Mage_Shipping_Model_Shipping class and modify its collectRates method. It seems like a good place since we have access to customer’s shipping address, all the cart items and we can control which shipping methods are returned (in our case it will be either all or none).

 

<?php
 
class Inchoo_ShippingRestriction_Model_Shipping extends Mage_Shipping_Model_Shipping
{
    public function collectRates(Mage_Shipping_Model_Rate_Request $request)
    {
        if ($this->isShippingRestriction($request)) {
            return $this;
        }
 
        return parent::collectRates($request);
    }
 
    /**
     * Check if shipping restrictions apply
     *
     * @param Mage_Shipping_Model_Rate_Request $request
     * @return bool
     */
    public function isShippingRestriction(Mage_Shipping_Model_Rate_Request $request)
    {
        $options = array();
 
        /** @var Mage_Sales_Model_Quote_Item $quoteItem */
        foreach ($request->getAllItems() as $quoteItem) {
            $options = array_merge(
                $options,
                explode(',', $quoteItem->getProduct()->getShippingRestriction())
            );
        }
 
        return in_array($request->getDestCountryId(), $options);
    }
}

You may notice that following line

$quoteItem->getProduct()->getShippingRestriction()

does not return any data at first. This is because Magento does not copy all the attribute data from product once it converts it to quote (cart) item. Luckily for us, Magento provides a mechanism to include additional attributes, all we need to do is write a few lines of XML.

The following code should be placed in your config.xml file inside <global> node:

<sales>
    <quote>
        <item>
            <product_attributes>
                <shipping_restriction/>
            </product_attributes>
        </item>
    </quote>
</sales>

Conclusion

Adding the config lines was the last piece of puzzle. Basic functionality is now implemented, but obviously there is always room for improvement, especially on frontend, in terms of notifications and error messages for customer. This is something we will leave as an exercise for reader since main focus of this article was on backend implementation.

Happy coding! 🙂

You made it all the way down here so you must have enjoyed this post! You may also like:

Making FedEx api show shipping estimate Toni Pap
Toni Pap, | 1

Making FedEx api show shipping estimate

Shell script for converting configurable to grouped products Tsvetan Stoychev
Tsvetan Stoychev, | 5

Shell script for converting configurable to grouped products

Custom shipping method in Magento Stjepan Udovicic
, | 44

Custom shipping method in Magento

12 comments

  1. Hi Thank you, for the write-up. I was able to learn from following your steps. I got the code working in the backend. Any tips or guidance for the front-end.

  2. What I need is to disallow shipping to specific cities in one state, not per country. This tutorial helped me create my own module for such feature. Now I can restrict products to be shipped in specific cities in my state/province. Thanks!

    1. i done everything in this but still checkout process continue without any error and can ship country in the list of ” disallow shipping to “. any other code need to stop the process if the country not in the list

  3. Hi I am just new with Mage Dev and I want to edit some checkout codes. I am not sure how to start with this? If I create the attribute with a script, how do I run it and how do you create the folder structure of the custom extension? Thank you very much!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.