Create Custom Shipping Module in Magento 2

November 25, 2015  |  10 Comments  |  by Raj (MagePsycho)  |  Magento 2

As you know Magento 2 GA is already released on mid of November, 2015. Now the development of Magento 2 components is taking by storm and the developers are especially busy with porting their popular Magento 1 extensions.

In ours case, We are already into Magento 2 development: busy porting some of our popular Magento 1 Extensions and side by side learning new concepts introduced by Magento 2.

In this tutorial we will learn how to create a custom shipping module in Magento2 which is fairly easy as compared to CRUD & Payment modules.

Assumptions:
Namespace = MagePsycho
Module = Customshipping

Custom Shipping Module Development

1. Register the module
File: app/code/MagePsycho/Customshipping/etc/module.xml

<?xml version="1.0"?>
<!--
/**
 * @category   MagePsycho
 * @package    MagePsycho_Customshipping
 * @author     magepsycho@gmail.com
 * @website    http://www.magepsycho.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="MagePsycho_Customshipping" setup_version="1.0.0">
    </module>
</config>

2. Add System Configuration Settings
File: app/code/MagePsycho/Customshipping/etc/adminhtml/system.xml

<?xml version="1.0"?>
<!--
/**
 * @category   MagePsycho
 * @package    MagePsycho_Customshipping
 * @author     magepsycho@gmail.com
 * @website    http://www.magepsycho.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../Magento/Config/etc/system_file.xsd">
    <system>
        <section id="carriers" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
            <group id="magepsycho_customshipping" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Shipping</label>               
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Shipping Cost</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="specificerrmsg" translate="label" type="textarea" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Displayed Error Message</label>
                </field>
                <field id="sallowspecific" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Applicable Countries</label>
                    <frontend_class>shipping-applicable-country</frontend_class>
                    <source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
                <field id="showmethod" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
            </group>
        </section>
    </system>
</config>

3. Define shipping carrier model
File: app/code/MagePsycho/Customshipping/etc/config.xml

<?xml version="1.0"?>
<!--
/**
 * @category   MagePsycho
 * @package    MagePsycho_Customshipping
 * @author     magepsycho@gmail.com
 * @website    http://www.magepsycho.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../Magento/Store/etc/config.xsd">
    <default>
        <carriers>
            <magepsycho_customshipping>
                <active>0</active>
                <sallowspecific>0</sallowspecific>
                <price>0</price>
                <model>MagePsycho\Customshipping\Model\Carrier\Customshipping</model>
                <name>Fixed</name>
                <title>Custom Shipping</title>
                <specificerrmsg>This shipping method is not available. To use this shipping method, please contact us.</specificerrmsg>
            </magepsycho_customshipping>
        </carriers>
    </default>
</config>

Notes: ‘config/default/carriers/model’ node is used to define Model class for Custom Shipping which will be responsible for handling the Shipping Charges.
In fact this file is also used to set default values for Shipping Settings.

4. Create shipping carrier model class
File: app/code/MagePsycho/Customshipping/Model/Carrier/Customshipping.php

<?php

namespace MagePsycho\Customshipping\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;

/**
 * @category   MagePsycho
 * @package    MagePsycho_Customshipping
 * @author     magepsycho@gmail.com
 * @website    http://www.magepsycho.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
class Customshipping extends \Magento\Shipping\Model\Carrier\AbstractCarrier implements
	\Magento\Shipping\Model\Carrier\CarrierInterface
{
	/**
	 * @var string
	 */
	protected $_code = 'magepsycho_customshipping';

	/**
	 * @var bool
	 */
	protected $_isFixed = true;

	/**
	 * @var \Magento\Shipping\Model\Rate\ResultFactory
	 */
	protected $_rateResultFactory;

	/**
	 * @var \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
	 */
	protected $_rateMethodFactory;

	/**
	 * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
	 * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
	 * @param \Psr\Log\LoggerInterface $logger
	 * @param \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory
	 * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
	 * @param array $data
	 */
	public function __construct(
		\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
		\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
		\Psr\Log\LoggerInterface $logger,
		\Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
		\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
		array $data = []
	) {
		$this->_rateResultFactory = $rateResultFactory;
		$this->_rateMethodFactory = $rateMethodFactory;
		parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
	}

	/**
	 * @param RateRequest $request
	 * @return \Magento\Shipping\Model\Rate\Result
	 * @SuppressWarnings(PHPMD.UnusedLocalVariable)
	 */
	public function collectRates(RateRequest $request)
	{
		if (!$this->getConfigFlag('active')) {
			return false;
		}

		/** @var \Magento\Shipping\Model\Rate\Result $result */
		$result = $this->_rateResultFactory->create();

		$shippingPrice = $this->getConfigData('price');

		$method = $this->_rateMethodFactory->create();

		$method->setCarrier($this->_code);
		$method->setCarrierTitle($this->getConfigData('title'));

		$method->setMethod($this->_code);
		$method->setMethodTitle($this->getConfigData('name'));

		$method->setPrice($shippingPrice);
		$method->setCost($shippingPrice);
		
		$result->append($method);

		return $result;
	}

	/**
	 * Get allowed shipping methods
	 *
	 * @return array
	 */
	public function getAllowedMethods()
	{
		return [$this->_code => $this->getConfigData('name')];
	}
}

