Projet

Général

Profil

Paste
Télécharger (37,2 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / commerce / modules / price / commerce_price.module @ dbb0c974

1
<?php
2

    
3
/**
4
 * @file
5
 * Defines the Price field with widgets and formatters used to add prices with
6
 * currency codes to various Commerce entities.
7
 */
8

    
9

    
10
/**
11
 * Implements hook_hook_info().
12
 */
13
function commerce_price_hook_info() {
14
  $hooks = array(
15
    'commerce_price_field_calculation_options' => array(
16
      'group' => 'commerce',
17
    ),
18
    'commerce_price_component_type_info' => array(
19
      'group' => 'commerce',
20
    ),
21
    'commerce_price_component_type_info_alter' => array(
22
      'group' => 'commerce',
23
    ),
24
    'commerce_price_field_formatter_prepare_view' => array(
25
      'group' => 'commerce',
26
    ),
27
    'commerce_price_formatted_components_alter' => array(
28
      'group' => 'commerce',
29
    ),
30
  );
31

    
32
  return $hooks;
33
}
34

    
35
/**
36
 * Implements hook_theme().
37
 */
38
function commerce_price_theme() {
39
  return array(
40
    'commerce_price_formatted_components' => array(
41
      'variables' => array('components' => array(), 'price' => array()),
42
    ),
43
  );
44
}
45

    
46
/**
47
 * Implements hook_field_info().
48
 */
49
function commerce_price_field_info() {
50
  return array(
51
    'commerce_price' => array(
52
      'label' => t('Price'),
53
      'description' => t('This field stores prices for products consisting of an amount and a currency.'),
54
      'settings' => array(),
55
      'instance_settings' => array(),
56
      'default_widget' => 'commerce_price_simple',
57
      'default_formatter' => 'commerce_price_formatted_amount',
58
      'property_type' => 'commerce_price',
59
      'property_callbacks' => array('commerce_price_property_info_callback'),
60
      'default_token_formatter' => 'commerce_price_formatted_amount'
61
    ),
62
  );
63
}
64

    
65
/**
66
 * Implements hook_field_validate().
67
 */
68
function commerce_price_field_validate($entity_type, $entity, $field, $instance, $langcode, &$items, &$errors) {
69
  $translated_instance = commerce_i18n_object('field_instance', $instance);
70

    
71
  // Ensure only numeric values are entered in price fields.
72
  foreach ($items as $delta => &$item) {
73
    if (!empty($item['amount']) && !is_numeric($item['amount'])) {
74
      $errors[$field['field_name']][$langcode][$delta][] = array(
75
        'error' => 'price_numeric',
76
        'message' => t('%name: you must enter a numeric value for the price.', array('%name' => $translated_instance['label'])),
77
      );
78
    }
79
  }
80
}
81

    
82
/**
83
 * Implements hook_field_load().
84
 */
85
function commerce_price_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
86
  // Convert amounts to their floating point values and deserialize data arrays.
87
  foreach ($entities as $id => $entity) {
88
    foreach ($items[$id] as $delta => $item) {
89
      // Unserialize the data array if necessary.
90
      if (!empty($items[$id][$delta]['data']) && !is_array($items[$id][$delta]['data'])) {
91
        $items[$id][$delta]['data'] = unserialize($items[$id][$delta]['data']);
92
      }
93
      else {
94
        $items[$id][$delta]['data'] = array('components' => array());
95
      }
96
    }
97
  }
98
}
99

    
100
/**
101
 * Returns an array of commerce_price field names from a specific entity.
102
 *
103
 * @param $entity_type
104
 *   The entity type variable passed through hook_field_storage_pre_*() or
105
 *   hook_field_attach_*().
106
 * @param $entity
107
 *   The entity variable passed through hook_field_storage_pre_*() or
108
 *   hook_field_attach_*().
109
 *
110
 * @return array
111
 *   An array of commerce_price field names or an empty array if none are found.
112
 */
113
function _commerce_price_get_price_fields($entity_type, $entity) {
114
  $commerce_price_fields = array();
115

    
116
  // Determine the list of instances to iterate on.
117
  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
118
  $instances = field_info_instances($entity_type, $bundle);
119

    
120
  // Iterate through the instances and collect results.
121
  foreach ($instances as $instance) {
122
    $field_name = $instance['field_name'];
123
    $field = field_info_field($field_name);
124

    
125
    // If the instance is a price field with data...
126
    if ($field['type'] == 'commerce_price' && isset($entity->{$field_name})) {
127
      $commerce_price_fields[] = $field_name;
128
    }
129
  }
130

    
131
  return $commerce_price_fields;
132
}
133

    
134
/**
135
 * Converts price field data to a serialized array.
136
 *
137
 * @param $entity_type
138
 *   The entity type variable passed through hook_field_storage_pre_*().
139
 * @param $entity
140
 *   The entity variable passed through hook_field_storage_pre_*().
141
 */
