Projet

Général

Profil

Paste
Télécharger (17,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / commerce / modules / product_pricing / commerce_product_pricing.module @ 13755f8d

1
<?php
2

    
3
/**
4
 * @file
5
 * Enables Rules based product sell price calculation for dynamic product pricing.
6
 */
7

    
8
/**
9
 * Implements hook_hook_info().
10
 */
11
function commerce_product_pricing_hook_info() {
12
  $hooks = array(
13
    'commerce_product_valid_pre_calculation_rule' => array(
14
      'group' => 'commerce',
15
    ),
16
    'commerce_product_valid_pre_calculation_product' => array(
17
      'group' => 'commerce',
18
    ),
19
    'commerce_product_calculate_sell_price_line_item_alter' => array(
20
      'group' => 'commerce',
21
    ),
22
  );
23

    
24
  return $hooks;
25
}
26

    
27
/**
28
 * Implements hook_commerce_price_field_calculation_options().
29
 *
30
 * To accommodate dynamic sell price calculation on the display level, we depend
31
 * on display formatter settings to alert the module when to calculate a price.
32
 * However, by default all price fields are set to show the original price as
33
 * loaded with no option to change this. This module needs to add its own option
34
 * to the list so it can know which prices should be calculated on display.
35
 *
36
 * @see commerce_product_pricing_commerce_price_field_formatter_prepare_view()
37
 */
38
function commerce_product_pricing_commerce_price_field_calculation_options($field, $instance, $view_mode) {
39
  // If this is a single value purchase price field attached to a product...
40
  if (($instance['entity_type'] == 'commerce_product' || $field['entity_types'] == array('commerce_product')) &&
41
    $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
42
    return array('calculated_sell_price' => t('Display the calculated sell price for the current user.'));
43
  }
44
}
45

    
46
/**
47
 * Implements hook_commerce_price_field_formatter_prepare_view().
48
 *
49
 * It isn't until the point of display that we know whether a particular price
50
 * field should be altered to display the current user's purchase price of the
51
 * product. Therefore, instead of trying to calculate dynamic prices on load,
52
 * we calculate them prior to display but at the point where we know the full
53
 * context of the display, including the display formatter settings for the
54
 * pertinent view mode.
55
 *
56
 * The hook is invoked before a price field is formatted, so this implementation
57
 * lets us swap in the calculated sell price of a product for a given point of
58
 * display. The way it calculates the price is by creating a pseudo line item
59
 * for the current product that is passed to Rules for transformation. Rule
60
 * configurations may then use actions to set and alter the unit price of the
61
 * line item, which, being an object, is passed by reference through all the
62
 * various actions. Upon completion of the Rules execution, the unit price data
63
 * is then swapped in for the data of the current field for display.
64
 */
65
function commerce_product_pricing_commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
66
  // If this is a single value purchase price field attached to a product...
67
  if ($entity_type == 'commerce_product' && $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
68
    // Prepare the items for each entity passed in.
69
    foreach ($entities as $product_id => $product) {
70
      // If this price should be calculated...
71
      if (!empty($displays[$product_id]['settings']['calculation']) &&
72
        $displays[$product_id]['settings']['calculation'] == 'calculated_sell_price') {
73
        // If this price has already been calculated, reset it to its original
74
        // value so it can be recalculated afresh in the current context.
75
        if (isset($items[$product_id][0]['original'])) {
76
          $original = $items[$product_id][0]['original'];
77
          $items[$product_id] = array(0 => $original);
78

    
79
          // Reset the price field value on the product object used to perform
80
          // the calculation.
81
          foreach ($product->commerce_price as $langcode => $value) {
82
            $product->commerce_price[$langcode] = $items[$product_id];
83
          }
84
        }
85
        else {
86
          // Save the original value for use in subsequent calculations.
87
          $original = isset($items[$product_id][0]) ? $items[$product_id][0] : NULL;
88
        }
89

    
90
        // Replace the data being displayed with data from a calculated price.
91
        $items[$product_id] = array();
92
        $items[$product_id][0] = commerce_product_calculate_sell_price($product);
93
        $items[$product_id][0]['original'] = $original;
94
      }
95
    }
96
  }
97
}
98

    
99
/**
100
 * Returns the calculated sell price for the given product.
101
 *
102
 * @param $product
103
 *   The product whose sell price will be calculated.
104
 * @param $precalc
105
 *   Boolean indicating whether or not the pre-calculated sell price from the
106
 *     database should be requested before calculating it anew.
107
 *
108
 * @return
109
 *   A price field data array as returned by entity_metadata_wrapper().
110
 */
