Creating Custom Shipping Methods In Magento

Advertisement

In this tutorial, we will use Magento’s powerful shipping-method code abstraction to create a shipping carrier. We will create three shipping methods that provide a fixed shipping price, allow for free shipping promotions, define logic based on an item’s weight and, finally, make it all configurable in the admin panel.

We will cover the following:

  • Extend the abstract shipping class and implement the required methods.
  • Make the shipping method configurable in Magento’s admin panel.
  • Work with promotions to allow for free shipping.
  • Allow tracking codes to be set against an order.

Before We Start

This tutorial assumes that you are familiar with how to create a Magento module. If you are not, please first refer to an earlier tutorial in this series, “The Basics of Creating a Magento Module1.” To begin, you will need a Community or Enterprise installation of Magento, either locally or on a server that you are able to access.

The logic we will implement in this tutorial could be client-specific, so we will implement our module as a “local module” and, therefore, create it in app/code/local. Let’s start by creating the following file structure:

app
  - code
    - local
      - SmashingMagazine
        - MyCarrier
          - Model
            - Carrier.php
          - etc
            - config.xml
            - system.xml
  - etc
    - modules
      - SmashingMagazine_MyCarrier.xml

Now we can create SmashingMagazine_MyCarrier.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <SmashingMagazine_MyCarrier>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Shipping />
            </depends>
        </SmashingMagazine_MyCarrier>
    </modules>
</config>

Notice the dependency on the shipping module. This ensures that our SmashingMagazine MyCarrier module will load after the Mage Shipping module, and it will throw an error if the Mage Shipping module has been disabled.

Carriers, Methods, Requests And Results

Before continuing, we should understand the terminology that Magento uses throughout its shipping abstraction. A “carrier” represents a shipping carrier in the sense you would expect (DPD, FedEx, etc.). Each carrier has one or many shipping methods, which contain the carrier code, the carrier title, the method code, the method title, a price to be paid by the customer and a cost of shipping to the retailer (optional).

During the checkout process, Magento creates a shipping-rate “request” object that contains all of the shipping information. The request can be used to determine which rates apply. For example, an “express” shipping method might not apply to orders under $10. All applicable rates are then “appended” to a shipping-rate “result” object, which generates a list of methods for the customer to choose from.

The following list names these concepts defined above, along with their representation as Magento classes:

  • Request
    Mage_Shipping_Model_Rate_Request
  • Result
    Mage_Shipping_Model_Rate_Result
  • Method
    Mage_Shipping_Model_Rate_Result_Method
  • Carrier
    Any class that extends the abstract class Mage_Shipping_Model_Carrier_Abstract and implements the interface Mage_Shipping_Model_Carrier_Interface

Extending The Shipping Abstract

To create our shipping carrier, we need to extend Mage_Shipping_Model_Carrier_Abstract, implement Mage_Shipping_Model_Carrier_Interface and add the required abstract methods.

The most important method is collectRates. This is the method that receives a shipping request, appends applicable shipping methods and returns a shipping result.

Copy the following code into app/code/local/SmashingMagazine/MyCarrier/Model/Carrier.php:

<?php
class SmashingMagazine_MyCarrier_Model_Carrier
    extends Mage_Shipping_Model_Carrier_Abstract
    implements Mage_Shipping_Model_Carrier_Interface
{
    protected $_code = 'smashingmagazine_mycarrier';

    public function collectRates(
        Mage_Shipping_Model_Rate_Request $request
    )
    {
        return Mage::getModel('shipping/rate_result');
    }

    public function getAllowedMethods()
    {
        return array();
    }
}

This is the skeleton for a shipping method class, but it is pretty useless because we have no shipping methods.

Let’s start by hardcoding a method. This method will be called “standard” and have a price of $4.99. For now, we will assume there is no cost to the retailer.