142
function _commerce_price_field_serialize_data($entity_type, $entity) {
143
  // Loop over all the price fields attached to this entity.
144
  foreach (_commerce_price_get_price_fields($entity_type, $entity) as $field_name) {
145
    // Iterate over the items arrays for each language.
146
    foreach (array_keys($entity->{$field_name}) as $langcode) {
147
      $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
148

    
149
      // Serialize data arrays before saving.
150
      foreach ($items as $delta => $item) {
151
        // Serialize an existing data array.
152
        if (isset($item['data']) && is_array($item['data'])) {
153
          $entity->{$field_name}[$langcode][$delta]['data'] = serialize($item['data']);
154
        }
155
      }
156
    }
157
  }
158
}
159

    
160
/**
161
 * Converts saved price field data columns back to arrays for use in the rest of
162
 * the current page request execution.
163
 *
164
 * @param $entity_type
165
 *   The entity type variable passed through hook_field_attach_*().
166
 * @param $entity
167
 *   The entity variable passed through hook_field_attach_*().
168
 */
169
function _commerce_price_field_unserialize_data($entity_type, $entity) {
170
  // Loop over all the price fields attached to this entity.
171
  foreach (_commerce_price_get_price_fields($entity_type, $entity) as $field_name) {
172
    // Iterate over the items arrays for each language.
173
    foreach (array_keys($entity->{$field_name}) as $langcode) {
174
      $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
175

    
176
      // For each item in the array, unserialize or initialize its data array.
177
      foreach ($items as $delta => $item) {
178
        // If we have a non-array $item['data'], unserialize it.
179
        if (!empty($item['data']) && !is_array($item['data'])) {
180
          $entity->{$field_name}[$langcode][$delta]['data'] = unserialize($item['data']);
181
        }
182
        // If we have no data element (or an existing empty), create an empty
183
        // array.
184
        elseif (empty($item['data'])) {
185
          $entity->{$field_name}[$langcode][$delta]['data'] = array('components' => array());
186
        }
187
      }
188
    }
189
  }
190
}
191

    
192
/**
193
 * Implements hook_field_storage_pre_insert().
194
 */
195
function commerce_price_field_storage_pre_insert($entity_type, $entity) {
196
  _commerce_price_field_serialize_data($entity_type, $entity);
197
}
198

    
199
/**
200
 * Implements hook_field_storage_pre_update().
201
 */
202
function commerce_price_field_storage_pre_update($entity_type, $entity) {
203
  _commerce_price_field_serialize_data($entity_type, $entity);
204
}
205

    
206
/**
207
 * Implements hook_field_attach_insert().
208
 *
209
 * This hook is used to unserialize the price field's data array after it has
210
 * been inserted, because the data array is serialized before it is saved and
211
 * must be unserialized for compatibility with API requests performed during the
212
 * same request after the insert occurs.
213
 */
214
function commerce_price_field_attach_insert($entity_type, $entity) {
215
  _commerce_price_field_unserialize_data($entity_type, $entity);
216
}
217

    
218
/**
219
 * Implements hook_field_attach_update().
220
 *
221
 * This hook is used to unserialize the price field's data array after it has
222
 * been updated, because the data array is serialized before it is saved and
223
 * must be unserialized for compatibility with API requests performed during the
224
 * same request after the update occurs.
225
 */
226
function commerce_price_field_attach_update($entity_type, $entity) {
227
  _commerce_price_field_unserialize_data($entity_type, $entity);
228
}
229

    
230
/**
231
 * Implements of hook_field_is_empty().
232
 */
233
function commerce_price_field_is_empty($item, $field) {
234
  return !isset($item['amount']) || (string) $item['amount'] == '';
235
}
236

    
237
/**
238
 * Creates a required, locked instance of a price field on the specified bundle.
239
 *
240
 * @param $field_name
241
 *   The name of the field; if it already exists, a new instance of the existing
242
 *     field will be created. For fields governed by the Commerce modules, this
243
 *     should begin with commerce_.
244
 * @param $entity_type
245
 *   The type of entity the field instance will be attached to.
246
 * @param $bundle
247
 *   The bundle name of the entity the field instance will be attached to.
248
 * @param $label
249
 *   The label of the field instance.
250
 * @param $weight
251
 *   The default weight of the field instance widget and display.
252
 * @param $calculation
253
 *   A string indicating the default value of the display formatter's calculation
254
 *     setting.
255
 * @param $display
256
 *   An array of default display data used for the entity's current view modes.
257
 */
