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

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 admin

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

custom column 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 – 

Previous Article

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

Next Article

Understanding Exchange Rates: How They Impact Your International Money Transfers

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Get Connect With Us

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