Magento Customer Group Price Disabled for Store Scope

If you have ever googled “magento advanced pricing customer group price website field disabled” or “magento customer group price disabled for store scope,” this blog post is for you. The lack of results inspired me to look under the hood and, therefore, write about my findings.

The Story Behind Googling

Why was I even googling this? On one of our projects (two websites, each with one store view), I was setting up customer group prices for a small subset of products. Customer group pricing pretty much works like tier pricing; the only difference is that customer group prices have a quantity of 1. 

During the price setup, I didn’t pay much attention to scope since the customer group price form has <select> elements for the website. Advanced pricing modal (where the customer group price is set) looked something like this:

Everything looked and worked fine from both the admin and the front end. I informed the client that I had set the customer group prices and they were ready for testing on staging, along with other agreed-upon improvements. The client responded with a screenshot similar to this, with the message that adding customer group prices is disabled for the website’s scope.

Checking with Default Magento Installation

At first, I’ve thought this was a bug since it worked on default Magento with sample data. I’ve checked our modules and 3rd party extensions that have something to do with rendering of elements on  the admin product edit page. At that moment, I was unaware of the main prerequisite for this scenario:

catalog/price/scope config being set to website

Keep in mind that default Magento installation has this config set to global.

Setting Scope on tier_price Attribute

Few debugging hours later, here’s what I came up with while inspecting admin product edit form loading. This is the story of tier_price product attribute and (non) global scope which looks something like this:

(...) 
MagentoCatalogModelProductAttributeBackendPrice->setAttribute() 
MagentoCatalogModelProductAttributeBackendPrice->setScope() 

public function setScope($attribute) // $attribute is tier_price
{
    // checks catalog/price/scope config, in our case scope is 'website'
    if ($this->_helper->isPriceGlobal()) {
        $attribute->setIsGlobal(ScopedAttributeInterface::SCOPE_GLOBAL);
    } else {
        // attribute's scope is set to 'website'
        $attribute->setIsGlobal(ScopedAttributeInterface::SCOPE_WEBSITE);
    }
    return $this;
}
(...)

Later, when product attributes are prepared for form rendering in

MagentoCatalogUiDataProviderProductFormModifierEav

the tier_price attribute is (among the others) checked for scope, is it global or not.

Detective Mode Enabled: In Search for ‘disabled’ Property Culprit

If the catalog/price/scope is set to global (and therefore the tier_price attribute scope), part of the code where the disabled property is set is never executed. That’s why the “Add” button from the Customer Group Price form wasn’t disabled when I was checking the reported problem on the default installation. This is the part of product form configuration responsible for customer group price container:

MagentoCatalogUiDataProviderProductFormProductDataProvider->getMeta() // returns $meta array with product form configuration 
// part of $meta array which is the reason this debugging looks like this 
// (printed out in JSON format for easier readability)
{
  "arguments": {
	"data": {
  	"config": {
    	"formElement": "container",
    	"componentType": "container",
    	"breakLine": false,
    	"label": "Tier Price",
    	"required": "0",
    	"sortOrder": 40
  	}
	}
  },
  "children": {
	"tier_price": {
  	"arguments": {
    	"data": {
      	"config": {
        	"dataType": "text",
        	"formElement": "input",
        	"visible": "1",
        	"required": "0",
        	"notice": null,
        	"default": null,
        	"label": "Tier Price",
        	"code": "tier_price",
        	"source": "advanced-pricing",
        	"scopeLabel": "[WEBSITE]",
        	"globalScope": false,
        	"sortOrder": 40,
        	"service": {
          	"template": "ui/form/element/helper/service"
        	},
        	"componentType": "field",
        	"disabled": true -> causes “I’m disabled” meme
      	}
    	}
  	}
	}
  }
}

Who and where says that tier_price element is disabled? Let’s take a look at stack trace and responsible methods (pay attention to comments!):

(...) 
// group of interest is advanced-pricing {MagentoEavModelEntityAttributeGroup} 
MagentoCatalogUiDataProviderProductFormModifierEav->modifyMeta() 

// here we're "catching" tier_price attribute 
MagentoCatalogUiDataProviderProductFormModifierEav->getAttributesMeta() 

MagentoCatalogUiDataProviderProductFormModifierEav->addContainerChildren() 

