commerce_product_pricing.module in Commerce Core 7
Enables Rules based product sell price calculation for dynamic product pricing.
File
modules/product_pricing/commerce_product_pricing.moduleView source
<?php
/**
* @file
* Enables Rules based product sell price calculation for dynamic product pricing.
*/
/**
* Implements hook_hook_info().
*/
function commerce_product_pricing_hook_info() {
$hooks = array(
'commerce_product_valid_pre_calculation_rule' => array(
'group' => 'commerce',
),
'commerce_product_valid_pre_calculation_product' => array(
'group' => 'commerce',
),
'commerce_product_calculate_sell_price_line_item_alter' => array(
'group' => 'commerce',
),
'commerce_product_calculate_sell_price_line_item' => array(
'group' => 'commerce',
),
);
return $hooks;
}
/**
* Implements hook_commerce_price_field_calculation_options().
*
* To accommodate dynamic sell price calculation on the display level, we depend
* on display formatter settings to alert the module when to calculate a price.
* However, by default all price fields are set to show the original price as
* loaded with no option to change this. This module needs to add its own option
* to the list so it can know which prices should be calculated on display.
*
* @see commerce_product_pricing_commerce_price_field_formatter_prepare_view()
*/
function commerce_product_pricing_commerce_price_field_calculation_options($field, $instance, $view_mode) {
// If this is a single value purchase price field attached to a product...
if (($instance['entity_type'] == 'commerce_product' || $field['entity_types'] == array(
'commerce_product',
)) && $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
return array(
'calculated_sell_price' => t('Display the calculated sell price for the current user.'),
);
}
}
/**
* Implements hook_commerce_price_field_formatter_prepare_view().
*
* It isn't until the point of display that we know whether a particular price
* field should be altered to display the current user's purchase price of the
* product. Therefore, instead of trying to calculate dynamic prices on load,
* we calculate them prior to display but at the point where we know the full
* context of the display, including the display formatter settings for the
* pertinent view mode.
*
* The hook is invoked before a price field is formatted, so this implementation
* lets us swap in the calculated sell price of a product for a given point of
* display. The way it calculates the price is by creating a pseudo line item
* for the current product that is passed to Rules for transformation. Rule
* configurations may then use actions to set and alter the unit price of the
* line item, which, being an object, is passed by reference through all the
* various actions. Upon completion of the Rules execution, the unit price data
* is then swapped in for the data of the current field for display.
*/
function commerce_product_pricing_commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
// If this is a single value purchase price field attached to a product...
if ($entity_type == 'commerce_product' && $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
// Prepare the items for each entity passed in.
foreach ($entities as $product_id => $product) {
// If this price should be calculated...
if (!empty($displays[$product_id]['settings']['calculation']) && $displays[$product_id]['settings']['calculation'] == 'calculated_sell_price') {
// If this price has already been calculated, reset it to its original
// value so it can be recalculated afresh in the current context.
if (isset($items[$product_id][0]['original'])) {
$original = $items[$product_id][0]['original'];
$items[$product_id] = array(
0 => $original,
);
// Reset the price field value on the product object used to perform
// the calculation.
foreach ($product->commerce_price as $langcode => $value) {
$product->commerce_price[$langcode] = $items[$product_id];
}
}
else {
// Save the original value for use in subsequent calculations.
$original = isset($items[$product_id][0]) ? $items[$product_id][0] : NULL;
}
// Replace the data being displayed with data from a calculated price.
if (!empty($original)) {
$items[$product_id] = array();
$items[$product_id][0] = commerce_product_calculate_sell_price($product);
$items[$product_id][0]['original'] = $original;
}
}
}
}
}
/**
* Returns the calculated sell price for the given product.
*
* @param $product
* The product whose sell price will be calculated.
* @param $precalc
* Boolean indicating whether or not the pre-calculated sell price from the
* database should be requested before calculating it anew.
*
* @return
* A price field data array as returned by entity_metadata_wrapper().
*/
function commerce_product_calculate_sell_price($product, $precalc = FALSE) {
// First create a pseudo product line item that we will pass to Rules.
$line_item = commerce_product_line_item_new($product);
// Allow modules to prepare this as necessary.
drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
// Attempt to fetch a database stored price if specified.
if ($precalc) {
$module_key = commerce_product_pre_calculation_key();
$result = db_select('commerce_calculated_price')
->fields('commerce_calculated_price', array(
'amount',
'currency_code',
'data',
))
->condition('module', 'commerce_product_pricing')
->condition('module_key', $module_key)
->condition('entity_type', 'commerce_product')
->condition('entity_id', $product->product_id)
->condition('field_name', 'commerce_price')
->execute()
->fetchObject();
// If a pre-calculated price was found...
if (!empty($result)) {
// Wrap the line item, swap in the price, and return it.
$wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
$wrapper->commerce_unit_price->amount = $result->amount;
$wrapper->commerce_unit_price->currency_code = $result->currency_code;
// Unserialize the saved prices data array and initialize to an empty
// array if the column was empty.
$result->data = unserialize($result->data);
$wrapper->commerce_unit_price->data = !empty($result->data) ? $result->data : array();
return $wrapper->commerce_unit_price
->value();
}
}
// Invoke the callback specified via the "Product pricing process" option.
commerce_product_pricing_invoke($line_item);
return entity_metadata_wrapper('commerce_line_item', $line_item)->commerce_unit_price
->value();
}
/**
* Generates a price pre-calculation module key indicating which pricing Rules
* currently apply.
*/
function commerce_product_pre_calculation_key() {
// Load the sell price calculation event.
$event = rules_get_cache('event_commerce_product_calculate_sell_price');
// If there are no rule configurations, use an empty key.
if (empty($event)) {
return '';
}
// Build an array of the names of all rule configurations that qualify for
// dynamic pre-calculation.
$rule_names = array();
$state = new RulesState();
foreach ($event as $rule) {
// Only add Rules with conditions that evaluate to TRUE.
if (count($rule
->conditions()) > 0 && $rule
->conditionContainer()
->evaluate($state)) {
$rule_names[] = $rule->name;
}
}
// If no valid Rules were found, return an empty string.
if (empty($rule_names)) {
return '';
}
// Otherwise sort to ensure the names are in alphabetical order and return the
// imploded module key.
sort($rule_names);
return implode('|', $rule_names);
}
/**
* Returns an array of rule keys used for pre-calculating product sell prices.
*
* Rule keys represent every possible combination of rules that might alter any
* given product sell price. If no valid rules exist, an empty array is returned.
*/
function commerce_product_pre_calculation_rule_keys() {
static $rule_keys = NULL;
if (is_null($rule_keys)) {
// Load the sell price calculation event.
$event = rules_get_cache('event_commerce_product_calculate_sell_price');
// Build an array of the names of all rule configurations that qualify for
// dynamic pre-calculation.
$rule_names = array();
foreach ($event as $rule) {
if (commerce_product_valid_pre_calculation_rule($rule)) {
$rule_names[] = $rule->name;
}
}
// Sort to ensure the names are always in alphabetical order.
sort($rule_names);
// Using the array of names, generate an array that contains keys for every
// possible combination of these Rules applying (i.e. conditions all passing).
$rule_keys = array();
// First find the maximum number of combinations as a power of two.
$max = pow(2, count($rule_names));
// Loop through each combination expressed as an integer.
for ($i = 0; $i < $max; $i++) {
// Convert the integer to a string binary representation, reverse it (so the
// first bit is on the left instead of right), and split it into an array
// with each bit as its own value.
$bits = str_split(strrev(sprintf('%0' . count($rule_names) . 'b', $i)));
// Create a key of underscore delimited Rule IDs by assuming a 1 means the
// Rule ID in the $rule_ids array with the same key as the bit's position in
// the string should be assumed to have applied.
$key = implode('|', array_intersect_key($rule_names, array_intersect($bits, array(
'1',
))));
$rule_keys[] = $key;
}
}
return $rule_keys;
}
/**
* Pre-calculates sell prices for qualifying products based on valid rule
* configurations on the "Calculating product sell price" event.
*
* @param $from
* For calculating prices for a limited number of products at a time, specify
* the range's "from" amount.
* @param $count
* For calculating prices for a limited number of products at a time, specify
* the range's "count" amount.
*
* @return
* The number of pre-calculated prices created.
*/
function commerce_product_pre_calculate_sell_prices($from = NULL, $count = 1) {
$total = 0;
// Load the sell price calculation event.
$event = rules_get_cache('event_commerce_product_calculate_sell_price');
// If there are no rule configurations, leave without further processing.
if (empty($event)) {
return array();
}
// If a range was specified, query just those products...
if (!is_null($from)) {
$query = db_query_range("SELECT product_id FROM {commerce_product}", $from, $count);
}
else {
// Otherwise load all products.
$query = db_query("SELECT product_id FROM {commerce_product}");
}
while ($product_id = $query
->fetchField()) {
$product = commerce_product_load($product_id);
// If the product is valid for pre-calculation...
if (commerce_product_valid_pre_calculation_product($product)) {
// For each rule key (i.e. set of applicable rule configurations)...
foreach (commerce_product_pre_calculation_rule_keys() as $key) {
// Build a product line item and Rules state object.
$line_item = commerce_product_line_item_new($product);
$state = new RulesState();
$vars = $event
->parameterInfo(TRUE);
$state
->addVariable('commerce_line_item', $line_item, $vars['commerce_line_item']);
// For each Rule signified by the current key...
foreach (explode('|', $key) as $name) {
// Load the Rule and "fire" it, evaluating its actions without doing
// any condition evaluation.
if ($rule = rules_config_load($name)) {
$rule
->fire($state);
}
}
// Also fire any Rules that weren't included in the key because they
// don't have any conditions.
foreach ($event as $rule) {
if (count($rule
->conditions()) == 0) {
$rule
->fire($state);
}
}
// Build the record of the pre-calculated price and write it.
$wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (!is_null($wrapper->commerce_unit_price
->value())) {
$record = array(
'module' => 'commerce_product_pricing',
'module_key' => $key,
'entity_type' => 'commerce_product',
'entity_id' => $product_id,
'field_name' => 'commerce_price',
'language' => !empty($product->language) ? $product->language : LANGUAGE_NONE,
'delta' => 0,
'amount' => round($wrapper->commerce_unit_price->amount
->value()),
'currency_code' => $wrapper->commerce_unit_price->currency_code
->value(),
'data' => $wrapper->commerce_unit_price->data
->value(),
'created' => time(),
);
// Save the price and increment the total if successful.
if (drupal_write_record('commerce_calculated_price', $record) == SAVED_NEW) {
$total++;
}
}
}
}
}
return $total;
}
/**
* Determines if a given rule configuration meets the requirements for price
* pre-calculation.
*
* @param $rule
* A rule configuration belonging to the commerce_product_calculate_sell_price
* event.
* @param $limit_validation
* A boolean indicating whether or not the validation check should limit
* itself to the conditions in this function, effectively skipping the
* invocation of hook_commerce_product_valid_pre_calculation_rule().
*
* @return
* TRUE or FALSE indicating whether or not the rule configuration is valid.
*/
function commerce_product_valid_pre_calculation_rule($rule, $limit_validation = FALSE) {
// If a rule configuration doesn't have any conditions, it doesn't need to
// unique consideration in pre-calculation, as its actions will always apply.
if (count($rule
->conditions()) == 0) {
return FALSE;
}
// Inspect each condition on the rule configuration. This likely needs to be
// recursive for conditions in nested operator groups.
foreach ($rule
->conditions() as $condition) {
// Look for line item usage in any selector in the condition settings.
foreach ($condition->settings as $key => $value) {
if (substr($key, -7) == ':select') {
// If the selector references either line-item or line-item-unchanged,
// the Rule is not valid for price pre-calculation.
if (strpos($value, 'line-item') === 0) {
return FALSE;
}
}
}
}
// Allow other modules to invalidate this rule configuration.
if (!$limit_validation) {
if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_rule', $rule))) {
return FALSE;
}
}
return TRUE;
}
/**
* Determines if a given product should be considered for price pre-calculation.
*
* @param $product
* The product being considered for sell price pre-calculation.
*
* @return
* TRUE or FALSE indicating whether or not the product is valid.
*/
function commerce_product_valid_pre_calculation_product($product) {
// Allow other modules to invalidate this product.
if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_product', $product))) {
return FALSE;
}
return TRUE;
}
/**
* Sets a batch operation to pre-calculate product sell prices.
*
* This function prepares and sets a batch to pre-calculate product sell prices
* based on the number of variations for each price and the number of products
* to calculate. It does not call batch_process(), so if you are not calling
* this function from a form submit handler, you must process yourself.
*/
function commerce_product_batch_pre_calculate_sell_prices() {
// Create the batch array.
$batch = array(
'title' => t('Pre-calculating product sell prices'),
'operations' => array(),
'finished' => 'commerce_product_batch_pre_calculate_sell_prices_finished',
);
// Add operations based on the number of rule combinations and number of
// products to be pre-calculated.
$rule_keys = commerce_product_pre_calculation_rule_keys();
// Count the number of products.
$product_count = db_select('commerce_product', 'cp')
->fields('cp', array(
'product_id',
))
->countQuery()
->execute()
->fetchField();
// Create a "step" value based on the number of rule keys, i.e. how many
// products to calculate at a time. This will roughly limit the number of
// queries to 500 on any given batch operations.
if (count($rule_keys) > 500) {
$step = 1;
}
else {
$step = min(round(500 / count($rule_keys)) + 1, $product_count);
}
// Add batch operations to pre-calculate every price.
for ($i = 0; $i < $product_count; $i += $step) {
// Ensure the maximum step doesn't go over the total number of rows for
// accurate reporting later.
if ($i + $step > $product_count) {
$step = $product_count - $i;
}
$batch['operations'][] = array(
'_commerce_product_batch_pre_calculate_sell_prices',
array(
$i,
$step,
),
);
}
batch_set($batch);
}
/**
* Calculates pre-calculation using the given range values and updates the batch
* with processing information.
*/
function _commerce_product_batch_pre_calculate_sell_prices($from, $count, &$context) {
// Keep track of the total number of products covered and the total number of
// prices created in the results array.
if (empty($context['results'])) {
$context['results'] = array(
0,
0,
);
}
// Increment the number of products processed.
$context['results'][0] += $count;
// Increment the number of actual prices created.
$context['results'][1] += commerce_product_pre_calculate_sell_prices($from, $count);
}
/**
* Displays a message upon completion of a batched sell price pre-calculation.
*/
function commerce_product_batch_pre_calculate_sell_prices_finished($success, $results, $operations) {
if ($success) {
$message = format_plural($results[0], "Sell prices pre-calculated for 1 product resulting in @total prices created.", "Sell prices pre-calculated for @count products resulting in @total prices created.", array(
'@total' => $results[1],
));
}
else {
$message = t('Batch pre-calculation finished with an error.');
}
drupal_set_message($message);
}
/**
* Invokes the sell price calculation callback configured via the
* "Price calculation process" option.
*
* @param $line_item
* The product line item used for sell price calculation.
*/
function commerce_product_pricing_invoke($line_item) {
$callback = variable_get('commerce_product_pricing_callback', 'rules_invoke_event');
// Defaults to rules_invoke_event if the specified callback doesn't exist.
if (!function_exists($callback)) {
$callback = 'rules_invoke_event';
}
$callback('commerce_product_calculate_sell_price', $line_item);
}
Functions
Name | Description |
---|---|
commerce_product_batch_pre_calculate_sell_prices | Sets a batch operation to pre-calculate product sell prices. |
commerce_product_batch_pre_calculate_sell_prices_finished | Displays a message upon completion of a batched sell price pre-calculation. |
commerce_product_calculate_sell_price | Returns the calculated sell price for the given product. |
commerce_product_pre_calculate_sell_prices | Pre-calculates sell prices for qualifying products based on valid rule configurations on the "Calculating product sell price" event. |
commerce_product_pre_calculation_key | Generates a price pre-calculation module key indicating which pricing Rules currently apply. |
commerce_product_pre_calculation_rule_keys | Returns an array of rule keys used for pre-calculating product sell prices. |
commerce_product_pricing_commerce_price_field_calculation_options | Implements hook_commerce_price_field_calculation_options(). |
commerce_product_pricing_commerce_price_field_formatter_prepare_view | Implements hook_commerce_price_field_formatter_prepare_view(). |
commerce_product_pricing_hook_info | Implements hook_hook_info(). |
commerce_product_pricing_invoke | Invokes the sell price calculation callback configured via the "Price calculation process" option. |
commerce_product_valid_pre_calculation_product | Determines if a given product should be considered for price pre-calculation. |
commerce_product_valid_pre_calculation_rule | Determines if a given rule configuration meets the requirements for price pre-calculation. |
_commerce_product_batch_pre_calculate_sell_prices | Calculates pre-calculation using the given range values and updates the batch with processing information. |