How To

How to Add a Column to Tier Price in Magento 2 Admin?

Hello Magento Friends,

Tier pricing is a valuable feature in Magento 2 that allows store owners to set different pricing levels based on the quantity purchased, encouraging bulk purchases by offering discounts. However, sometimes, the default tier pricing grid might not meet all the requirements, and adding custom columns to this grid becomes necessary.

This blog will guide you through the process of adding a custom column to the tier price section in Magento 2 Admin.

Steps to Add a Column to Tier Price in Magento 2 Admin:

Step 1: Create a db_schema.xml file using the path given below.

{{magento_root}}\app\code\Vendor\Extension\etc\db_schema.xml

Then add the following code

<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
   <table name="catalog_product_entity_tier_price" resource="default" engine="innodb" comment="Tier Pricing Table">
       <column xsi:type="varchar" name="subscribesaveprice" nullable="true" length="255" comment="comment" />
   </table>
</schema>

Step 2: Create a di.xml in the path given below.

{{magento_root}}\app\code\Vendor\Extension\etc\adminhtml\di.xml  

Then add the code as given below

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
   <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool" type="Magento\Ui\DataProvider\Modifier\Pool">
       <arguments>
           <argument name="modifiers" xsi:type="array">
               <item name="customfield" xsi:type="array">
                   <item name="class" xsi:type="string">Vendor\Extension\Ui\DataProvider\Product\Form\Modifier\UpdateTierPricing</item>
                   <item name="sortOrder" xsi:type="number">200</item>
               </item>
           </argument>
       </arguments>
   </virtualType>
   <type name="Vendor\Extension\Ui\DataProvider\Product\Form\Modifier\UpdateTierPricing">
       <arguments>
           <argument name="scopeName" xsi:type="string">product_form.product_form</argument>
       </arguments>
   </type>
</config>

Step 3: Create a di.xml file in the path given below.

{{magento_root}}\app\code\Vendor\Extension\etc\di.xml

Now add the below-given code

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
   <preference for="Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler"
               type="Vendor\Extension\Model\Product\Attribute\Backend\TierPrice\UpdatePriceHandler" />
   <preference for="Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler"
               type="Vendor\Extension\Model\Product\Attribute\Backend\TierPrice\SavePriceHandler" />
   <preference for="Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice"
               type="Vendor\Extension\Model\ResourceModel\Product\Attribute\Backend\DataColumnUpdate" />
</config>

Step 4: Create an UpdateTierPricing.php file in the path given below.

{{magento_root}}\app\code\Vendor\Extension\Ui\DataProvider\Product\Form\Modifier\UpdateTierPricing.php

Now include the code snippet as given below

<?php

namespace Vendor\Extension\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Element\DataType\Price;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
 
class UpdateTierPricing extends AbstractModifier
{
   /**
    * @var ArrayManager
    * @since 101.0.0
    */   protected $arrayManager;

   /**
    * @var string
    * @since 101.0.0
    */   protected $scopeName;

   /**
    * @var array
    * @since 101.0.0
    */   protected $meta = [];

   /**
    * UpdateTierPricing constructor.
    * @param ArrayManager $arrayManager
    */   public function __construct(
       ArrayManager $arrayManager
   ) {
       $this->arrayManager = $arrayManager;
   }

   /**
    * @param array $data
    * @return array
    * @since 100.1.0
    */   public function modifyData(array $data)
   {
       // TODO: Implement modifyData() method.
       return $data;
   }

   /**
    * @param array $meta
    * @return array
    * @since 100.1.0
    */   public function modifyMeta(array $meta)
   {
       // TODO: Implement modifyMeta() method.
       $this->meta = $meta;

       $this->customizeTierPrice();

       return $this->meta;
   }

   /**
    * @return $this
    */   private function customizeTierPrice()
   {
       $tierPricePath = $this->arrayManager->findPath(
           ProductAttributeInterface::CODE_TIER_PRICE,
           $this->meta,
           null,
           'children'
       );

       if ($tierPricePath) {
           $this->meta = $this->arrayManager->merge(
               $tierPricePath,
               $this->meta,
               $this->getTierPriceStructure()
           );
       }

       return $this;
   }

