22

enter image description here

How to add a custom button to sales order view in magento2, since some of the events was remove in-favor of plugins.

  • Removed some events (plugins must be used instead):
    • adminhtml_widget_container_html_before (use in magento 1.x)
    • admin_session_user_logout
    • model_config_data_save_before
    • ...

See Magento2 Change Log

1

6 Answers 6

30

The cleanest solution I've seen so far is to use a plugin targeting 'beforeSetLayout'

This can target the exact block, saving the check for the current request, and also avoids the plugin being on 'getOrderId' which in my case could not be used as I needed to call getOrderId in my plugin method.

So this in di.xml

   <type name="Magento\Sales\Block\Adminhtml\Order\View">
    <plugin name="addMyButton" type="My\Module\Plugin\Block\Adminhtml\Order\View"/>
   </type>

And then this in the file My\Module\Plugin\Block\Adminhtml\Order\View.php

public function beforeSetLayout(\Magento\Sales\Block\Adminhtml\Order\View $view)
{
    $message ='Are you sure you want to do this?';
    $url = $view->getUrl('route_id/path').$view->getOrderId();


    $view->addButton(
        'order_myaction',
        [
            'label' => __('My Action'),
            'class' => 'myclass',
            'onclick' => "confirmSetLocation('{$message}', '{$url}')"
        ]
    );


}
1
  • Worked like a charm Mar 5, 2019 at 11:45
17

After trying many different ways, this is the only solution I could find that seem to work without affecting other modules. I would love to see other solutions.

Option 1

Create a plugin in Company/Module/etc/adminhtml/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Backend\Block\Widget\Button\Toolbar">
        <plugin name="MagePal_TestBed::pluginBefore" type="MagePal\TestBed\Plugin\PluginBefore" />
    </type>
</config>

Then in Plugin/PluginBefore.php

namespace MagePal\TestBed\Plugin;

class PluginBefore
{
    public function beforePushButtons(
        \Magento\Backend\Block\Widget\Button\Toolbar\Interceptor $subject,
        \Magento\Framework\View\Element\AbstractBlock $context,
        \Magento\Backend\Block\Widget\Button\ButtonList $buttonList
    ) {

        $this->_request = $context->getRequest();
        if($this->_request->getFullActionName() == 'sales_order_view'){
              $buttonList->add(
                'mybutton',
                ['label' => __('My Button'), 'onclick' => 'setLocation(window.location.href)', 'class' => 'reset'],
                -1
            );
        }

    }
}

Option 2

Create a plugin in Company/Module/etc/adminhtml/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="\Magento\Sales\Block\Adminhtml\Order\View">
        <plugin name="MagePal_TestBed::pluginBeforeView" type="MagePal\TestBed\Plugin\PluginBeforeView" />
    </type>
</config>

Then in Plugin/PluginBeforeView.php

namespace MagePal\TestBed\Plugin;

class PluginBeforeView
{

    public function beforeGetOrderId(\Magento\Sales\Block\Adminhtml\Order\View $subject){
        $subject->addButton(
                'mybutton',
                ['label' => __('My Buttion'), 'onclick' => 'setLocation(window.location.href)', 'class' => 'reset'],
                -1
            );

        return null;
    }

}

See Full Source code

6
  • @r-s I've tried 2nd option, and it causes an error - Warning: call_user_func_array() expects parameter 2 to be array, object given in D:\new\OpenServer\domains\graffiticaps-m2.loc\vendor\magento\framework\Interception\Interceptor.php on line 144, since __callPlugin() method adds what beforeGetOrderId() method returns to arguments of getOrderId() method. \vendor\magento\framework\Interception\Interceptor.php [line 124] - $arguments = $beforeResult;. So I think there must be returned smth else, but not object, meaning $subject Mar 6, 2016 at 17:13
  • 1
    I just test on Magento 2.0.2... Take a look at my update for option #2 .... See github.com/magepal/stackexchange/tree/develop/91071 Mar 6, 2016 at 21:17
  • Is there any way to call ajax on click of this button?
    – nuwaus
    Oct 3, 2017 at 9:48
  • @nuwaus ... you could change the 'onclick' to 'onclick="processAjax()"" then add you ajax function there or some other on click jquery binding Oct 3, 2017 at 14:23
  • here is a similar issue. magento.stackexchange.com/questions/251458/…
    – Ajwad Syed
    Nov 27, 2018 at 13:09
10

Create DI file: app/code/YourVendor/YourModule/etc/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">
    <virtualType name="SalesOrderViewWidgetContext" type="\Magento\Backend\Block\Widget\Context">
        <arguments>
            <argument name="buttonList" xsi:type="object">YourVendor\YourModule\Block\Adminhtml\Order\View\ButtonList
            </argument>
        </arguments>
    </virtualType>
    <type name="Magento\Sales\Block\Adminhtml\Order\View">
        <arguments>
            <argument name="context" xsi:type="object">SalesOrderViewWidgetContext</argument>
        </arguments>
    </type>