258
function commerce_price_create_instance($field_name, $entity_type, $bundle, $label, $weight = 0, $calculation = FALSE, $display = array()) {
259
  // Look for or add the specified price field to the requested entity bundle.
260
  commerce_activate_field($field_name);
261
  field_cache_clear();
262

    
263
  $field = field_info_field($field_name);
264
  $instance = field_info_instance($entity_type, $field_name, $bundle);
265

    
266
  if (empty($field)) {
267
    $field = array(
268
      'field_name' => $field_name,
269
      'type' => 'commerce_price',
270
      'cardinality' => 1,
271
      'entity_types' => array($entity_type),
272
      'translatable' => FALSE,
273
      'locked' => TRUE,
274
    );
275
    $field = field_create_field($field);
276
  }
277

    
278
  if (empty($instance)) {
279
    $instance = array(
280
      'field_name' => $field_name,
281
      'entity_type' => $entity_type,
282
      'bundle' => $bundle,
283

    
284
      'label' => $label,
285
      'required' => TRUE,
286
      'settings' => array(),
287

    
288
      // Because this widget is locked, we need it to use the full price widget
289
      // since the currency option can't be adjusted at the moment.
290
      'widget' => array(
291
        'type' => 'commerce_price_full',
292
        'weight' => $weight,
293
        'settings' => array(
294
          'currency_code' => 'default',
295
        ),
296
      ),
297

    
298
      'display' => array(),
299
    );
300

    
301
    $entity_info = entity_get_info($entity_type);
302

    
303
    // Spoof the default view mode and node teaser so its display type is set.
304
    $entity_info['view modes'] += array(
305
      'default' => array(),
306
      'node_teaser' => array(),
307
    );
308

    
309
    foreach ($entity_info['view modes'] as $view_mode => $data) {
310
      $instance['display'][$view_mode] = $display + array(
311
        'label' => 'hidden',
312
        'type' => 'commerce_price_formatted_amount',
313
        'settings' => array(
314
          'calculation' => $calculation,
315
        ),
316
        'weight' => $weight,
317
      );
318
    }
319

    
320
    field_create_instance($instance);
321
  }
322
}
323

    
324
/**
325
 * Implements hook_field_formatter_info().
326
 */
327
function commerce_price_field_formatter_info() {
328
  return array(
329
    'commerce_price_raw_amount' => array(
330
      'label' => t('Raw amount'),
331
      'field types' => array('commerce_price'),
332
      'settings' => array(
333
        'calculation' => FALSE,
334
      ),
335
    ),
336
    'commerce_price_formatted_amount' => array(
337
      'label' => t('Formatted amount'),
338
      'field types' => array('commerce_price'),
339
      'settings' => array(
340
        'calculation' => FALSE,
341
      ),
342
    ),
343
    'commerce_price_formatted_components' => array(
344
      'label' => t('Formatted amount with components'),
345
      'field types' => array('commerce_price'),
346
      'settings' => array(
347
        'calculation' => FALSE,
348
      ),
349
    ),
350
  );
351
}
352

    
353
/**
354
 * Implements hook_field_formatter_settings_form().
355
 */
356
function commerce_price_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
357
  $display = $instance['display'][$view_mode];
358
  $settings = $display['settings'];
359

    
360
  $element = array();
361

    
362
  // Do not display any settings for the component formatter.
363
  if ($display['type'] == 'commerce_price_formatted_components') {
364
    return;
365
  }
366

    
367
  // Get all the price calculation options.
368
  $options = module_invoke_all('commerce_price_field_calculation_options', $field, $instance, $view_mode);
369

    
370
  if (empty($options)) {
371
    $element['calculation'] = array(
372
      '#type' => 'value',
373
      '#value' => FALSE,
374
    );
375

    
376
    $element['help'] = array(
377
      '#markup' => '<p>' . t('No configuration is necessary. The original price will be displayed as loaded.') . '</p>',
378
    );
379
  }
380
  else {
381
    // Add the option to display the original price; unshifting will give it a
382
    // key of 0 which will equate to FALSE with an Equal operator.
383
    array_unshift($options, t('Display the original price as loaded.'));
384

    
385
    $element['calculation'] = array(
386
      '#type' => 'radios',
387
      '#options' => $options,
388
      '#default_value' => empty($settings['calculation']) ? '0' : $settings['calculation'],
389
    );
390
  }
391

    
392
  return $element;
393
}
394

    
395
/**
396
 * Implements hook_field_formatter_settings_summary().
397
 */
398
function commerce_price_field_formatter_settings_summary($field, $instance, $view_mode) {
399
  $display = $instance['display'][$view_mode];
400
  $settings = $display['settings'];
401

    
402
  // Do not display a summary for the component formatter.
403
  if ($display['type'] == 'commerce_price_formatted_components') {
404
    return;
405
  }
406

    
407
  $summary = array();
408

    
409
  if ($settings['calculation'] == FALSE) {
410
    $summary[] = t('Displaying the original price');
411
  }
412
  else {
413
    $summary[] = t('Displaying a calculated price');
414
  }
415

    
416
  return implode('<br />', $summary);
417
}
418

    
419
/**
420
 * Implements hook_field_formatter_prepare_view().
421
 */
422
function commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
423
  // TODO: Loop over the instances and pass them to this hook individually so we
424
  // can enforce prices displaying with components as not being altered.
425

    
426
  // Allow other modules to prepare the item values prior to formatting.