 private function getTierPriceStructure(){
  return [
  'children' => [
  'record' => [
    'children' => [
    'subscribesaveprice' => [
     'arguments' => [
     'data' => [
     'config' => [
     'formElement' => Input::NAME,
     'componentType' => Field::NAME,
     'dataType' => Price::NAME,
     'label' => __('Custom Column'),
     'dataScope' => 'subscribesaveprice',
     'sortOrder' => '100'
     ],
     ],
     ],
    ],
  ],
  ],
  ],
  ];
 }
}

Step 5: Create a SaveHandler.php file in path given below.

{{magento_root}}\app\code\Vendor\Extension\Model\Product\Attribute\Backend\TierPrice\SavePriceHandler.php

Now add the code as follows

<?php

namespace Vendor\Extension\Model\Product\Attribute\Backend\TierPrice;

use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler;

class SavePriceHandler extends SaveHandler
{

   /**
    * Get additional tier price fields.
    *
    * @param array $objectArray
    * @return array
    */   public function getAdditionalFields(array $objectArray): array
   {
       $percentageValue = $this->getPercentage($objectArray);

       return [
           'value' => $percentageValue ? null : $objectArray['price'],
           'percentage_value' => $percentageValue ?: null,
           'subscribesaveprice' => $this->getSecondaryUnit($objectArray),
       ];
   }

   /**
    * @param array $priceRow
    * @return mixed|null
    */   public function getSecondaryUnit(array  $priceRow)
   {
       return isset($priceRow['subscribesaveprice']) && !empty($priceRow['subscribesaveprice'])
           ? $priceRow['subscribesaveprice']
           : null;
   }
}

Step 6: Create a UpdatePriceHandler.php file in the given below path.

{{magento_root}}\app\code\Vendor\Extension\Model\UpdatePriceHandler.php

Then add the following code

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */declare(strict_types=1);

namespace Vendor\Extension\Model\Product\Attribute\Backend\TierPrice;

use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler;
use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\AbstractHandler;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Locale\FormatInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice;

