Magento – Magento 2 : How to display Shipping Rates on a Product Page

magento2multishippingproduct-pageshipping-methods

I want to display Current product shipping rate on the product detail page.

I have tried below code,

$qty = 1;
        $countryId='IN';
        $storeId = 1;
        $zipcode = 360002;
        $product = $this->_catalogProduct->load($id);
        $item = $objectManager->create('Magento\Quote\Model\Quote\Item')->setProduct($product)->setQty(1);
        $store = $objectManager->create('Magento\Store\Model\Store')->load($storeId);
        $request = $objectManager->create('Magento\Quote\Model\Quote\Address\RateRequest')->setAllItems(array($item))
            ->setDestCountryId($country)
            ->setPackageValue($product->getFinalPrice())
            ->setOrigPostcode($zipcode)  
            ->setPackageValueWithDiscount($product->getFinalPrice())
            ->setPackageWeight($product->getWeight())
            ->setPackageQty(1)
            ->setPackagePhysicalValue($product->getFinalPrice())
            ->setFreeMethodWeight(0)
            ->setStoreId($store->getId())
            ->setWebsiteId($store->getWebsiteId())
            ->setFreeShipping(0)
            ->setBaseCurrency($store->getBaseCurrency())
            ->setBaseSubtotalInclTax($product->getFinalPrice());
         $result = $this->_rateCollector->create()->collectRates($request)->getResult();

I want to display more than one enabled shipping methods rates in product display page. I have tried this code but i am getting this kind of error.

Uncaught Error: Call to a member function getItemsQty() on null
in/vendor/magento/module-quote/Model/Quote/Item.php:275

Please help me on this.

Best Answer

Although Manish's answer does show how to create a block on the product page to obtain the available shipping rates, the REST API used in the tutorial (rest/default/V1/carts/mine/estimate-shipping-methods) only fetches this for the items that are already in the cart, and not just for the currently viewed product. This is because Magento relies on items that are added to the active quote to obtain the available methods and costs, of which was fetched from the database and utilised. Despite this, we can create a blank quote in memory without saving it into the database, fill in the required data, and then apply this to the shipping calculator.

The best approach to this is to create a new API endpoint, which you can then swap out the endpoint defined in the tutorial with yours.

Interface

<?php

namespace Your\Module\Api;

use Magento\Quote\Api\Data\AddressInterface;

/**
 * Interface ShipmentForProductEstimationInterface
 * @api
 * @since 100.0.7
 */
interface ShipmentForProductEstimationInterface
{
    /**
     * Estimate shipping by address and product, and return list of available shipping methods
     * @param string $sku
     * @param AddressInterface $address
     * @return \Magento\Quote\Api\Data\ShippingMethodInterface[] An array of shipping methods
     */
    public function estimateByProductAndAddress($sku, AddressInterface $address);
}

Model

<?php

namespace Your\Module\Model\Api;

use Magento\Framework\App\ObjectManager;

class ShipmentForProductEstimation implements \Your\Module\Api\ShipmentForProductEstimationInterface {

    /**
     * Shipping method converter
     *
     * @var \Magento\Quote\Model\Cart\ShippingMethodConverter
     */
    protected $converter;

    /**
     * @var \Magento\Quote\Model\Quote\TotalsCollector
     */
    protected $totalsCollector;

    /**
     * @var \Magento\Framework\Reflection\DataObjectProcessor
     */
    protected $dataProcessor;

    /**
     * @var \Magento\Quote\Model\Quote
     */
    protected $quote;

    /**
     * @var \Magento\Catalog\Api\ProductRepositoryInterface
     */
    protected $product;

    /**
     * @var \Magento\Quote\Model\Quote\Item
     */
    protected $item;

    public function __construct(
        \Magento\Quote\Model\Cart\ShippingMethodConverter $converter,
        \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector,
        \Magento\Quote\Model\Quote $quote,
        \Magento\Catalog\Api\ProductRepositoryInterface $product,
        \Magento\Quote\Model\Quote\Item $item
    ) {
        $this->converter = $converter;
        $this->totalsCollector = $totalsCollector;
        $this->quote = $quote;
        $this->product = $product;
        $this->item = $item;
    }

    /**
     * @inheritdoc
     */
    public function estimateByProductAndAddress($sku, \Magento\Quote\Api\Data\AddressInterface $address)
    {
        $output = [];

        $address->setCountryId('IN');

        $p = $this->product->get($sku);

        $this->item->setProduct($p);
        $this->item->setQty(1);

        $this->quote->addItem( $this->item );

        $shippingAddress = $this->quote->getShippingAddress();
        $shippingAddress->addData($this->extractAddressData($address));
        $shippingAddress->setCollectShippingRates(true);

        $this->totalsCollector->collectAddressTotals($this->quote, $shippingAddress);
        $shippingRates = $shippingAddress->getGroupedAllShippingRates();
        foreach ($shippingRates as $carrierRates) {
            foreach ($carrierRates as $rate) {
                $output[] = $this->converter->modelToDataObject($rate, $this->quote->getQuoteCurrencyCode());
            }
        }
        return $output;

    }

    /**
     * Get transform address interface into Array
     *
     * @param \Magento\Framework\Api\ExtensibleDataInterface  $address
     * @return array
     */
    protected function extractAddressData($address)
    {
        $className = \Magento\Customer\Api\Data\AddressInterface::class;
        if ($address instanceof \Magento\Quote\Api\Data\AddressInterface) {
            $className = \Magento\Quote\Api\Data\AddressInterface::class;
        } elseif ($address instanceof \Magento\Quote\Api\Data\EstimateAddressInterface) {
            $className = \Magento\Quote\Api\Data\EstimateAddressInterface::class;
        }
        return $this->getDataObjectProcessor()->buildOutputDataArray(
            $address,
            $className
        );
    }

    /**
     * Gets the data object processor
     *
     * @return \Magento\Framework\Reflection\DataObjectProcessor
     */
    protected function getDataObjectProcessor()
    {
        if ($this->dataProcessor === null) {
            $this->dataProcessor = ObjectManager::getInstance()->get(\Magento\Framework\Reflection\DataObjectProcessor::class);
        }
        return $this->dataProcessor;
    }

}

di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Your\Module\Api\ShipmentForProductEstimationInterface" type="Your\Module\Model\Api\ShipmentForProductEstimation" />
</config>

webapi.xml

<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <route url="/V1/shipping/estimate/:sku" method="POST">
        <service class="Your\Module\Api\ShipmentForProductEstimationInterface" method="estimateByProductAndAddress"/>
        <resources>
            <resource ref="anonymous" />
        </resources>
    </route>
</routes>

If you are familiar with API endpoints, you will recognise that this can be called via /rest/default/V1/shipping/estimate/:sku, where :sku is the product SKU you would like to check the shipping methods for. The POST object and return JSON will be exactly the same as that of carts/mine/estimate-shipping-methods, so they are interchangeable.

If the rates displayed are different from when you add the item into the active cart, then that suggests the shipping rate fetches the item and/or address data from the session rather than the Magento\Quote\Model\Quote\Address\RateRequest object - if this is the case, then you may need to alter those shipping classes, or contact the developer to have this looked into.

Related Topic