427
  foreach(module_implements('commerce_price_field_formatter_prepare_view') as $module) {
428
    $function = $module . '_commerce_price_field_formatter_prepare_view';
429
    $function($entity_type, $entities, $field, $instances, $langcode, $items, $displays);
430
  }
431
}
432

    
433
/**
434
 * Implements hook_field_formatter_view().
435
 */
436
function commerce_price_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
437
  $translated_instance = commerce_i18n_object('field_instance', $instance);
438

    
439
  $element = array();
440

    
441
  // Loop through each price value in this field.
442
  foreach ($items as $delta => $item) {
443
    // Do not render a price if the amount is NULL (i.e. non-zero empty value).
444
    if (is_null($item['amount'])) {
445
      // TODO: Consider if we should render as N/A or something indicating a
446
      // price was not available as opposed to just leaving a blank.
447
      continue;
448
    }
449

    
450
    // Theme the display of the price based on the display type.
451
    switch ($display['type']) {
452
      case 'commerce_price_raw_amount':
453
        $element[$delta] = array(
454
          '#markup' => check_plain($item['amount']),
455
        );
456
        break;
457

    
458
      case 'commerce_price_formatted_amount':
459
        $element[$delta] = array(
460
          '#markup' => commerce_currency_format($item['amount'], $item['currency_code'], $entity),
461
        );
462
        break;
463

    
464
      case 'commerce_price_formatted_components':
465
        // Build an array of component display titles and their prices.
466
        $components = array();
467
        $weight = 0;
468

    
469
        foreach ($item['data']['components'] as $key => $component) {
470
          $component_type = commerce_price_component_type_load($component['name']);
471

    
472
          if (empty($components[$component['name']])) {
473
            $components[$component['name']] = array(
474
              'title' => check_plain($component_type['display_title']),
475
              'price' => commerce_price_component_total($item, $component['name']),
476
              'weight' => $component_type['weight'],
477
            );
478

    
479
            $weight = max($weight, $component_type['weight']);
480
          }
481
        }
482

    
483
        // If there is only a single component and its price equals the field's,
484
        // then remove it and just show the actual price amount.
485
        if (count($components) == 1 && in_array('base_price', array_keys($components))) {
486
          $components = array();
487
        }
488

    
489
        // Add the actual field value to the array.
490
        $components['commerce_price_formatted_amount'] = array(
491
          'title' => check_plain($translated_instance['label']),
492
          'price' => $item,
493
          'weight' => $weight + 1,
494
        );
495

    
496
        // Allow other modules to alter the components.
497
        drupal_alter('commerce_price_formatted_components', $components, $item, $entity);
498

    
499
        // Sort the components by weight.
500
        uasort($components, 'drupal_sort_weight');
501

    
502
        // Format the prices for display.
503
        foreach ($components as $key => &$component) {
504
          $component['formatted_price'] = commerce_currency_format(
505
            $component['price']['amount'],
506
            $component['price']['currency_code'],
507
            $entity
508
          );
509
        }
510

    
511
        $element[$delta] = array(
512
          '#markup' => theme('commerce_price_formatted_components', array('components' => $components, 'price' => $item)),
513
        );
514
        break;
515
    }
516
  }
517

    
518
  return $element;
519
}
520

    
521
/**
522
 * Themes a price components table.
523
 *
524
 * @param $variables
525
 *   Includes the 'components' array and original 'price' array.
526
 */
527
function theme_commerce_price_formatted_components($variables) {
528
  // Add the CSS styling to the table.
529
  drupal_add_css(drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css');
530

    
531
  // Build table rows out of the components.
532
  $rows = array();
533

    
534
  foreach ($variables['components'] as $name => $component) {
535
    $rows[] = array(
536
      'data' => array(
537
        array(
538
          'data' => $component['title'],
539
          'class' => array('component-title'),
540
        ),
541
        array(
542
          'data' => $component['formatted_price'],
543
          'class' => array('component-total'),
544
        ),
545
      ),
546
      'class' => array(drupal_html_class('component-type-' . $name)),
547
    );
548
  }
549

    
550
  return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('commerce-price-formatted-components'))));
551
}
552

    
553
/**
554
 * Implements hook_field_widget_info().
555
 */
556
function commerce_price_field_widget_info() {
557
  return array(
558
    'commerce_price_simple' => array(
559
      'label' => t('Price textfield'),
560
      'field types' => array('commerce_price'),
561
      'settings' => array(
562
        'currency_code' => 'default',
563
      ),
564
    ),
565
    'commerce_price_full' => array(
566
      'label' => t('Price with currency'),
567
      'field types' => array('commerce_price'),
568
      'settings' => array(
569
        'currency_code' => 'default',
570
      ),
571
    ),
572
  );
573
}
574

    
575
/**
576
 * Implements hook_field_widget_settings_form().
577
 */
