Drupal Commerce - Different prices for different customer groups

What are we looking to do?

When you have an online store, by default a product has a set price which is what everyone pays. You may have discounts through voucher codes but occasionally you want to offer a different price to shoppers i.e. wholesalers.

So what we will be doing is creating a new role called 'Wholesaler', adding a new price field to our product, then we will set up a rule which will calculate the right price for the wholesaler.

The tutorial will be focused around a basic Drupal 7 install with the Commerce module (and the other relevant modules installed).

Tutorial

Create user role

Go to admin/people/permissions/roles and add a new role.

Whenever you have a wholesaler wishing to use the site, you will need to give the wholesaler role to that user.

Set up a new price field

Go to admin/commerce/products/types/product/fields and in the Add new field, type Wholesaler price.

For the field type select Price, then chose the Price with currency widget type and add the field.

On the settings form, it is up to you whether you want to make it required etc. But leave the Number of values set to 1.

Set up the pricing rule

Go to admin/commerce/config/product-pricing and click the Add a pricing rule link.

For the name, just enter Wholesaler price rule.

Under events, it will automatically set the event called Calculating the sell price of a product.

Skip past to the Conditions section and click the Add condition link.

From the drop-down, select Entity has field.

Under Entity, for the Data selector set it to commerce-line-item. From the drop-down this will be listed as commerce-line-item (Product line item).

For the field, select commerce_product. Click Save.

Add another condition, and select Entity has field again. For the Data selector, choose commerce-line-item:commerce-product. In the drop-down you will need to select commerce-line-item... (Product line item) (not the ...) which will bring up a few more options, from these select commerce-line-item:commerce-product (Product).

Then in the field box select field_wholesaler_price or whatever you called the field in step 2.

Lastly, had a new condition called and select User has role(s). From the data selector chose site:current-user. You can choose this by going to site:...(Site information) then choosing site: current-user (Logged in user).

Then in the Roles section click your wholesaler role.

Save the condition.

Finally, under Actions click Add action. In the drop down under Commerce Line Item, select Set the unit price to a specific amount.

The Data selector should already be set to commerce_line_item.

Under Amount, click the link which says Switch to data selection.

From the drop down select commerce-line-item:... (Product line item) then select commerce-line-item: commerce-product:... (Product).

Then select your price field which will be commerce-line-item: commerce-product: field-wholesaler-price:... (Wholesaler price). Then select commerce-line-item: commerce-product: field-wholesaler-price: amount (Amount).

Leave the Price component type set to Base price. Under Price rounding mode it is up to you how this is set.

Conclusion

If you now set up a dummy user, give them the role wholesaler and login as them and browse the site, all the prices will now be showing the wholesaler price by default. Log out and the prices will go back to retail prices.

You can set up multiple different pricing structures thanks to Drupals fields module which allows an infinite amount of price fields.

Potential problem

This may cause a few issues for people on how they want data to appear. When a wholesaler is logged in they will see the wholesaler price and not the retail price. If you want to show the retailer price as well you will have to alter your product display template file to show both prices.

Creating roles/fields/rules automatically through code

If you are importing data to your site or you have a lot of different price structures for different roles then you can import them.

Creating the role

Through code, this is very simple:

<?php
$role 
= new stdClass();
$role->name 'Role name';
user_role_save($role);
?>

That will create a new role called Role name. Very simple to do.

Creating price fields per role

Rather than adding this code to a custom function, I would recommend using hook_user_role_insert($role). This way if you create a role through the admin screens of the site all the heavy lifting has been done for you.

