How to Implement a Custom "Order Approval Workflow" for B2B Stores

How to Implement a Custom "Order Approval Workflow" for B2B Stores

Running a B2B store on Magento 2? You probably deal with complex purchasing processes where commandes need approval before processing. Unlike B2C, B2B transactions often involve mulconseille decision-makers—managers, procurement teams, or finance departments—who need to avis and approve purchases before they’re finalized.

Magento 2’s default setup doesn’t include a built-in commande approval flux de travail, but with some personnalisation, you can create a seamless approval process tailored to your entreprise needs. Dans ce guide, nous’ll walk through comment implement a custom commande approval flux de travail étape par étape.

Why You Need an Order Approval Workflow

Avant diving into the code, let’s understand why this fonctionnalité is crucial for B2B stores:

  • Prevents Unauthorized Purchases: Ensures only approved commandes proceed.
  • Multi-Level Approvals: Allows different roles (e.g., managers, finance) to avis commandes.
  • Budget Control: Restricts spending beyond predefined limits.
  • Audit Trail: Keeps track of who approved what and when.

Step 1: Setting Up the Database Structure

Premièrement, we need a way to store approval requests. We’ll create a new table de base de données to track commande approvals.


// File: app/code/YourVendor/OrderApproval/Setup/InstallSchema.php

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        $table = $installer->getConnection()->newTable(
            $installer->getTable('order_approval_requests')
        )->addColumn(
            'approval_id',
            Table::TYPE_INTEGER,
            null,
            ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
            'Approval ID'
        )->addColumn(
            'order_id',
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => false],
            'Order ID'
        )->addColumn(
            'status',
            Table::TYPE_TEXT,
            20,
            ['nullable' => false, 'default' => 'pending'],
            'Approval Status (pending/approved/rejected)'
        )->addColumn(
            'approver_id',
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => true],
            'Admin User ID who approved/rejected'
        )->addColumn(
            'comments',
            Table::TYPE_TEXT,
            '64k',
            ['nullable' => true],
            'Approver Comments'
        )->addColumn(
            'created_at',
            Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
            'Creation Time'
        )->addColumn(
            'updated_at',
            Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE],
            'Update Time'
        )->addForeignKey(
            $installer->getFkName(
                'order_approval_requests',
                'order_id',
                'sales_order',
                'entity_id'
            ),
            'order_id',
            $installer->getTable('sales_order'),
            'entity_id',
            Table::ACTION_CASCADE
        )->setComment(
            'Order Approval Requests'
        );

        $installer->getConnection()->createTable($table);
        $installer->endSetup();
    }
}

Step 2: Creating the Approval Logic

Ensuite, we’ll create an observateur that triggers when an commande is placed and creates an approval request.


// File: app/code/YourVendor/OrderApproval/Observer/OrderPlaceAfter.php

namespace YourVendor\OrderApproval\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use YourVendor\OrderApproval\Model\ApprovalFactory;

class OrderPlaceAfter implements ObserverInterface
{
    protected $approvalFactory;

    public function __construct(ApprovalFactory $approvalFactory)
    {
        $this->approvalFactory = $approvalFactory;
    }

    public function execute(Observer $observer)
    {
        $order = $observer->getEvent()->getOrder();
        
        // Skip approval for certain customer groups or small orders
        if ($order->getCustomerGroupId() == 1 || $order->getGrandTotal() < 1000) {
            return;
        }

        $approval = $this->approvalFactory->create();
        $approval->setOrderId($order->getId())
                ->setStatus('pending')
                ->save();

        // Send email notification to approvers
        $this->sendApprovalEmail($order);
    }

    protected function sendApprovalEmail($order)
    {
        // Email logic here
    }
}

Step 3: Building the Admin Approval Interface

Now we need a way for admins to avis and approve commandes. Nous allons add a new grid in the Magento admin.


// File: app/code/YourVendor/OrderApproval/Block/Adminhtml/Approval/Grid.php

namespace YourVendor\OrderApproval\Block\Adminhtml\Approval;

class Grid extends \Magento\Backend\Block\Widget\Grid\Extended
{
    protected $_approvalCollectionFactory;

    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Backend\Helper\Data $backendHelper,
        \YourVendor\OrderApproval\Model\ResourceModel\Approval\CollectionFactory $approvalCollectionFactory,
        array $data = []
    ) {
        $this->_approvalCollectionFactory = $approvalCollectionFactory;
        parent::__construct($context, $backendHelper, $data);
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setId('approvalGrid');
        $this->setDefaultSort('created_at');
        $this->setDefaultDir('DESC');
        $this->setSaveParametersInSession(true);
        $this->setUseAjax(true);
    }

    protected function _prepareCollection()
    {
        $collection = $this->_approvalCollectionFactory->create();
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('approval_id', [
            'header' => __('ID'),
            'index' => 'approval_id',
            'type' => 'number'
        ]);

        $this->addColumn('order_id', [
            'header' => __('Order #'),
            'index' => 'order_id',
            'renderer' => \YourVendor\OrderApproval\Block\Adminhtml\Approval\Renderer\Order::class
        ]);

        $this->addColumn('status', [
            'header' => __('Status'),
            'index' => 'status',
            'type' => 'options',
            'options' => [
                'pending' => __('Pending'),
                'approved' => __('Approved'),
                'rejected' => __('Rejected')
            ]
        ]);

        // Add more columns as needed

        return parent::_prepareColumns();
    }

    public function getGridUrl()
    {
        return $this->getUrl('*/*/grid', ['_current' => true]);
    }
}