111
function commerce_product_calculate_sell_price($product, $precalc = FALSE) {
112
  // First create a pseudo product line item that we will pass to Rules.
113
  $line_item = commerce_product_line_item_new($product);
114

    
115
  // Allow modules to prepare this as necessary.
116
  drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
117

    
118
  // Attempt to fetch a database stored price if specified.
119
  if ($precalc) {
120
    $module_key = commerce_product_pre_calculation_key();
121

    
122
    $result = db_select('commerce_calculated_price')
123
      ->fields('commerce_calculated_price', array('amount', 'currency_code', 'data'))
124
      ->condition('module', 'commerce_product_pricing')
125
      ->condition('module_key', $module_key)
126
      ->condition('entity_type', 'commerce_product')
127
      ->condition('entity_id', $product->product_id)
128
      ->condition('field_name', 'commerce_price')
129
      ->execute()
130
      ->fetchObject();
131

    
132
    // If a pre-calculated price was found...
133
    if (!empty($result)) {
134
      // Wrap the line item, swap in the price, and return it.
135
      $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
136

    
137
      $wrapper->commerce_unit_price->amount = $result->amount;
138
      $wrapper->commerce_unit_price->currency_code = $result->currency_code;
139

    
140
      // Unserialize the saved prices data array and initialize to an empty
141
      // array if the column was empty.
142
      $result->data = unserialize($result->data);
143
      $wrapper->commerce_unit_price->data = !empty($result->data) ? $result->data : array();
144

    
145
      return $wrapper->commerce_unit_price->value();
146
    }
147
  }
148

    
149
  // Pass the line item to Rules.
150
  rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
151

    
152
  return entity_metadata_wrapper('commerce_line_item', $line_item)->commerce_unit_price->value();
153
}
154

    
155
/**
156
 * Generates a price pre-calculation module key indicating which pricing Rules
157
 *   currently apply.
158
 */
159
function commerce_product_pre_calculation_key() {
160
  // Load the sell price calculation event.
161
  $event = rules_get_cache('event_commerce_product_calculate_sell_price');
162

    
163
  // If there are no rule configurations, use an empty key.
164
  if (empty($event)) {
165
    return '';
166
  }
167

    
168
  // Build an array of the names of all rule configurations that qualify for
169
  // dynamic pre-calculation.
170
  $rule_names = array();
171

    
172
  $state = new RulesState();
173

    
174
  foreach ($event as $rule) {
175
    // Only add Rules with conditions that evaluate to TRUE.
176
    if (count($rule->conditions()) > 0 &&
177
      $rule->conditionContainer()->evaluate($state)) {
178
      $rule_names[] = $rule->name;
179
    }
180
  }
181

    
182
  // If no valid Rules were found, return an empty string.
183
  if (empty($rule_names)) {
184
    return '';
185
  }
186

    
187
  // Otherwise sort to ensure the names are in alphabetical order and return the
188
  // imploded module key.
189
  sort($rule_names);
190

    
191
  return implode('|', $rule_names);
192
}
193

    
194
/**
195
 * Returns an array of rule keys used for pre-calculating product sell prices.
196
 *
197
 * Rule keys represent every possible combination of rules that might alter any
198
 * given product sell price. If no valid rules exist, an empty array is returned.
199
 */
200
function commerce_product_pre_calculation_rule_keys() {
201
  static $rule_keys = NULL;
202

    
203
  if (is_null($rule_keys)) {
204
    // Load the sell price calculation event.
205
    $event = rules_get_cache('event_commerce_product_calculate_sell_price');
206

    
207
    // Build an array of the names of all rule configurations that qualify for
208
    // dynamic pre-calculation.
209
    $rule_names = array();
210

    
211
    foreach ($event as $rule) {
212
      if (commerce_product_valid_pre_calculation_rule($rule)) {
213
        $rule_names[] = $rule->name;
214
      }
215
    }
216

    
217
    // Sort to ensure the names are always in alphabetical order.
218
    sort($rule_names);
219

    
220
    // Using the array of names, generate an array that contains keys for every
221
    // possible combination of these Rules applying (i.e. conditions all passing).
222
    $rule_keys = array();
223

    
224
    // First find the maximum number of combinations as a power of two.
225
    $max = pow(2, count($rule_names));
226

    
227
    // Loop through each combination expressed as an integer.
228
    for ($i = 0; $i < $max; $i++) {
229
      // Convert the integer to a string binary representation, reverse it (so the
230
      // first bit is on the left instead of right), and split it into an array
231
      // with each bit as its own value.
232
      $bits = str_split(strrev(sprintf('%0' . count($rule_names) . 'b', $i)));
233

    
234
      // Create a key of underscore delimited Rule IDs by assuming a 1 means the
235
      // Rule ID in the $rule_ids array with the same key as the bit's position in
236
      // the string should be assumed to have applied.
237
      $key = implode('|', array_intersect_key($rule_names, array_intersect($bits, array('1'))));
238

    
239
      $rule_keys[] = $key;
240
    }
241
  }
242

    
243
  return $rule_keys;
244
}
245

    
246
/**
247
 * Pre-calculates sell prices for qualifying products based on valid rule
248
 *   configurations on the "Calculating product sell price" event.
249
 *
250
 * @param $from
251
 *   For calculating prices for a limited number of products at a time, specify
252
 *     the range's "from" amount.
253
 * @param $count
254
 *   For calculating prices for a limited number of products at a time, specify
255
 *     the range's "count" amount.
256
 *
257
 * @return
258
 *   The number of pre-calculated prices created.
259
 */