// attribute_code = tier_price, $groupCode = advanced-pricing, $sortOrder = 4 
MagentoCatalogUiDataProviderProductFormModifierEav->getContainerChildren(ProductAttributeInterface $attribute, $groupCode, $sortOrder) 

MagentoCatalogUiDataProviderProductFormModifierEav->setupAttributeMeta() 
MagentoCatalogUiDataProviderProductFormModifierEav->addUseDefaultValueCheckbox() 

private function addUseDefaultValueCheckbox(ProductAttributeInterface $attribute, array $meta)
{
   /**
    * $canDisplayService = false if catalog/price/scope is global
    * so 'disabled' property is never set in this case
    */
    $canDisplayService = $this->canDisplayUseDefault($attribute);

    if ($canDisplayService) {
        $meta['arguments']['data']['config']['service'] = [
            'template' => 'ui/form/element/helper/service',
        ];

        /** 
          * This evaluates to !false = true so 'disabled' property for tier_price element in 
          * Customer Group Price form is set to true.  
          * Answer to “Who and where says that tier_price element is disabled?” It's HERE! 
          */
        $meta['arguments']['data']['config']['disabled'] = 
            !$this->scopeOverriddenValue->containsValue(
                MagentoCatalogApiDataProductInterface::class,
                $this->locator->getProduct(),
                $attribute->getAttributeCode(),
                $this->locator->getStore()->getId()
            );
        }

    return $meta;
}
-----------------------------------------------------------------------------------------------------------
private function canDisplayUseDefault(ProductAttributeInterface $attribute)
{
    $attributeCode = $attribute->getAttributeCode();

    /** @var Product $product */
    $product = $this->locator->getProduct();
    if ($product->isLockedAttribute($attributeCode)) {
        return false;
    }

    if (isset($this->canDisplayUseDefault[$attributeCode])) {
        return $this->canDisplayUseDefault[$attributeCode];
    }

    // $attribute is tier_price, and it's scope depends on catalog/price/scope
    return $this->canDisplayUseDefault[$attributeCode] = (
        // scope 'website' != scope 'global'
        ($attribute->getScope() != ProductAttributeInterface::SCOPE_GLOBAL_TEXT)  
        && $product
        && $product->getId()
        && $product->getStoreId()
    );
}

Workaround for Enabling the Add Button

However, since the client explicitly requested to be able to edit Customer Group Price while on store view scope, I’ve prepared a workaround in the form of a plugin. Create a new module (mine is Inchoo_CustomerGroupPrice) and add below code to your etc/di.xml file.

<?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="MagentoCatalogUiDataProviderProductFormModifierAdvancedPricing">
        <plugin name="advanced_pricing_customer_group_price_plugin"
                type="InchooCustomerGroupPricePluginCatalogUiDataProviderProductFormModifierAdvancedPricingPlugin"/>
    </type>
</config>

AdvancedPricingPlugin checks $meta array that contains necessary information about tier_price form element and forces enabling of that element. Here’s the implementation:

<?php

declare(strict_types=1);

namespace InchooCustomerGroupPricePluginCatalogUiDataProviderProductFormModifier;

use MagentoCatalogUiDataProviderProductFormModifierAdvancedPricing;
use MagentoFrameworkStdlibArrayManager;

class AdvancedPricingPlugin
{
    /**
     * @param ArrayManager $arrayManager
     */
    public function __construct(protected ArrayManager $arrayManager)
    {
    }

    /**
     * @param AdvancedPricing $subject
     * @param array $meta
     * @return array
     */
    public function afterModifyMeta(AdvancedPricing $subject, array $meta): array
    {
        $path = 'advanced_pricing_modal/children/advanced-pricing/children/tier_price/arguments/data/config/disabled';

        if ($this->arrayManager->get($path, $meta) === true) {
            $meta = $this->arrayManager->set($path, $meta, false);
        }

        return $meta;
    }
}

Remember to flush the cache before testing the plugin! The plugin’s execution should result in the customer group price being enabled for editing while being on store scope in the Magento admin dashboard.

A Feature or a Bug?

Enabling customer group price for website scope is a specific problem, i.e. subject (it’s a feature, not a bug! ). I hope that it will help someone, at least at some point, not to lose time for something that is simply related to Magento configuration and works as it should. It’s much quicker when you know where to look at.