<?php
class SmashingMagazine_MyCarrier_Model_Carrier
    extends Mage_Shipping_Model_Carrier_Abstract
    implements Mage_Shipping_Model_Carrier_Interface
{
    protected $_code = 'smashingmagazine_mycarrier';

    public function collectRates(
        Mage_Shipping_Model_Rate_Request $request
    )
    {
        $result = Mage::getModel('shipping/rate_result');
        /* @var $result Mage_Shipping_Model_Rate_Result */

        $result->append($this->_getStandardShippingRate());

        return $result;
    }

    protected function _getStandardShippingRate()
    {
        $rate = Mage::getModel('shipping/rate_result_method');
        /* @var $rate Mage_Shipping_Model_Rate_Result_Method */

        $rate->setCarrier($this->_code);
        /**
         * getConfigData(config_key) returns the configuration value for the
         * carriers/[carrier_code]/[config_key]
         */
        $rate->setCarrierTitle($this->getConfigData('title'));

        $rate->setMethod('standand');
        $rate->setMethodTitle('Standard');

        $rate->setPrice(4.99);
        $rate->setCost(0);

        return $rate;
    }

    public function getAllowedMethods()
    {
        return array(
            'standard' => 'Standard',
        );
    }
}

Now we are just one step away from a working shipping method — the module configuration file.

Module Configuration

The module configuration has the standard structure (as detailed in “The Basics of Creating a Magento Module2”). Copy the following into app/code/local/SmashingMagazine/MyCarrier/etc/config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <SmashingMagazine_MyCarrier>
            <module>0.0.1</module>
        </SmashingMagazine_MyCarrier>
    </modules>
    <global>
        <models>
            <smashingmagazine_mycarrier>
                <class>SmashingMagazine_MyCarrier_Model</class>
            </smashingmagazine_mycarrier>
        </models>
    </global>
    <!-- Default configuration -->
    <default>
        <carriers>
            <smashingmagazine_mycarrier>
                <active>1</active>
                <!--
                     This configuration should not be made visible
                     to the administrator, because it specifies
                     the model to be used for this carrier.
                -->
                <model>smashingmagazine_mycarrier/carrier</model>
                <!--
                    The title as referenced in the carrier class
                -->
                <title>Smashing Magazine Carrier</title>
                <!--
                    The sort order specifies the position that
                    this carrier appears relative to the other
                    carriers available in checkout.
                -->
                <sort_order>10</sort_order>
                <!--
                    Out of the box, Magento offers shipping
                    carriers the ability to restrict themselves
                    to specific countries. For this configuration
                    option, 0 means allow all countries available,
                    and 1 means allow all countries specified
                    in the country list that we will add later
                    in system.xml
                -->
                <sallowspecific>0</sallowspecific>
            </smashingmagazine_mycarrier>
        </carriers>
    </default>
</config>

This default configuration “registers” the model we have just created as a shipping carrier. As you may know, Magento merges all of its configuration XML together and caches the result (if the cache is enabled). When a customer loads the shipping-method list, Magento loops through all of the carriers in the carriers node of the configuration and loads the shipping methods from the models determined by the “active” carriers.

We should now be able to see our shipping method in the checkout.

Making It Configurable

We have already specified the default configuration for this module. So, let’s make our module configurable in the admin panel by copying the following into app/code/local/SmashingMagazine/etc/system.xml:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <sections>
        <carriers translate="label" module="shipping">
            <groups>
                <smashingmagazine_mycarrier translate="label">
                    <label>Smashing Magazine Carrier</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>2</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <!--
                            The following fields are available
                            to modify in the admin panel.
                            The values are saved in the
                            database.

                            This shipping carrier abstract checks
                            this value to determine whether
                            the carrier should be shown.
                        -->
                        <active translate="label">
                            <label>Enabled</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>1</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </active>
                        <!--
                            This value can be used to specify a
                            custom title for our method.
                        -->
                        <title translate="label">
                            <label>Title</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>2</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </title>
                        <!--
                            The sort order is used in Magento
                            to determine what order the carrier
                            will appear in relative to the
                            other carriers available.
                        -->
                        <sort_order translate="label">
                            <label>Sort Order</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>100</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </sort_order>
                        <!--
                            This value is used to specify whether
                            the carrier is available only for
                            specific countries or all countries
                            available in the current Magento
                            installation.
                        -->
                        <sallowspecific translate="label">
                            <label>Ship to Applicable Countries</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>90</sort_order>
                            <frontend_class>shipping-applicable-country</frontend_class>
                            <source_model>adminhtml/system_config_source_shipping_allspecificcountries</source_model>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </sallowspecific>
                        <!--
                            If 'specific countries' is chosen
                            in the previous option, then this field
                            allows the administrator to specify
                            which specific countries this carrier
                            should be available for.
                        -->
                        <specificcountry translate="label">
                            <label>Ship to Specific Countries</label>
                            <frontend_type>multiselect</frontend_type>
                            <sort_order>91</sort_order>
                            <source_model>adminhtml/system_config_source_country</source_model>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <can_be_empty>1</can_be_empty>
                        </specificcountry>
                    </fields>
                </smashingmagazine_mycarrier>
            </groups>
        </carriers>
    </sections>