578
function commerce_price_field_widget_settings_form($field, $instance) {
579
  $form = array();
580

    
581
  // Build an options array of allowed currency values including the option for
582
  // the widget to always use the store's default currency.
583
  $options = array(
584
    'default' => t('- Default store currency -'),
585
  );
586

    
587
  foreach (commerce_currencies(TRUE) as $currency_code => $currency) {
588
    $options[$currency_code] = t('@code - @name', array('@code' => $currency['code'], '@name' => $currency['name']));
589
  }
590

    
591
  $form['currency_code'] = array(
592
    '#type' => 'select',
593
    '#title' => ($instance['widget']['type'] == 'commerce_price_simple') ? t('Currency') : t('Default currency'),
594
    '#options' => $options,
595
    '#default_value' => $instance['widget']['settings']['currency_code'],
596
  );
597

    
598
  return $form;
599
}
600

    
601
/**
602
 * Implements hook_field_widget_form().
603
 */
604
function commerce_price_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
605
  // Use the default currency if the setting is not present.
606
  if (empty($instance['widget']['settings']['currency_code']) || $instance['widget']['settings']['currency_code'] == 'default') {
607
    $default_currency_code = NULL;
608
  }
609
  else {
610
    $default_currency_code = $instance['widget']['settings']['currency_code'];
611
  }
612

    
613
  // If a price has already been set for this instance prepare default values.
614
  if (isset($items[$delta]['amount'])) {
615
    $currency = commerce_currency_load($items[$delta]['currency_code']);
616

    
617
    // Convert the price amount to a user friendly decimal value.
618
    $default_amount = commerce_currency_amount_to_decimal($items[$delta]['amount'], $currency['code']);
619

    
620
    // Run it through number_format() to ensure it has the proper number of
621
    // decimal places.
622
    $default_amount = number_format($default_amount, $currency['decimals'], '.', '');
623

    
624
    $default_currency_code = $items[$delta]['currency_code'];
625
  }
626
  else {
627
    $default_amount = NULL;
628
  }
629

    
630
  // Load the default currency for this instance.
631
  $default_currency = commerce_currency_load($default_currency_code);
632

    
633
  $element['#attached']['css'][] = drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css';
634

    
635
  // Build the form based on the type of price widget.
636
  switch ($instance['widget']['type']) {
637
    // The simple widget is just a textfield with a non-changeable currency.
638
    case 'commerce_price_simple':
639
      $element['amount'] = array(
640
        '#type' => 'textfield',
641
        '#title' => $element['#title'],
642
        '#default_value' => $default_amount,
643
        '#required' => $instance['required'] && ($delta == 0 || $field['cardinality'] > 0),
644
        '#size' => 10,
645
        '#field_suffix' => $default_currency['code'],
646
      );
647

    
648
      // Add the help text if specified.
649
      if (!empty($element['#description'])) {
650
        $element['amount']['#field_suffix'] .= '<div class="description">' . $element['#description'] . '</div>';
651
      }
652

    
653
      $element['currency_code'] = array(
654
        '#type' => 'value',
655
        '#default_value' => $default_currency['code'],
656
      );
657
      break;
658

    
659
    // The full widget is a textfield with a currency select list.
660
    case 'commerce_price_full':
661
      $element['amount'] = array(
662
        '#type' => 'textfield',
663
        '#title' => $element['#title'],
664
        '#default_value' => $default_amount,
665
        '#required' => $instance['required'] && ($delta == 0 || $field['cardinality'] > 0),
666
        '#size' => 10,
667
      );
668

    
669
      // Build a currency options list from all enabled currencies.
670
      $options = array();
671

    
672
      foreach (commerce_currencies(TRUE) as $currency_code => $currency) {
673
        $options[$currency_code] = check_plain($currency['code']);
674
      }
675

    
676
      // If the current currency value is not available, add it now with a
677
      // message in the help text explaining it.
678
      if (empty($options[$default_currency['code']])) {
679
        $options[$default_currency['code']] = check_plain($default_currency['code']);
680

    
681
        $description = t('The currency set for this price is not currently enabled. If you change it now, you will not be able to set it back.');
682
      }
683
      else {
684
        $description = '';
685
      }
686

    
687
      // If only one currency option is available, don't use a select list.
688
      if (count($options) == 1) {
689
        $currency_code = key($options);
690

    
691
        $element['amount']['#field_suffix'] = $currency_code;
692

    
693
        // Add the help text if specified.
694
        if (!empty($element['#description'])) {
695
          $element['amount']['#field_suffix'] .= '<div class="description">' . $element['#description'] . '</div>';
696
        }
697

    
698
        $element['currency_code'] = array(
699
          '#type' => 'value',
700
          '#default_value' => $currency_code,
701
        );
702
      }
703
      else {
704
        $element['amount']['#prefix'] = '<div class="commerce-price-full">';
705

    
706
        $element['currency_code'] = array(
707
          '#type' => 'select',
708
          '#description' => $description,
709
          '#options' => $options,
710
          '#default_value' => isset($items[$delta]['currency_code']) ? $items[$delta]['currency_code'] : $default_currency['code'],
711
          '#suffix' => '</div>',
712
        );
713

    
714
        // Add the help text if specified.
715
        if (!empty($element['#description'])) {
716
          $element['currency_code']['#suffix'] .= '<div class="description">' . $element['#description'] . '</div>';
717
        }
718
      }
719
      break;
720
  }