<?php
function MODULE_NAME_user_role_insert($role) {
  
// Create and add the new price field to the product
  
$field_name 'field_price_role_'$role->rid;
  if (!
field_info_field($field_name)) {
    
$field = array(
      
'field_name' => $field_name,
      
'type' => 'commerce_price',
      
'entity_types' => array('commerce_product'),
      
'translatable' => FALSE,
      
'cardinality' => 1,
    );
    
$field field_create_field($field);
    
$instance = array(
      
'field_name' => $field_name,
      
'entity_type' => 'commerce_product',
      
'label' => 'Price for 'str_replace("/"" "$role->name),
      
'bundle' => 'product',
      
'description' => '',
      
'default_value' => NULL,
      
'deleted' => FALSE,
      
'required' => FALSE,
      
'settings' => array(
        
'user_register_form' => FALSE,
      ),
      
'widget' => array(
        
'type' => 'commerce_price_full',
        
'module' => 'commerce_price',
        
'active' => 1,
        
'settings' => array(
          
'currency_code' => 'default',
        ),
      ),
      
'display' => array(
        
'default' => array(
          
'label' => 'above',
          
'type' => 'hidden',
          
'weight' => 1,
          
'settings' => array(),
        ),
        
'line_item' => array(
          
'label' => 'above',
          
'type' => 'hidden',
          
'weight' => 0,
          
'settings' => array(),
        ),
        
'node_teaser' => array(
          
'label' => 'above',
          
'type' => 'hidden',
          
'weight' => 0,
          
'settings' => array(),
        ),
      ),
    );
    
field_create_instance($instance);
  }
}
?>

Here we are creating a new field of the name field_price_role_XXX where XXX would be the role ID. This will always be unique so you won't have any problems with name clashes.

Once the field is created, we create an instance of it on the commerce_product entity type. If you have a more complex set up with different product types you may need to set up multiple instances.

The other thing you may want to handle, is the deletion of a role. Once the role is deleted the field is no longer required. So you can use hook_user_role_delete($role).

<?php
function MODULE_NAME_user_role_delete($role) {
  
$field_name 'field_price_role_'$role->rid;
  
field_delete_field($field_name);
}
?>

The code is very simple and having the role ID used in the field name makes it extremely easy to know which field belongs to which role.

The field will still exist in the database until cron has run.

Automatically setting up a pricing rule

Rules can be defined in code by creating a MODULE_NAME.rules_defaults.inc file within your module folder.

What we do is get all the roles from the database, remove the standard authenticated user as this will pick up the default price automatically. Then we will set up a rule per role.