</config>

These fields are visible in the admin panel by navigating to System → Configuration → Shipping Method → Smashing Magazine Carrier.

Using Multiple Shipping Methods

Express Shipping

So far, we have added a standard shipping method for the price of $9.99. However, the customer may wish to pay more to receive their order faster. The following code creates a shipping rate with a higher price and different shipping code:

protected function _getExpressShippingRate()
{
    $rate = Mage::getModel('shipping/rate_result_method');
    /* @var $rate Mage_Shipping_Model_Rate_Result_Method */
    $rate->setCarrier($this->_code);
    $rate->setCarrierTitle($this->getConfigData('title'));
    $rate->setMethod('express');
    $rate->setMethodTitle('Express (Next day)');
    $rate->setPrice(12.99);
    $rate->setCost(0);
    return $rate;
}

To make this shipping rate appear next to the standard rate that we created earlier, we will need to modify the code in the collectRates method to append the new rate. Add the following before the return statement:

$result->append($this->_getExpressShippingRate());

Finally, add the shipping method to the allowed methods array in getAllowedMethods:

public function getAllowedMethods()
{
    return array(
        'standard' => 'Standard',
        'express' => 'Express',
    );
}

Free Shipping

Many websites offer free shipping when a customer spends over a certain amount or satisfies certain conditions. We need to be able to do the same here. In Magento, you can set up a “Shopping Cart Rule.” With it, you can specify a set of conditions and define actions if those conditions are met; one of those actions is free shipping.

If free shipping is available for a customer, then the request object will populated with is_free_shipping set to 1. We need to check for and handle this possibility in our shipping method. Add the following before the return statement in the collectRates method:

if ($request->getFreeShipping()) {
    /**
     *  If the request has the free shipping flag,
     *  append a free shipping rate to the result.
     */
    $freeShippingRate = $this->_getFreeShippingRate();
    $result->append($freeShippingRate);
}

Add the following code to app/code/local/SmashingMagazine/MyCarrier/Model/Carrier.php:

protected function _getFreeShippingRate()
{
    $rate = Mage::getModel('shipping/rate_result_method');
    /* @var $rate Mage_Shipping_Model_Rate_Result_Method */
    $rate->setCarrier($this->_code);
    $rate->setCarrierTitle($this->getConfigData('title'));
    $rate->setMethod('free_shipping');
    $rate->setMethodTitle('Free Shipping (3 - 5 days)');
    $rate->setPrice(0);
    $rate->setCost(0);
    return $rate;
}

Remember to add the method to the allowed methods array:

public function getAllowedMethods()
{
    return array(
        'standard' => 'Standard',
        'express' => 'Express',
        'free_shipping' => 'Free Shipping',
    );
}

Taking It A Bit Further

Tracking Deliveries

Tracking numbers may be added to shipments through either the admin panel or an API. But to make our shipping methods visible in the admin panel, we will have to overwrite the isTrackingAvailable method in the abstract to return true.

Add the following method to the end of SmashingMagazine_MyCarrier_Model_Carrier.

public function isTrackingAvailable()
{
    return true;
}

You should now see the shipping carriers and methods available in the delivery courier drop-down menu when you try to place a shipment in the admin panel.

Using the Weight

Earlier, we added a more expensive express shipping method. But heavier items that require complex shipping arrangements might not be available for next-day delivery. We can check for this using the weight attribute of the request object by wrapping the code that appends the shipping method to the shipping result:

// ...
$expressWeightThreshold =
    $this->getConfigData('express_weight_threshold');

$eligibleForExpressDelivery = true;
foreach ($request->getAllItems() as $_item) {
    if ($_item->getWeight() > $expressWeightThreshold) {
        $eligibleForExpressDelivery = false;
    }
}

if ($eligibleForExpressDelivery) {
    $result->append($this->_getExpressShippingRate());
}
// ...

Notice that we have added a reference to the configuration. To make this appear in the admin panel, we need to add the following XML to the app/code/local/SmashingMagazine/MyCarrier/etc/system.xml file in the fields node:

<express_weight_threshold translate="label">
    <label>Express Weight Threshold</label>
    <frontend_type>text</frontend_type>
    <sort_order>100</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</express_weight_threshold>

Summary

With a relatively small amount of code, we have been able to define our own shipping logic that integrates with checkout, the admin panel and even the shopping-cart promotions. You can learn much more about creating shipping modules in Magento by looking at the examples in the core files — namely, Mage_Usa and Mage_Shipping.

The code from this tutorial can be downloaded here3.

I welcome any questions and would love to hear your feedback in the comments area below.

(al, ea)

Footnotes

  1. 1 http://www.smashingmagazine.com/2012/03/01/basics-creating-magento-module/
  2. 2 http://www.smashingmagazine.com/2012/03/01/basics-creating-magento-module/
  3. 3 http://amp.co/K457V

↑ Back to topShare on Twitter

Matthew wrote his first piece of code when he was just five years old in Locomotive BASIC. Ever since he has dabbled in everything from MATLAB to Java, but took particular favour towards PHP and web development. A Zend Certified and Magento Certified Developer Plus, Matthew has delivered complex multichannel solutions for major retailers like Poundland and SKINS at Ampersand Commerce.

Advertising
  1. 1

    Really good post. I saved it to my evernote to use when I need. Thank you for sharing.

    2
  2. 2

    Will try this now, this is what I am looking for my client asked me about this.. thanks!

    0
  3. 3

    Hey, want a really good advise?
    D O N ‘ T U S E M A G E N T O !!!!
    It’s the most annoying framework ever. Try alternatives and be amazed with the structure simplicity and user friendly systems of the competitors. You’ll thank me later.

    -5
    • 4

      Thanks for the article, I’ll give it a go. I’m assuming it works for Magento 1.8 given the date of the article.
      @Ze – yes it’s a complex product, but it’s also dominating large-scale online businesses for a reason. It’s not designed for small shops, unsophisticated users, or hackers who are usually the first to criticise it. I’d change your advice to ‘don’t use Magento if you’re going to try and get it built on the cheap in the third world’. Magento development really shows up the shoddy, poor quality, low-skilled work or many of these outfits in places like India and Russia, creating disastrous implementations. If you’re going to get Magento, you need to pay top dollar for its development in a first world country with quality coders, which makes it expensive, but does cut out the fly-by-night 3rd world developer market and means the user community is a lot more sophisticated.
      My only gripe with Magento is its processing load – the EAV data model and total OOP approach really makes most servers grind. In my mind, it really only runs effectively on a VPS which is a big bit of overhead to handle.

      0
  4. 6

    Excellent post. Posted just at the right time… It will help me a lot today. Thank you.

    0
  5. 7

    Interesting article. One question: don’t you have to omit the coma in arrays at the last item? You have:
    public function getAllowedMethods()
    {
    return array(
    ‘standard’ => ‘Standard’,
    ‘express’ => ‘Express’,
    ‘free_shipping’ => ‘Free Shipping’,
    );
    }
    Is the comma at the end of free_shipping supposed to be there? You do that for the same method elsewhere in this post.

    0
    • 8

      In PHP this is allowed but my preference is not too.
      Doing this in Javascript will leave IE users with an error and a bug for you to chase down.

      0
  6. 9

    Thank you for your comments!

    @Joren: Thank you. You don’t ‘have’ to omit the comma, however I like to comply with the Zend Coding Standards for Magento.

    @Ze: I agree that Magento is complex, however this complexity comes with the features and customisability that most of its competitors can’t compete with. I have tried alternatives and I personally feel that Magento offers a lot more in terms of scalability.

    0
    • 10

      Thank you @Matthew for great article, you explained it great,, and that is very hard when Magento is the subject!

      @Girish,

      I don’t think you can have checkbox there,, that would mean that you can deliver goods via more that one courier.. no way!

      0
  7. 11

    I want to add an input box in the shipping method block in this carrier to take UPS account number from customer. I need to show the account number in order details page in magento admin panel. Can you/anyone please guide me on this? I am running out of time so can you please respond asap?

    0
  8. 12

    I am trying to calculate shipping price based on dimensions, post code and type of parcel(regular/express). I am able to calculate that using AUSPOST api but the problem is to save the shipping rate outside of the collectRates method in magento. If I do this…

    $result = Mage::getModel(‘shipping/rate_result’);
    $method = Mage::getModel(‘shipping/rate_result_method’);
    $method->setCarrier(“excellence_excellence”);
    $method->setMethod(“excellence_excellence”);
    $method->setCarrierTitle(“”);
    $method->setMethodTitle(“”);
    $method->setPrice($myValue);
    $method->setCost($myValue);
    $result->append($method);

    It does not work because the $result has to be saved in the quote. Can anyone help me on this? at http://proresults.in

    0
  9. 13

    A great article that actually makes sense unlike most of the others I’ve found on shipping methods.

    I’ve successfully implemented your code (called the carrier “eashipping” with a method of “eashipping”) and can see it active if I call getActiveCarriers()

    I’m creating a quote programatically (pulling in orders from another source) and the quote creates successfully but the shipping cost is always zero, even when I try to overide the cost within the main code.

    The code I have is :

    // Set shipping
    $shippingPrice = 20;
    Mage::register(‘shipping_cost’, $shippingPrice);
    $quote->getShippingAddress()->setSameAsBilling(0)->setLimitCarrier(‘eashipping’)->setShippingMethod(‘eashipping_eashipping’)->setShippingDescription(‘eashipping’)->setShippingAmount($shippingPrice)->setBaseShippingAmount($shippingPrice);

    $quote->getShippingAddress()->collectShippingRates()->collectTotals();

    echo “Price : $shippingPrice”;

    $quote->getBillingAddress()->setLimitCarrier(‘eashipping’)->setShippingMethod(‘eashipping_eashipping’)->setCollectShippingRates(true)->collectTotals();

    Any ideas

    0
  10. 14

    Hey Matthew
    Excellent post. I think you laid everything out there in an easy to understand way, which is always appreciated. Anyway, we thought this would be valuable information for our readers, and we wanted to let you know that we included your post in our roundup of the best content from January. http://blog.nexcess.net/2014/02/05/januarys-best-expressionengine-wordpress-and-magento-content/

    0
  11. 15

    I wanted to thank you for your nice article! great help!!!!

    0
  12. 16

    Thanks for the great tutorial. Really struggled finding something with such detail about the way the Magento shipping methods worked.

    0
  13. 17

    thank you very much for this module. it works perfect on magento 1.7.0.2.

    0
  14. 18

    I want to add a text box with the shipping method. Please suggest me how can i do this.

    Thank’s in advance

    0
  15. 19

    Hi,
    I followed all instructions and everything is visible in backend in delivery methods. However this method is not showing in front end. Any ideas? Do I need to enable compilation?

    0
    • 20

      Fixed the above issue but have another. The new method is set to be selected by default. How do I set it to be optional not default??

      0
  16. 21

    Hi Matthew, I’m writing you from Argentina, sorry for my english.
    Realy your post is very usefull, I will try to implement this weekend ;-)
    I have a question, “OCA” is the name of one carrier provider in Argentina, they provide a webservice interface to access and query the cost to shipping from certain Postal Code to another, can access a guide to know where is it your merchansis an anothers posibilitys too.
    My question is, which php file I have to write the calls to those webservices to implement the third-party service? is in ../Carrier.php ??

    Best regards and many thanks
    Fernando

    0
  17. 22

    Hi Matthew,
    Thanks for the great article. I used this in my code. One more thing I need here is one check-box along with the delivery charges radio button. Is it possible.? Please suggest me how can i do this.

    Thank’s in advance

    0
  18. 23

    Thank you so much.

    0
  19. 24

    Matthew-

    We are trying to set up our furniture store using Magento and are looking for a way to have up to 4 shipping choices per product ie: Free curbside delivery, Basic inside delivery +$99, White Glove inside delivery +$199, etc. What is the best/easiest way of doing this?
    (we currently have this shipping data and its associated product info in a db)

    Thank in advance for your help.

    0
  20. 25

    Hey, i followed your tutorial and it worked brilliantly so thank you fro that. One ting though how would i hide these shipping rates using the unset method, As i dont want these rates to be avaible on certain products. I have the code that checks for through the produtcs so thats no trouble and im using

    unset($_shippingRateGroups["flatrate"]);

    to disable the standard magento shipping. How would i be able to disable yours??

    thank you

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top