721

    
722
  $element['data'] = array(
723
    '#type' => 'value',
724
    '#default_value' => !empty($items[$delta]['data']) ? $items[$delta]['data'] : array('components' => array()),
725
  );
726

    
727
  $element['#element_validate'][] = 'commerce_price_field_widget_validate';
728

    
729
  return $element;
730
}
731

    
732
/**
733
 * Validate callback: ensures the amount value is numeric and converts it from a
734
 * decimal value to an integer price amount.
735
 */
736
function commerce_price_field_widget_validate($element, &$form_state) {
737
  if ($element['amount']['#value'] !== '') {
738
    // Ensure the price is numeric.
739
    if (!is_numeric($element['amount']['#value'])) {
740
      form_error($element['amount'], t('%title: you must enter a numeric value for the price amount.', array('%title' => $element['amount']['#title'])));
741
    }
742
    else {
743
      // Convert the decimal amount value entered to an integer based amount value.
744
      form_set_value($element['amount'], commerce_currency_decimal_to_amount($element['amount']['#value'], $element['currency_code']['#value']), $form_state);
745
    }
746
  }
747
}
748

    
749
/**
750
 * Implements hook_field_widget_error().
751
 */
752
function commerce_price_field_widget_error($element, $error, $form, &$form_state) {
753
  form_error($element['amount'], $error['message']);
754
}
755

    
756
/**
757
 * Callback to alter the property info of price fields.
758
 *
759
 * @see commerce_price_field_info().
760
 */
761
function commerce_price_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
762
  $name = $field['field_name'];
763
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
764

    
765
  $property['type'] = ($field['cardinality'] != 1) ? 'list<commerce_price>' : 'commerce_price';
766
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
767
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
768
  $property['auto creation'] = 'commerce_price_field_data_auto_creation';
769
  $property['property info'] = commerce_price_field_data_property_info();
770

    
771
  unset($property['query callback']);
772
}
773

    
774
/**
775
 * Returns the default array structure for a Price field for use when creating
776
 *   new data arrays through an entity metadata wrapper.
777
 */
778
function commerce_price_field_data_auto_creation() {
779
  return array('amount' => 0, 'currency_code' => commerce_default_currency(), 'data' => array('components' => array()));
780
}
781

    
782
/**
783
 * Defines info for the properties of the Price field data structure.
784
 */
785
function commerce_price_field_data_property_info($name = NULL) {
786
  return array(
787
    'amount' => array(
788
      'label' => t('Amount'),
789
      'description' => !empty($name) ? t('Amount value of field %name', array('%name' => $name)) : '',
790
      'type' => 'decimal',
791
      'getter callback' => 'entity_property_verbatim_get',
792
      'setter callback' => 'entity_property_verbatim_set',
793
    ),
794
    'amount_decimal' => array(
795
      'label' => t('Amount (decimal)'),
796
      'description' => !empty($name) ? t('Amount value of field %name (as a decimal)', array('%name' => $name)) : '',
797
      'type' => 'decimal',
798
      'getter callback' => 'commerce_price_amount_decimal_get',
799
      'computed' => TRUE,
800
    ),
801
    'currency_code' => array(
802
      'label' => t('Currency'),
803
      'description' => !empty($name) ? t('Currency code of field %name', array('%name' => $name)) : '',
804
      'type' => 'text',
805
      'getter callback' => 'entity_property_verbatim_get',
806
      'setter callback' => 'entity_property_verbatim_set',
807
      'options list' => 'commerce_currency_code_options_list',
808
    ),
809
    'data' => array(
810
      'label' => t('Data'),
811
      'description' => !empty($name) ? t('Data array of field %name', array('%name' => $name)) : '',
812
      'type' => 'struct',
813
      'getter callback' => 'entity_property_verbatim_get',
814
      'setter callback' => 'entity_property_verbatim_set',
815
    ),
816
  );
817
}
818

    
819
/**
820
 * Property getter callback returing the amount (as a decimal).
821
 */
822
function commerce_price_amount_decimal_get($data, array $options, $name, $type, $info) {
823
  return commerce_currency_amount_to_decimal($data['amount'], $data['currency_code']);
824
}
825

    
826
/**
827
 * Returns the data array of a single value price field from a wrapped entity,
828
 * using an optional default value if the entity does not have data in the field.
829
 *
830
 * @param $wrapper
831
 *   An EntityMetadataWrapper for the entity whose price should be retrieved.
832
 * @param $field_name
833
 *   The name of the field to retrieve data from in the wrapper.
834
 * @param $default
835
 *   Boolean indicating whether or not to return a default price array if the
836
 *   entity does not have data in the specified price field.
837
 *
838
 * @return
839
 *   The data array of the specified price field.
840
 */