</config>

What we do here is:

  1. Set custom context argument into the Order\View block. This context is defined as a virtual type.
  2. Define virtual type for a widget context. We set custom buttonList argument with our own button list class.

Implement your button list class:

<?php
namespace YourVendor\YourModule\Block\Adminhtml\Order\View;

class ButtonList extends \Magento\Backend\Block\Widget\Button\ButtonList
{
   public function __construct(\Magento\Backend\Block\Widget\Button\ItemFactory $itemFactory)
   {
       parent::__construct($itemFactory);
       $this->add('mybutton', [
           'label' => __('My button label')
       ]);
   }
}
3
  • 1
    Thank you for this solution ! I think this is the best and the most elegant.
    – eInyzant
    Apr 26, 2018 at 4:24
  • This looked nice, elegant and easy to understand but unfortunately it does not work. In Magento 2.3.4 when clicking in a order, it throws an error Exception occurred during order load Feb 17, 2020 at 9:40
  • Thank you working fine for Magento 2.4.3 Jun 13, 2023 at 9:30
10

This is one of the best solutions I have seen so far without using plugins. It is also used by the PayPal module.

MagePal/CustomButton/view/adminhtml/layout/sales_order_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="sales_order_edit">
            <block class="MagePal\CustomButton\Block\Adminhtml\Order\View\Buttons" name="custom_buttons" />
        </referenceBlock>
    </body>
</page>

MagePal/CustomButton/Block/Adminhtml/Order/View/Buttons.php

namespace MagePal\CustomButton\Block\Adminhtml\Order\View;
    
class Buttons extends \Magento\Sales\Block\Adminhtml\Order\View
{    

   protected function _construct()
    {
        parent::_construct();

        if(!$this->getOrderId()) {
            return $this;
        }

        $buttonUrl = $this->_urlBuilder->getUrl(
            'adminhtml/custombutton/new',
            ['order_id' => $this->getOrderId()]
        );

        $this->addButton(
            'create_custom_button',
            ['label' => __('Custom Button'), 'onclick' => 'setLocation(\'' . $buttonUrl . '\')']
        );
        
        return $this;
    }

}
2
  • There is mistake in adminhtml_sales_order_view.xml should be sales_order_view.xml
    – Zahirabbas
    Dec 27, 2019 at 10:20
  • 1
    There is no need in public function __construct Feb 28, 2020 at 11:44
2

Create di.xml following location

app/code/Learning/RewriteSales/etc/di.xml

Content should be

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Backend\Block\Widget\Context">
        <plugin name="add_custom_button_sales_veiw" type="Learning\RewriteSales\Plugin\Widget\Context" sortOrder="1"/>
    </type>
</config>

Create Context.php following loaction

app/code/Learning/RewriteSales/Plugin/Widget/Context.php

Content should be

namespace Learning\RewriteSales\Plugin\Widget;


class Context
{
    public function afterGetButtonList(
        \Magento\Backend\Block\Widget\Context $subject,
        $buttonList
    )
    {
        $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
        $request = $objectManager->get('Magento\Framework\App\Action\Context')->getRequest();
        if($request->getFullActionName() == 'sales_order_view'){
            $buttonList->add(
                'custom_button',
                [
                    'label' => __('Custom Button'),
                    'onclick' => 'setLocation(\'' . $this->getCustomUrl() . '\')',
                    'class' => 'ship'
                ]
            );
        }

        return $buttonList;
    }

    public function getCustomUrl()
    {
        $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
        $urlManager = $objectManager->get('Magento\Framework\Url');
        return $urlManager->getUrl('sales/*/custom');
    }
}

Clear Magento cache and run update command

php bin/magento setup:upgrade
8
  • Correct me if i'm wrong, but from all my testing so far preference type is the equivalent of rewrite in magento 1. Therefore only one module can take advantage of it Nov 22, 2015 at 16:26
  • yes. But you can't create plugin for protected function.
    – Sohel Rana
    Nov 22, 2015 at 16:37
  • Just update my answer using plugin
    – Sohel Rana
    Nov 22, 2015 at 18:41
  • 1
    Instead of loading the objectManager you could have done $subject->getRequest()->getFullActionName() Nov 22, 2015 at 19:08
  • add this before afterGetButtonList function....... protected $urlBuider; public function __construct(\Magento\Framework\UrlInterface $urlBuilder) { $this->urlBuilder = $urlBuilder; } Then in getCustomUrl() function add this line only..... return $this->urlBuilder->getUrl('modulename/controllername/methodname',array('parameter'=>parameter_value));
    – KA9
    Jul 31, 2017 at 12:11
0

I tried the below link its working perfect. Add a Custom Button to Admin Sales Order View

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.