That’s all from development point of view.
Now you need to enable the module by running following series of commands(from root of your Magento 2 installation):

php bin/magento module:enable MagePsycho_Customshipping --clear-static-content
php bin/magento setup:upgrade

Go to the backend: System > Configuration > Sales > Shipping Methods > You will see new tab named ‘Custom Shipping’. And the settings looks like:

Magento 2 - Custom Shipping Method

Magento 2 – Custom Shipping Method

And the frontend looks like:

Magento2  - Custom Shipping at Checkout

Magento2 – Custom Shipping – Checkout

Magento2 - Custom Shipping - Checkout Summary

Magento2 – Custom Shipping – Checkout Summary

This module is available on GitHub.
In order to install the module from github:
Installation Using Composer

composer config repositories.magesycho-magento2-custom-shipping git git@github.com:MagePsycho/magento2-custom-shipping.git
composer require magepsycho/magento2-custom-shipping:dev-master

Installation Using Zip
Download the zip file, extract & upload the files to path: app/code/MagePsycho/Customshipping/

After installation, don’t forget to enable the Module as described above.

Please do comment below if you have any queries regarding Shipping Module development in Magento 2.
Happy Magento 2 Coding!

Change shipping price & handling fee on the fly in Magento

October 10, 2013  |  3 Comments  |  by Raj (MagePsycho)  |  Magento

Problem: How do I change shipping prices and handling fees on the fly?

You may want to have different shipping prices for different scenarios. Your scenario could be any of the following:

  1. 1. Different shipping price based on shipping address
  2. 2. Different shipping price based on weight
  3. 3. Different shipping price based on categories
  4. 4. Different shipping price based on no of items
  5. 5. Different shipping price based on your custom module
  6. 6. Different shipping price based on some conditions

There may be many solutions. A flexible solution which works for most cases is to develop a custom module which implements the event called ‘sales_quote_collect_totals_before’

Solution:

Assuming the skeleton module: MagePsycho_Shipmentfilter has already been created.
1. Register the event ‘sales_quote_collect_totals_before’:
Add the following xml code in app/code/local/MagePsycho/Shipmentfilter/etc/config.xml

...
<events>
    <sales_quote_collect_totals_before>
        <observers>
            <your_module>
                <type>singleton</type>
                <class>shipmentfilter/observer</class>
                <method>salesQuoteCollectTotalsBefore</method>
            </your_module>
        </observers>
    </sales_quote_collect_totals_before>
</events>
...

2. Implement the observer model:
Create the observer file: app/code/local/MagePsycho/Shipmentfilter/Model/Observer.php and add the following code:

class MagePsycho_Shipmentfilter_Model_Observer
{
    public function salesQuoteCollectTotalsBefore(Varien_Event_Observer $observer)
    {
        /** @var Mage_Sales_Model_Quote $quote */
        $quote = $observer->getQuote();
        $someConditions = true; //this can be any condition based on your requirements
        $newHandlingFee = 15;
        $store    = Mage::app()->getStore($quote->getStoreId());
        $carriers = Mage::getStoreConfig('carriers', $store);
        foreach ($carriers as $carrierCode => $carrierConfig) {
            if($carrierCode == 'fedex'){
                if($someConditions){
                    Mage::log('Handling Fee(Before):' . $store->getConfig("carriers/{$carrierCode}/handling_fee"), null, 'shipping-price.log');
                    $store->setConfig("carriers/{$carrierCode}/handling_type", 'F'); #F - Fixed, P - Percentage                  
                    $store->setConfig("carriers/{$carrierCode}/handling_fee", $newHandlingFee);
    
                    ###If you want to set the price instead of handling fee you can simply use as:
                    #$store->setConfig("carriers/{$carrierCode}/price", $newPrice);
    
                    Mage::log('Handling Fee(After):' . $store->getConfig("carriers/{$carrierCode}/handling_fee"), null, 'shipping-price.log');
                }
            }
        }
    }
}