/**
 * Process tier price data for handled existing product.
 */class UpdatePriceHandler extends AbstractHandler
{
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */    private $storeManager;

    /**
     * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
     */    private $attributeRepository;

    /**
     * @var \Magento\Framework\EntityManager\MetadataPool
     */    private $metadataPoll;

    /**
     * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice
     */    private $tierPriceResource;

    /**
     * @var FormatInterface
     */    private $localeFormat;

    /**
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository
     * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
     * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
     * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource
     * @param FormatInterface|null $localeFormat
     */    public function __construct(
        StoreManagerInterface $storeManager,
        ProductAttributeRepositoryInterface $attributeRepository,
        GroupManagementInterface $groupManagement,
        MetadataPool $metadataPool,
        Tierprice $tierPriceResource,
        FormatInterface $localeFormat = null
    ) {
        parent::__construct($groupManagement);

        $this->storeManager = $storeManager;
        $this->attributeRepository = $attributeRepository;
        $this->metadataPoll = $metadataPool;
        $this->tierPriceResource = $tierPriceResource;
        $this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(FormatInterface::class);
    }

    /**
     * Perform action on relation/extension attribute.
     *
     * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity
     * @param array $arguments
     * @return \Magento\Catalog\Api\Data\ProductInterface|object
     * @throws \Magento\Framework\Exception\InputException
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */    public function execute($entity, $arguments = [])
    {
        $attribute = $this->attributeRepository->get('tier_price');
        $priceRows = $entity->getData($attribute->getName());
        if (null !== $priceRows) {
            if (!is_array($priceRows)) {
                throw new \Magento\Framework\Exception\InputException(
                    __('Tier prices data should be array, but actually other type is received')
                );
            }
            $websiteId = (int)$this->storeManager->getStore($entity->getStoreId())->getWebsiteId();
            $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0;
            $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
            $productId = (int)$entity->getData($identifierField);

            // prepare original data to compare
            $origPrices = $entity->getOrigData($attribute->getName());
            $old = $this->prepareOldTierPriceToCompare($origPrices);
            // prepare data for save
            $new = $this->prepareNewDataForSave($priceRows, $isGlobal);

            $delete = array_diff_key($old, $new);
            $insert = array_diff_key($new, $old);
            $update = array_intersect_key($new, $old);

            $isAttributeChanged = $this->deleteValues($productId, $delete);
            $isAttributeChanged |= $this->insertValues($productId, $insert);
            $isAttributeChanged |= $this->updateValues($update, $old);

            if ($isAttributeChanged) {
                $valueChangedKey = $attribute->getName() . '_changed';
                $entity->setData($valueChangedKey, 1);
            }
        }

        return $entity;
    }

    /**
     * Update existing tier prices for processed product
     *
     * @param array $valuesToUpdate
     * @param array $oldValues
     * @return bool
     */    public function updateValues(array $valuesToUpdate, array $oldValues): bool
 {

    $isChanged = false;
    foreach ($valuesToUpdate as $key => $value) {
     if ((!empty($value['value']) && (float)$oldValues[$key]['price'] !== (float)$value['value'])
      || $this->getPercentage($oldValues[$key]) !== $this->getPercentage($value)
      || $this->getSecondaryUnit($oldValues[$key]) !== $this->getSecondaryUnit($value)
     ) {
      $price = new \Magento\Framework\DataObject(
       [
        'value_id' => $oldValues[$key]['price_id'],
        'value' => $value['value'],
        'percentage_value' => $this->getPercentage($value),
        'subscribesaveprice' => $this->getSecondaryUnit($value),
       ]
      );
      $this->tierPriceResource->savePriceData($price);
      $isChanged = true;
     }
    }

    return $isChanged;
 }

 /**
 * Get additional tier price fields.
 *
 * @param array $objectArray
 * @return array
 */ public function getAdditionalFields(array $objectArray): array
 {
    $percentageValue = $this->getPercentage($objectArray);

    return [
     'value' => $percentageValue ? null : $objectArray['price'],
     'percentage_value' => $percentageValue ?: null,
     'subscribesaveprice' => $this->getSecondaryUnit($objectArray),
    ];
 }

 /**
 * @param array $priceRow
 * @return mixed|null
 */ public function getSecondaryUnit(array  $priceRow)
 {
    return isset($priceRow['subscribesaveprice']) && !empty($priceRow['subscribesaveprice'])
     ? $priceRow['subscribesaveprice']
     : null;
 }

    /**
     * Insert new tier prices for processed product
     *
     * @param int $productId
     * @param array $valuesToInsert
     * @return bool
     */    private function insertValues(int $productId, array $valuesToInsert): bool
    {
        $isChanged = false;
        $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
        foreach ($valuesToInsert as $data) {
            $price = new \Magento\Framework\DataObject($data);
            $price->setData(
                $identifierField,
                $productId
            );
            $this->tierPriceResource->savePriceData($price);
            $isChanged = true;
        }

        return $isChanged;
    }

    /**
     * Delete tier price values for processed product
     *
     * @param int $productId
     * @param array $valuesToDelete
     * @return bool
     */    private function deleteValues(int $productId, array $valuesToDelete): bool
    {
        $isChanged = false;
        foreach ($valuesToDelete as $data) {
            $this->tierPriceResource->deletePriceData($productId, null, $data['price_id']);
            $isChanged = true;
        }

        return $isChanged;
    }

    /**
     * Get generated price key based on price data
     *
     * @param array $priceData
     * @return string
     */    private function getPriceKey(array $priceData): string
    {
        $qty = $this->parseQty($priceData['price_qty']);
        $key = implode(
            '-',
            array_merge([$priceData['website_id'], $priceData['cust_group']], [$qty])
        );

        return $key;
    }

    /**
     * Check by id is website global
     *
     * @param int $websiteId
     * @return bool
     */    private function isWebsiteGlobal(int $websiteId): bool
    {
        return $websiteId === 0;
    }

    /**
     * Prepare old data to compare.
     *
     * @param array|null $origPrices
     * @return array
     */    private function prepareOldTierPriceToCompare(?array $origPrices): array
    {
        $old = [];
        if (is_array($origPrices)) {
            foreach ($origPrices as $data) {
                $key = $this->getPriceKey($data);
                $old[$key] = $data;
            }
        }

        return $old;
    }

    /**
     * Prepare new data for save.
     *
     * @param array $priceRows
     * @param bool $isGlobal
     * @return array
     * @throws \Magento\Framework\Exception\LocalizedException
     */    private function prepareNewDataForSave(array $priceRows, bool $isGlobal = true): array
    {
        $new = [];
        $priceRows = array_filter($priceRows);
        foreach ($priceRows as $data) {
            if (empty($data['delete'])
                && (!empty($data['price_qty'])
                    || isset($data['cust_group'])
                    || $isGlobal === $this->isWebsiteGlobal((int)$data['website_id']))
            ) {
                $key = $this->getPriceKey($data);
                $new[$key] = $this->prepareTierPrice($data);
            }
        }

        return $new;
    }
}