Step 4: Implementing Approval Actions

Now let's create the contrôleur that handles approval/rejection actions:


// File: app/code/YourVendor/OrderApproval/Controller/Adminhtml/Approval/Approve.php

namespace YourVendor\OrderApproval\Controller\Adminhtml\Approval;

class Approve extends \Magento\Backend\App\Action
{
    protected $approvalFactory;
    protected $orderRepository;
    protected $messageManager;

    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \YourVendor\OrderApproval\Model\ApprovalFactory $approvalFactory,
        \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
    ) {
        parent::__construct($context);
        $this->approvalFactory = $approvalFactory;
        $this->orderRepository = $orderRepository;
        $this->messageManager = $context->getMessageManager();
    }

    public function execute()
    {
        $approvalId = $this->getRequest()->getParam('id');
        $approval = $this->approvalFactory->create()->load($approvalId);
        
        if (!$approval->getId()) {
            $this->messageManager->addErrorMessage(__('Approval request not found.'));
            return $this->_redirect('*/*/');
        }

        try {
            $approval->setStatus('approved')
                     ->setApproverId($this->_auth->getUser()->getId())
                     ->save();

            $order = $this->orderRepository->get($approval->getOrderId());
            $order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING)
                  ->setStatus('processing')
                  ->save();

            $this->messageManager->addSuccessMessage(__('Order has been approved.'));
        } catch (\Exception $e) {
            $this->messageManager->addErrorMessage($e->getMessage());
        }

        return $this->_redirect('*/*/');
    }
}

Step 5: Customizing the Customer Experience

Let's modify the frontend to show statut de commande and approval information to clients:


// File: app/code/YourVendor/OrderApproval/Block/Order/ApprovalStatus.php

namespace YourVendor\OrderApproval\Block\Order;

class ApprovalStatus extends \Magento\Framework\View\Element\Template
{
    protected $approvalFactory;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \YourVendor\OrderApproval\Model\ApprovalFactory $approvalFactory,
        array $data = []
    ) {
        $this->approvalFactory = $approvalFactory;
        parent::__construct($context, $data);
    }

    public function getApprovalStatus($orderId)
    {
        $approval = $this->approvalFactory->create()
            ->getCollection()
            ->addFieldToFilter('order_id', $orderId)
            ->getFirstItem();

        if ($approval->getId()) {
            return $approval->getStatus();
        }

        return 'approved'; // Default if no approval needed
    }
}

Step 6: Adding Email Notifications

Communication is clé in approval flux de travails. Let's set up e-mail notifications:


// File: app/code/YourVendor/OrderApproval/Model/Email/Sender.php

namespace YourVendor\OrderApproval\Model\Email;

class Sender
{
    protected $transportBuilder;
    protected $storeManager;
    protected $inlineTranslation;

    public function __construct(
        \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
    ) {
        $this->transportBuilder = $transportBuilder;
        $this->storeManager = $storeManager;
        $this->inlineTranslation = $inlineTranslation;
    }

    public function sendApprovalRequestEmail($order, $approvers)
    {
        $this->inlineTranslation->suspend();
        
        $storeId = $this->storeManager->getStore()->getId();
        $templateVars = [
            'order' => $order,
            'approve_url' => $this->getApprovalUrl($order->getId())
        ];

        foreach ($approvers as $approver) {
            $transport = $this->transportBuilder
                ->setTemplateIdentifier('order_approval_request')
                ->setTemplateOptions(['area' => 'frontend', 'store' => $storeId])
                ->setTemplateVars($templateVars)
                ->setFrom('general')
                ->addTo($approver->getEmail(), $approver->getName())
                ->getTransport();

            $transport->sendMessage();
        }

        $this->inlineTranslation->resume();
    }
}

Advanced Customizations

Une fois you have the basic flux de travail working, consider these enhancements:

  • Multi-Level Approvals: Create sequential approval étapes for different departments.
  • Approval Rules: Set rules basé sur commande amount, groupe de clients, or product category.
  • Partial Approvals: Allow approvers to modify quantities before approval.
  • Escalation Rules: Automatically escalate if approval takes too long.

Testing Your Workflow

Avant going live, thoroughly test:

  1. Place commandes that should and shouldn't require approval
  2. Test approval/rejection from admin
  3. Verify e-mail notifications
  4. Check statut de commande updates
  5. Test with mulconseille approvers if applicable

Conclusion

Implementing a custom commande approval flux de travail in Magento 2 requires several composants working together: database tracking, admin interfaces, frontend displays, and e-mail notifications. Tandis que this guide covers the fundamentals, you can extend it further to match your specific entreprise processes.

For entreprisees that prefer ready-made solutions, check out Magefine's Order Approval Extension which offers these fonctionnalités and more prêt à l'emploi.

Have you implemented an approval flux de travail in your Magento store? Share your experiences in the comments!