<?php
function MODULE_NAME_default_rules_configuration() {
  
$configs = array();
  
$roles user_roles(TRUE);
  unset(
$roles[2]); // Get rid of default authenticated user
  
if (count($roles) > 0) {
    foreach (
$roles as $rid => $role) {
      
$rule '{ "rules_product_pricing_for_role_'$rid .'" : {
          "LABEL" : "Product pricing for '
$role .' role",
          "PLUGIN" : "reaction rule",
          "REQUIRES" : [ "rules", "commerce_line_item", "commerce_product_reference" ],
          "ON" : [ "commerce_product_calculate_sell_price" ],
          "IF" : [
            { "entity_has_field" : { "entity" : [ "commerce-line-item" ], "field" : "commerce_product" } },
            { "entity_has_field" : {
                "entity" : [ "commerce-line-item:commerce-product" ],
                "field" : "field_price_role_'
$rid .'"
              }
            },
            { "user_has_role" : {
                "account" : [ "site:current-user" ],
                "roles" : { "value" : { "'
$rid .'" : "'$rid .'" } }
              }
            }
          ],
          "DO" : [
            { "commerce_line_item_unit_price_amount" : {
                "commerce_line_item" : [ "commerce-line-item" ],
                "amount" : [ "commerce-line-item:commerce-product:field-price-role-'
$rid .':amount" ],
                "component_name" : "base_price",
                "round_mode" : "1"
              }
            }
          ]
        }
      }'
;
      
$configs['rules_pricing_per_role_'$rid] = rules_import($rule);
    }
  }
  return 
$configs;
}
?>

This produces the same rule as we did through the admin interface but will create a new rule per role.

Note that the code within $rule is the result of an export from the Rules UI module. So you can make your own rule within the Rules UI then export it and use that to create your rules dynamically.

You will need to run cron each time you add a new role for the rule to become active and take affect. Once cron has run it will appear in the Rules UI, at which point you can override any part of it.

Comments

First, thank you very much. It's exactly what I needed (I just needed to change slightly the field names to replace the roled id by the price list id in my system, makes stuff easier to check if prices are ok).
However, why can't you create the rules in the insert role hook instead of creating some "default" rules ?

I'm not sure if there is a function which allows you to create rules that way. If there is then that would certainly be another option although presumably the rule will be stored in the database.

That would be definitely much cleaner to create the rule at the same time as the role (and the field).
However if there is no way to insert a rules, then the default rules configuration seems to be the way.

Thanks anyway.

This works really well. However it seems to break to the price search facet (filter and sorting).
Any idea to solve this ?

No idea, I haven't implemented this on any sites with faceted search.

I kind of sort it out. I have to create one facet per price list, and change the block permissions to only display the one corresponding to the current role. (I have done that manually although it would be better to create those face programmatically). It works but I have to change the default kickstart commerce css to work for all blocks. The default css use the form id and having many block alters the form ids.
(the change is to change .search-api-ranges-block-slider-view-form by [id^="search-api-ranges-block-slider-view-form"]. (you have to do the same "trick" for all subelement selected by id.).

For the sorting, I cheated and always sort by the standard price. Generally the order of prices and discounted prices is the same (this is not the case for me as one customer as a special price on one product, but I'm sure he won't notice it ;-))

wow, I can follow the steps (before the php code part) as a newbie and non-programmer. That is pretty good docuementation.
1. Like to show both retail and wholesale price , for wholesaler. Any way to do that?
2. About the code part. I need a bit of hand holding - will be helpfulwith more detailed steps like copy to notepad, save as what file name, where to put the file into which folder site/all/???etc. Maybe it is not meant to be for non-programmer, then pls ignore my request. thks.

May be worth reading this to get a better idea of where the code needs to go - http://drupal.org/node/361112

But you are basically creating a new module. The first bit of code goes in the MODULE_NAME.info file, the second bit of code goes in the MODULE_NAME.module file then the last bit of code goes in the MODULE_NAME.rules_defaults.inc file.

i tried following your steps again. fyi i am trying kickstart 2.0
shop will see both trade and retail price
annoy. and authen. user only see retail price

instead of setup a new field called wholesale price
I use commerce_price as trade price , commerce_price is a standard field when i setup a product variation type.
use commerce_price_eur as retail price, another std field when enabling multicurrency
Is this approach wrong?

I made commerce_price hidden , is this wrong?

admin/people/permissions/roles and add a new role: shop

It did not work.
what did I do wrong?

The steps all work but they only take in to account a single currency although I can't see why multicurrency wouldn't work.

after setting up wholesale price, visible to all , in all display formats.
do I need to make this a hidden field (maybe here is where I do wrong), such that only shop login will see then?

You should only have the one price field (the original one) showing for the product. Once the rule(s) is/are set up and configured this should update depending on the role of the current user viewing the product.

James,
I need to have the price in Drupal 7 commerce set by user role, each product needs to be a specific price by role, calculations of price will not work.

Example:
(Role 1)
Product 1 cost $5.51 for quantities 0-10 customer 1 (Role 1)
Product 1 cost $4.44 for quantities 11-100 customer 1 (Role 1)
Product 1 cost $1.99 for quantities 101-1000 customer 1 (Role 1)

(Role 2)
Product 1 cost $41.00 for quantities 0-10 customer 2 (Role 2)
Product 1 cost $28.00 for quantities 11-100 customer 2 (Role 2)
Product 1 cost $3.09 for quantities 101-1000 customer 2 (Role 2)

Solution needs to be scalable for any number of products, customers and roles.
This needs to be manageable from the backend by non-programmer types.

Look at the Commerce Price Table module, does what you need.

Hi James, thanks for sharing your module.
Do you know how to add to your module a rule for calculatating a field from another content type; for example cost-coeefficient 2.85 (to calculate additional cost for sale-price) So the admin can use this custom field to calculate the price per role.

Not that I know of. If you can set it up within rules then you should be able to have a reference field to be able to access the entity with the cost-coefficient but it's not something that I have done or needed to do.