260
function commerce_product_pre_calculate_sell_prices($from = NULL, $count = 1) {
261
  $total = 0;
262

    
263
  // Load the sell price calculation event.
264
  $event = rules_get_cache('event_commerce_product_calculate_sell_price');
265

    
266
  // If there are no rule configurations, leave without further processing.
267
  if (empty($event)) {
268
    return array();
269
  }
270

    
271
  // If a range was specified, query just those products...
272
  if (!is_null($from)) {
273
    $query = db_query_range("SELECT product_id FROM {commerce_product}", $from, $count);
274
  }
275
  else {
276
    // Otherwise load all products.
277
    $query = db_query("SELECT product_id FROM {commerce_product}");
278
  }
279

    
280
  while ($product_id = $query->fetchField()) {
281
    $product = commerce_product_load($product_id);
282

    
283
    // If the product is valid for pre-calculation...
284
    if (commerce_product_valid_pre_calculation_product($product)) {
285
      // For each rule key (i.e. set of applicable rule configurations)...
286
      foreach (commerce_product_pre_calculation_rule_keys() as $key) {
287
        // Build a product line item and Rules state object.
288
        $line_item = commerce_product_line_item_new($product);
289

    
290
        $state = new RulesState();
291
        $vars = $event->parameterInfo(TRUE);
292
        $state->addVariable('commerce_line_item', $line_item, $vars['commerce_line_item']);
293

    
294
        // For each Rule signified by the current key...
295
        foreach (explode('|', $key) as $name) {
296
          // Load the Rule and "fire" it, evaluating its actions without doing
297
          // any condition evaluation.
298
          if ($rule = rules_config_load($name)) {
299
            $rule->fire($state);
300
          }
301
        }
302

    
303
        // Also fire any Rules that weren't included in the key because they
304
        // don't have any conditions.
305
        foreach ($event as $rule) {
306
         if (count($rule->conditions()) == 0) {
307
            $rule->fire($state);
308
          }
309
        }
310

    
311
        // Build the record of the pre-calculated price and write it.
312
        $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
313

    
314
        if (!is_null($wrapper->commerce_unit_price->value())) {
315
          $record = array(
316
            'module' => 'commerce_product_pricing',
317
            'module_key' => $key,
318
            'entity_type' => 'commerce_product',
319
            'entity_id' => $product_id,
320
            'field_name' => 'commerce_price',
321
            'language' => !empty($product->language) ? $product->language : LANGUAGE_NONE,
322
            'delta' => 0,
323
            'amount' => round($wrapper->commerce_unit_price->amount->value()),
324
            'currency_code' => $wrapper->commerce_unit_price->currency_code->value(),
325
            'data' => $wrapper->commerce_unit_price->data->value(),
326
            'created' => time(),
327
          );
328

    
329
          // Save the price and increment the total if successful.
330
          if (drupal_write_record('commerce_calculated_price', $record) == SAVED_NEW) {
331
            $total++;
332
          }
333
        }
334
      }
335
    }
336
  }
337

    
338
  return $total;
339
}
340

    
341
/**
342
 * Determines if a given rule configuration meets the requirements for price
343
 *   pre-calculation.
344
 *
345
 * @param $rule
346
 *   A rule configuration belonging to the commerce_product_calculate_sell_price
347
 *     event.
348
 * @param $limit_validation
349
 *   A boolean indicating whether or not the validation check should limit
350
 *     itself to the conditions in this function, effectively skipping the
351
 *     invocation of hook_commerce_product_valid_pre_calculation_rule().
352
 *
353
 * @return
354
 *   TRUE or FALSE indicating whether or not the rule configuration is valid.
355
 */