Step 7: Create a DataColumnUpdate.php file in path given below.

{{magento_root}}\app\code\Vendor\Extension\Model\ResourceModel\Product\Attribute\Backend\DataColumnUpdate.php

Now include the following code

<?php

namespace Vendor\Extension\Model\ResourceModel\Product\Attribute\Backend;

use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice;

class DataColumnUpdate extends Tierprice
{
   /**
    * @param array $columns
    * @return array
    */   protected function _loadPriceDataColumns($columns)
   {
       $columns = parent::_loadPriceDataColumns($columns);
       $columns['subscribesaveprice'] = 'subscribesaveprice';
       return $columns;
   }
}

Step 8: Create a catalog_product_prices.xml file in path given below.

{{magento_root}}\app\code\Vendor\Extension\view\base\layout\catalog_product_prices.xml

After that, add the below-mentioned code

<?xml version="1.0"?>
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
    <referenceBlock name="render.product.prices">
        <arguments>
            <argument name="default" xsi:type="array">
                <item name="prices" xsi:type="array">
                    <item name="tier_price" xsi:type="array">
                        <item name="render_template" xsi:type="string">Vendor_Extension::product/price/tier_price.phtml</item>
                    </item>
                </item>
            </argument>
        </arguments>
    </referenceBlock>
</layout>

Step 9: Create a tier_price.phtml file in path given below.

{{magento_root}}\app\code\Vendor\Extension\view\base\templates\product\price\tier_price.phtml

Now add the following code

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */?>

<?php
// phpcs:disable Magento2.Templates.ThisInTemplate
// phpcs:disable Generic.WhiteSpace.ScopeIndent