Notes: Here we are adding handling fee of 15 for Fedex shipping carrier on some conditions.
If you want to set a new price instead of a handling fee then you can simply use:

$store->setConfig("carriers/{$carrierCode}/price", $newPrice);

in above code.
Similarly you can add any custom shipping price for any other shipping method.

3. Bingo!

How to filter shipping method in onepage checkout?

April 21, 2013  |  2 Comments  |  by Raj (MagePsycho)  |  Latest, Magento

Introduction

You may want to filter shipping method in onepage checkout for one of the following cases:
– Filter shipping method based on Customer Group
– Filter Shipping method based on Country, State, Zipcode etc
– Filter Shipping method based on products
– etc.

Unlike event: ‘payment_method_is_active’ for payment method filtration, we don’t have similar event: ‘shipping_method_is_active’ available for shipping method. May be Magento team forgot to implement it or it was too hard to implement due to the structure.
Whatever may be the reason we still can filter by overriding: Mage_Shipping_Model_Shipping::collectCarrierRates()

Solution

Suppose a skeleton module(MagePsycho_Shipmentfilter) has already been created. And for example purpose we will be hiding flat rate shipping for non-logged in customer.
1> Rewrite the shipping model class: ‘Mage_Shipping_Model_Shipping’
File: app/code/local/MagePsycho/Shipmentfilter/etc/config.xml
Code:

...
<global>
	...
	<models>
		<shipping>
			<rewrite>
				<shipping>MagePsycho_Shipmentfilter_Model_Shipping</shipping>
			</rewrite>
		</shipping>
	</models>
	...
</global>

2> Override the method: collectCarrierRates()
File: app/code/local/MagePsycho/Shipmentfilter/Model/Shipping.php
Code:

<?php
/**
 * @category   MagePsycho
 * @package    MagePsycho_Shipmentfilter
 * @author     magepsycho@gmail.com
 * @website    http://www.magepsycho.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 	*/
class MagePsycho_Shipmentfilter_Model_Shipping extends Mage_Shipping_Model_Shipping
{
    public function collectCarrierRates($carrierCode, $request)
    {
        if (!$this->_checkCarrierAvailability($carrierCode, $request)) {
            return $this;
        }
        return parent::collectCarrierRates($carrierCode, $request);
    }

	protected function _checkCarrierAvailability($carrierCode, $request = null)
	{
		$isLoggedIn	 = Mage::getSingleton('customer/session')->isLoggedIn();
		if(!$isLoggedIn){
			if($carrierCode == 'flatrate'){ #Hide Flat Rate for non logged in customers
				return false;
			}
		}
		return true;
	}
}

3> That’s all.

Please share any other ideas you have.

Hide other shipping methods when free shipping is enabled

March 19, 2013  |  1 Comments  |  by Raj (MagePsycho)  |  Latest, Magento

When free shipping method is enabled, it is shown along with other available shipping methods unlike free payment method ‘Zero Subtotal Checkout’.
There is no harm in showing other payment methods along with free shipping method. Nevertheless some merchants wants to hide rest of the methods when it is enabled.

There are many ways to do it. One of the way is to override the method:
Mage_Checkout_Block_Onepage_Shipping_Method_Available::getShippingRates()

1. Rewrite the block class

File: app/code/local/MagePsycho/Shipmentfilter/etc/config.xml:
Code:

...
<blocks>
	...
	<checkout>
		<rewrite>
			<onepage_shipping_method_available>MagePsycho_Shipmentfilter_Block_Onepage_Shipping_Method_Available</onepage_shipping_method_available>
		</rewrite>
	</checkout>
	...
</blocks>
...

2. Override the getShippingRates() method

File: app/code/local/MagePsycho/Shipmentfilter/Block/Onepage/Shipping/Method/Available.php:
Code:

<?php
/**
 * @category   MagePsycho
 * @package    MagePsycho_Shipmentfilter
 * @author     magepsycho@gmail.com
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
class MagePsycho_Shipmentfilter_Block_Onepage_Shipping_Method_Available extends Mage_Checkout_Block_Onepage_Shipping_Method_Available
{
	public function getShippingRates()
	{
		$rates = parent::getShippingRates();
		if (array_key_exists('freeshipping', $rates)) {
			$rates = array('freeshipping' => $rates['freeshipping']);
		}

		return $rates;
	}
}

3. Refresh the cache. Bingo!