841
function commerce_price_wrapper_value($wrapper, $field_name, $default = FALSE) {
842
  // Extract the price field's value array from the given entity.
843
  $price = $wrapper->{$field_name}->value();
844

    
845
  // If the value is empty and we want to return a default value for the field,
846
  // use the auto creation value defined for Entity API usage.
847
  if (empty($price) && $default) {
848
    $price = commerce_price_field_data_auto_creation();
849
  }
850

    
851
  return $price;
852
}
853

    
854
/**
855
 * Implements hook_commerce_price_component_type_info().
856
 */
857
function commerce_price_commerce_price_component_type_info() {
858
  return array(
859
    'base_price' => array(
860
      'title' => t('Base price'),
861
      'display_title' => t('Subtotal'),
862
      'weight' => -50,
863
    ),
864
    'discount' => array(
865
      'title' => t('Discount'),
866
      'weight' => -10,
867
    ),
868
    'fee' => array(
869
      'title' => t('Fee'),
870
      'weight' => -20,
871
    ),
872
  );
873
}
874

    
875
/**
876
 * Returns a list of all available price component types.
877
 */
878
function commerce_price_component_types() {
879
  // First check the static cache for a components array.
880
  $component_types = &drupal_static(__FUNCTION__);
881

    
882
  // If it did not exist, fetch the types now.
883
  if (!isset($component_types)) {
884
    // Find components defined by hook_commerce_price_component_type_info().
885
    $component_types = module_invoke_all('commerce_price_component_type_info');
886

    
887
    // Add default values to the component type definitions.
888
    foreach ($component_types as $name => &$component_type) {
889
      $component_type += array(
890
        'name' => $name,
891
        'display_title' => $component_type['title'],
892
        'weight' => 0,
893
      );
894
    }
895

    
896
    // Allow the info to be altered by other modules.
897
    drupal_alter('commerce_price_component_type_info', $component_types);
898
  }
899

    
900
  return $component_types;
901
}
902

    
903
/**
904
 * Returns an array of price component type titles keyed by name.
905
 */
906
function commerce_price_component_titles() {
907
  static $titles = array();
908

    
909
  if (empty($titles)) {
910
    foreach (commerce_price_component_types() as $name => $component_type) {
911
      $titles[$name] = $component_type['title'];
912
    }
913
  }
914

    
915
  return $titles;
916
}
917

    
918
/**
919
 * Returns a component type array.
920
 *
921
 * @param $name
922
 *   The machine-name of the component type to return.
923
 *
924
 * @return
925
 *   A component type array or FALSE if not found.
926
 */
927
function commerce_price_component_type_load($name) {
928
  $component_types = commerce_price_component_types();
929
  return !empty($component_types[$name]) ? $component_types[$name] : FALSE;
930
}
931

    
932
/**
933
 * Adds a price component to a price's data array.
934
 *
935
 * @param $price
936
 *   The price array the component should be added to.
937
 * @param $type
938
 *   The machine-name of the component type to be added to the array.
939
 * @param $component_price
940
 *   The price array for the component as defined by the price field.
941
 * @param $included
942
 *   Boolean indicating whether or not the price component has already been
943
 *   included in the price the component is being added to.
944
 * @param $add_base_price
945
 *   Boolean indicating whether or not to add the base price component if it is
946
 *   missing.
947
 *
948
 * @return
949
 *   The updated data array.
950
 */
951
function commerce_price_component_add($price, $type, $component_price, $included, $add_base_price = TRUE) {
952
  // If no price components have been added yet, add the base price first.
953
  if ($add_base_price && empty($price['data']['components']) && $type != 'base_price') {
954
    $price['data'] = commerce_price_component_add($price, 'base_price', $price, TRUE);
955
  }
956

    
957
  $price['data']['components'][] = array(
958
    'name' => $type,
959
    'price' => $component_price,
960
    'included' => $included,
961
  );
962

    
963
  return $price['data'];
964
}
965

    
966
/**
967
 * Returns every component of a particular type from a price's data array.
968
 *
969
 * @param $price
970
 *   The price array to load components from.
971
 * @param $type
972
 *   The machine-name of the component type to load.
973
 *
974
 * @return
975
 *   An array of components from the data array matching the type.
976
 */
977
function commerce_price_component_load($price, $type) {
978
  $components = array();
979

    
980
  if (!empty($price['data']['components'])) {
981
    foreach ($price['data']['components'] as $key => $component) {
982
      if ($component['name'] == $type) {
983
        $components[] = $component;
984
      }
985
    }
986
  }
987

    
988
  return $components;
989
}
990

    
991
/**
992
 * Remove all instances of a particular component from a price's data array.
993
 *
994
 * @param &$price
995
 *   The price array to remove components from.
996
 * @param $type
997
 *   The machine-name of the component type to delete.
998
 *
999
 * @return
1000
 *   The updated data array.
1001
 */