/** @var \Magento\Catalog\Pricing\Render\PriceBox $block *//** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer *//** @var \Magento\Framework\Locale\LocaleFormatter $localeFormatter*//** @var \Magento\Catalog\Pricing\Price\TierPrice $tierPriceModel */$tierPriceModel = $block->getPrice();
$tierPrices = $tierPriceModel->getTierPriceList();
$msrpShowOnGesture = $block->getPriceType('msrp_price')->isShowPriceOnGesture();
$product = $block->getSaleableItem();
?>
<?php if (count($tierPrices)): ?>
    <ul class="<?= $block->escapeHtmlAttr(($block->hasListClass() ? $block->getListClass(): 'prices-tier items')) ?>">
        <?php foreach ($tierPrices as $index => $price): ?>
            <li class="item">
                <?php
                $productId = $product->getId();
                $isSaleable = $product->isSaleable();
                $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20);
                if ($msrpShowOnGesture && $price['price']->getValue() < $product->getMsrp()):
                    $addToCartUrl = '';
                    if ($isSaleable) {
                        $addToCartUrl = $this->helper(\Magento\Checkout\Helper\Cart::class)
                            ->getAddUrl($product, ['qty' => $price['price_qty']]);
                    }
                    $tierPriceData = [
                        'addToCartUrl' => $addToCartUrl,
                        'name' => $product->getName(),
                        'realPrice' => $block->renderAmount(
                            $price['price'],
                            [
                                'price_id'          => $index,
                                'id_suffix'         => '-' . $index,
                                'include_container' => true
                            ]
                        ),
                        'msrpPrice' => $block->renderAmount(
                            $block->getPriceType('msrp_price')->getAmount(),
                            [
                                'price_id'          => $index,
                                'id_suffix'         => '-' . $index,
                                'include_container' => true
                            ]
                        ),
                    ];
                    if ($block->getCanDisplayQty($product)) {
                        $tierPriceData['qty'] = $price['price_qty'];
                    }
                    ?>
                    <?= $block->escapeHtml(__('Buy %1 for: ', $price['price_qty'])) ?>
                    <a href="#"
                       id="<?= $block->escapeHtmlAttr($popupId) ?>"
                       data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>">
                        <?= $block->escapeHtml(__('Click for price')) ?>
                    </a>
                    <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag(
                        'onclick',
                        'event.preventDefault()',
                        'a#' . $block->escapeHtmlAttr($popupId)
                    ) ?>
                <?php else:
                    $priceAmountBlock = $block->renderAmount(
                        $price['price'],
                        [
                            'price_id'          => $index,
                            'id_suffix'         => '-' . $index,
                            'include_container' => true,
                            'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_OPTION
                        ]
                    );
                    ?>
                    <?= /* @noEscape */ ($block->getShowDetailedPrice() !== false)
                    ? __(
                        'Buy %1 for %2 each and '.
                        '<strong class="benefit">save<span class="percent tier-%3">&nbsp;%4</span>%</strong> %5 ',
                        $localeFormatter->formatNumber($price['price_qty']),
                        $priceAmountBlock,
                        $index,
                        $localeFormatter->formatNumber(
                            $block->formatPercent($tierPriceModel->getSavePercent($price['price']))
                        ),
                        $localeFormatter->formatNumber($price['subscribesaveprice'])
                    )
                    : __('Buy %1 for %2 each', $price['price_qty'], $priceAmountBlock);
                    ?>
                <?php endif; ?>
            </li>
        <?php endforeach; ?>
    </ul>
    <?php if ($msrpShowOnGesture):?>
        <script type="text/x-magento-init">
            {
                ".product-info-main": {
                    "addToCart": {
                        "origin": "tier",
                        "addToCartButton": "#product_addtocart_form [type=submit]",
                        "inputQty": "#qty",
                        "attr": "[data-tier-price]",
                        "productForm": "#product_addtocart_form",
                        "productId": "<?= (int) $productId ?>",
                        "productIdInput": "input[type=hidden][name=product]",
                        "isSaleable": "<?= (bool) $isSaleable ?>"
                    }
                }
            }
        </script>
    <?php endif;?>
<?php endif; ?>

Output:

The custom column will be added to the admin grid for tier pricing.

Custom column content of tier price is displayed in Magento 2 frontend.

Conclusion:

By following these steps, you can easily add a custom column to the tier price grid in the Magento 2 Admin.

Related Tutorials – 

Click to rate this post!
[Total: 1 Average: 5]
Dhiren Vasoya

Dhiren Vasoya is a Director and Co-founder at MageComp, Passionate ?️ Certified Magento Developer?‍?. He has more than 9 years of experience in Magento Development and completed 850+ projects to solve the most important E-commerce challenges. He is fond❤️ of coding and if he is not busy developing then you can find him at the cricket ground, hitting boundaries.?

Recent Posts

Understanding Exchange Rates: How They Impact Your International Money Transfers

When you take a look at the bank info for international money transfers, it’s all…

36 mins ago

How to Create a Shopify Draft Order in Shopify Remix Using GraphQL?

Shopify's Draft Orders feature is an essential tool for merchants, allowing them to create orders…

1 day ago

How to Use CSS with Shopify Remix Vite?

CSS (Cascading Style Sheets) is essential in web development to create visually appealing and responsive…

2 days ago

Latest Hyvä Theme Trends to Elevate your Magento UI/UX

Your eCommerce website theme is highly important for your business's online success as it reflects…

2 days ago

Use Hyvä Products at their Full Potential

Hyvä represents more than just a theme; it is a comprehensive suite of extensions and…

3 days ago

Magento 2: Add Number of Products Displayed Per Page in Invoice PDF

Hello Magento mates, Invoices are critical documents in every eCommerce business, providing details of product…

4 days ago