356
function commerce_product_valid_pre_calculation_rule($rule, $limit_validation = FALSE) {
357
  // If a rule configuration doesn't have any conditions, it doesn't need to
358
  // unique consideration in pre-calculation, as its actions will always apply.
359
  if (count($rule->conditions()) == 0) {
360
    return FALSE;
361
  }
362

    
363
  // Inspect each condition on the rule configuration. This likely needs to be
364
  // recursive for conditions in nested operator groups.
365
  foreach ($rule->conditions() as $condition) {
366
    // Look for line item usage in any selector in the condition settings.
367
    foreach ($condition->settings as $key => $value) {
368
      if (substr($key, -7) == ':select') {
369
        // If the selector references either line-item or line-item-unchanged,
370
        // the Rule is not valid for price pre-calculation.
371
        if (strpos($value, 'line-item') === 0) {
372
          return FALSE;
373
        }
374
      }
375
    }
376
  }
377

    
378
  // Allow other modules to invalidate this rule configuration.
379
  if (!$limit_validation) {
380
    if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_rule', $rule))) {
381
      return FALSE;
382
    }
383
  }
384

    
385
  return TRUE;
386
}
387

    
388
/**
389
 * Determines if a given product should be considered for price pre-calculation.
390
 *
391
 * @param $product
392
 *   The product being considered for sell price pre-calculation.
393
 *
394
 * @return
395
 *   TRUE or FALSE indicating whether or not the product is valid.
396
 */
397
function commerce_product_valid_pre_calculation_product($product) {
398
  // Allow other modules to invalidate this product.
399
  if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_product', $product))) {
400
    return FALSE;
401
  }
402

    
403
  return TRUE;
404
}
405

    
406
/**
407
 * Sets a batch operation to pre-calculate product sell prices.
408
 *
409
 * This function prepares and sets a batch to pre-calculate product sell prices
410
 * based on the number of variations for each price and the number of products
411
 * to calculate. It does not call batch_process(), so if you are not calling
412
 * this function from a form submit handler, you must process yourself.
413
 */
414
function commerce_product_batch_pre_calculate_sell_prices() {
415
  // Create the batch array.
416
  $batch = array(
417
    'title' => t('Pre-calculating product sell prices'),
418
    'operations' => array(),
419
    'finished' => 'commerce_product_batch_pre_calculate_sell_prices_finished',
420
  );
421

    
422
  // Add operations based on the number of rule combinations and number of
423
  // products to be pre-calculated.
424
  $rule_keys = commerce_product_pre_calculation_rule_keys();
425

    
426
  // Count the number of products.
427
  $product_count = db_select('commerce_product', 'cp')
428
    ->fields('cp', array('product_id'))
429
    ->countQuery()
430
    ->execute()
431
    ->fetchField();
432

    
433
  // Create a "step" value based on the number of rule keys, i.e. how many
434
  // products to calculate at a time. This will roughly limit the number of
435
  // queries to 500 on any given batch operations.
436
  if (count($rule_keys) > 500) {
437
    $step = 1;
438
  }
439
  else {
440
    $step = min(round(500 / count($rule_keys)) + 1, $product_count);
441
  }
442

    
443
  // Add batch operations to pre-calculate every price.
444
  for ($i = 0; $i < $product_count; $i += $step) {
445
    // Ensure the maximum step doesn't go over the total number of rows for
446
    // accurate reporting later.
447
    if ($i + $step > $product_count) {
448
      $step = $product_count - $i;
449
    }
450

    
451
    $batch['operations'][] = array('_commerce_product_batch_pre_calculate_sell_prices', array($i, $step));
452
  }
453

    
454
  batch_set($batch);
455
}
456

    
457
/**
458
 * Calculates pre-calculation using the given range values and updates the batch
459
 *   with processing information.
460
 */
461
function _commerce_product_batch_pre_calculate_sell_prices($from, $count, &$context) {
462
  // Keep track of the total number of products covered and the total number of
463
  // prices created in the results array.
464
  if (empty($context['results'])) {
465
    $context['results'] = array(0, 0);
466
  }
467

    
468
  // Increment the number of products processed.
469
  $context['results'][0] += $count;
470

    
471
  // Increment the number of actual prices created.
472
  $context['results'][1] += commerce_product_pre_calculate_sell_prices($from, $count);
473
}
474

    
475
/**
476
 * Displays a message upon completion of a batched sell price pre-calculation.
477
 */
478
function commerce_product_batch_pre_calculate_sell_prices_finished($success, $results, $operations) {
479
  if ($success) {
480
    $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]));
481
  }
482
  else {
483
    $message = t('Batch pre-calculation finished with an error.');
484
  }
485

    
486
  drupal_set_message($message);
487
}