1002
function commerce_price_component_delete($price, $type) {
1003
  foreach ((array) $price['data']['components'] as $key => $component) {
1004
    if ($component['name'] == $type) {
1005
      unset($price['data']['components'][$key]);
1006
    }
1007
  }
1008

    
1009
  return $price['data'];
1010
}
1011

    
1012
/**
1013
 * Combines the price components of two prices into one components array,
1014
 *   merging all components of the same type into a single component.
1015
 *
1016
 * @param $price
1017
 *   The base price array whose full data array will be returned.
1018
 * @param $price2
1019
 *   A price array whose components will be combined with those of the base price.
1020
 *
1021
 * @return
1022
 *   A data array with the two sets of components combined but without any
1023
 *     additional data from $price2's data array.
1024
 */
1025
function commerce_price_components_combine($price, $price2) {
1026
  // Ensure the base price data array has a components array.
1027
  if (empty($price['data']['components'])) {
1028
    $price['data']['components'] = array();
1029
  }
1030

    
1031
  // Loop over the components in the second price's data array.
1032
  foreach ($price2['data']['components'] as $key => $component) {
1033
    // Convert the component to the proper currency first.
1034
    if ($component['price']['currency_code'] != $price['currency_code']) {
1035
      $component['price']['amount'] = commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $price['currency_code']);
1036
      $component['price']['currency_code'] = $price['currency_code'];
1037
    }
1038

    
1039
    // Look for a matching component in the base price data array.
1040
    $matched = FALSE;
1041

    
1042
    foreach ($price['data']['components'] as $base_key => $base_component) {
1043
      // If the component type matches the component in question...
1044
      if ($base_component['name'] == $component['name']) {
1045
        // Add the two prices together and mark this as a match.
1046
        $price['data']['components'][$base_key]['price']['amount'] += $component['price']['amount'];
1047

    
1048
        $matched = TRUE;
1049
      }
1050
    }
1051

    
1052
    // If no match was found, bring the component in as is.
1053
    if (!$matched) {
1054
      $price['data']['components'][] = $component;
1055
    }
1056
  }
1057

    
1058
  return $price['data'];
1059
}
1060

    
1061
/**
1062
 * Returns the total value of components in a price array converted to the
1063
 *   currency of the price array.
1064
 *
1065
 * @param $price
1066
 *   The price whose components should be totalled.
1067
 * @param $name
1068
 *   Optionally specify a component name to restrict the totalling to components
1069
 *     of that type.
1070
 *
1071
 * @return
1072
 *   A price array representing the total value.
1073
 */
1074
function commerce_price_component_total($price, $name = NULL) {
1075
  // Initialize the total price array.
1076
  $total = array(
1077
    'amount' => 0,
1078
    'currency_code' => $price['currency_code'],
1079
    'data' => array(),
1080
  );
1081

    
1082
  // Bail out if there are no components.
1083
  if (empty($price['data']['components'])) {
1084
    return $total;
1085
  }
1086

    
1087
  // Loop over each component.
1088
  foreach ($price['data']['components'] as $key => $component) {
1089
    // If we're totalling all components or this one matches the requested type...
1090
    if (empty($name) || $name == $component['name']) {
1091
      $total['amount'] += commerce_currency_convert(
1092
        $component['price']['amount'],
1093
        $component['price']['currency_code'],
1094
        $total['currency_code']
1095
      );
1096
    }
1097
  }
1098

    
1099
  return $total;
1100
}
1101

    
1102
/**
1103
 * Implements hook_views_api().
1104
 */
1105
function commerce_price_views_api() {
1106
  return array(
1107
    'api' => 3,
1108
    'path' => drupal_get_path('module', 'commerce_price') . '/includes/views',
1109
  );
1110
}
1111

    
1112
/**
1113
 * Validates data entered via a price input form in a Rules condition or action.
1114
 */
1115
function _commerce_price_rules_data_ui_element_validate($element, &$form_state, $form) {
1116
  $value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
1117

    
1118
  $value['amount'] = trim($value['amount']);
1119
  $currency = commerce_currency_load($value['currency_code']);
1120

    
1121
  // Required elements don't work on these input forms, so instead catch
1122
  // an empty value here and require a numeric amount.
1123
  if ($value['amount'] == '') {
1124
    form_error($element, t('A numeric amount is required for setting and comparing against price field data.'));
1125
  }
1126

    
1127
  // Only convert price amount to major units if we have a numeric value,
1128
  // otherwise throw an error.
1129
  if (is_numeric($value['amount'])) {
1130
    // Ensure price amount is formatted correctly in major units according to the
1131
    // currency code.
1132
    $minor_unit_amount = number_format($value['amount'], $currency['decimals'], '.', '');
1133

    
1134
    // Now that the minor unit amount expected by the currency, we can safely
1135
    // convert back to major units for storage.
1136
    $value['amount'] = commerce_currency_decimal_to_amount($minor_unit_amount, $value['currency_code']);
1137

    
1138
    form_set_value($element, $value, $form_state);
1139
  }
1140
  else {
1141
    form_error($element, t('Price amount must be numeric.'));
1142
  }
1143
}