You are here

commerce_bpc.module in Commerce Bulk Product Creation 7

Same filename and directory in other branches
  1. 7.2 commerce_bpc.module

File

commerce_bpc.module
View source
<?php

/**
 * @file
 * This module will use a form input page to allow users to create related
 * products quickly and easily.
 *
 * It will also create new functions that can be used by other modules for
 * bulk product creation.
 */
module_load_include('inc', 'commerce_bpc', 'commerce_bpc.hooks_list');
module_load_include('inc', 'commerce_bpc', 'commerce_bpc.tokens');
module_load_include('inc', 'commerce_bpc', 'commerce_bpc.settings');

/**
 * Implements hook_help().
 */
function commerce_bpc_help($path, $arg) {
  switch ($path) {
    case 'admin/help#commerce_bpc':
      return t('This module helps you create groups of related products quickly and easily.
        To use it, you must have at least one Product Type that has one or more fields attached to it.
        If you wish to attach the products all at once to a Display Node as part of the Bulk Creation process,
        you must first have created a Display Node Type that has a commerce_product_reference field which can accept multiple products.');
  }
}

/**
 * Implements hook_menu().
 */
function commerce_bpc_menu() {

  // Add a product.
  $items['admin/commerce/products/add-bulk'] = array(
    'title' => 'Bulk add products',
    'description' => 'Add a new group of products for sale in bulk.',
    'page callback' => 'commerce_bpc_add_page',
    'access callback' => 'commerce_bpc_product_add_any_access',
    'file' => 'commerce_bpc.pages.inc',
  );
  foreach (commerce_product_types() as $type => $product_type) {
    if (commerce_bpc_valid_product_type($type)) {
      $items['admin/commerce/products/add-bulk/' . $type] = array(
        'title' => 'Bulk add @name',
        'title arguments' => array(
          '@name' => $product_type['name'],
        ),
        'description' => $product_type['description'],
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'commerce_bpc_create_bulk_form',
          $type,
        ),
        'file' => 'commerce_bpc.forms.inc',
        'access callback' => 'commerce_product_access',
        'access arguments' => array(
          'create',
          commerce_product_new($type),
        ),
      );
      $items['admin/commerce/products/add-bulk/' . $type . '/display/%'] = array(
        'title' => 'Create display node',
        'title arguments' => array(
          '@name' => $product_type['name'],
        ),
        'description' => 'Create a display node for all created products',
        'page callback' => 'commerce_bpc_create_bulk_form_display_node',
        'page arguments' => array(
          $type,
          6,
        ),
        'file' => 'commerce_bpc.forms.inc',
        'type' => MENU_CALLBACK,
        'access callback' => 'commerce_product_access',
        'access arguments' => array(
          'create',
          commerce_product_new($type),
        ),
      );
    }
  }
  $items['admin/commerce/config/commerce_bpc'] = array(
    'title' => 'Bulk product creation',
    'description' => 'Settings for the bulk product creation functionality',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_bpc_pattern_settings_form',
    ),
    'access arguments' => array(
      'configure commerce bpc',
    ),
    'file' => 'commerce_bpc.admin.inc',
    'weight' => 1,
  );
  $items['admin/commerce/config/commerce_bpc/patterns'] = array(
    'title' => 'Patterns',
    'access arguments' => array(
      'configure commerce bpc',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/commerce/config/commerce_bpc/display_nodes'] = array(
    'title' => 'Display node settings',
    'description' => 'Settings for the bulk product creation display node settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_bpc_display_node_settings_form',
    ),
    'access arguments' => array(
      'configure commerce bpc',
    ),
    'file' => 'commerce_bpc.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
  foreach (commerce_product_types() as $type => $product_type) {
    if (commerce_bpc_valid_product_type($type)) {
      $items['admin/commerce/products/types/' . $type . '/commerce_bpc'] = array(
        'title' => 'Bulk product creation',
        'description' => 'Settings for the bulk product creation functionality',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'commerce_bpc_pattern_settings_form',
          $type,
        ),
        'access arguments' => array(
          'configure commerce bpc',
        ),
        'file' => 'commerce_bpc.admin.inc',
        'weight' => 10,
        'type' => MENU_LOCAL_TASK,
      );
      $items['admin/commerce/products/types/' . $type . '/commerce_bpc/patterns'] = array(
        'title' => 'Patterns',
        'access arguments' => array(
          'configure commerce bpc',
        ),
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => 0,
      );
      $items['admin/commerce/products/types/' . $type . '/commerce_bpc/display_nodes'] = array(
        'title' => 'Display node settings',
        'description' => 'Settings for the bulk product creation display node settings',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'commerce_bpc_display_node_settings_form',
          $type,
        ),
        'access arguments' => array(
          'configure commerce bpc',
        ),
        'file' => 'commerce_bpc.admin.inc',
        'type' => MENU_LOCAL_TASK,
        'weight' => 10,
      );
    }
  }
  return $items;
}

/**
 *  Implements hook_permission().
 */
function commerce_bpc_permission() {
  return array(
    'configure commerce bpc' => array(
      'title' => t('Configure Commerce bulk product creation'),
      'description' => t('Allows users to update settings pertaining to Product bulk creation.'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function commerce_bpc_menu_local_tasks_alter(&$data, $router_item, $root_path) {

  // Add action link 'admin/commerce/products/add-bulk' on
  // 'admin/commerce/products'.
  if ($root_path == 'admin/commerce/products') {
    $item = menu_get_item('admin/commerce/products/add-bulk');
    if ($item['access']) {
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_action',
        '#link' => $item,
      );
    }
  }
}

/**
 * Implements hook_field_create_instance().
 *
 * Rebuilds the menu so that new menu items are created under
 * /admin/commerce/add-bulk if the product type is now
 * available for bulk creation.
 */
function commerce_bpc_field_create_instance($instance) {
  if ($instance['entity_type'] == 'commerce_product' && array_key_exists($instance['bundle'], commerce_product_types())) {
    variable_set('menu_rebuild_needed', TRUE);
  }
}

/**
 * Implements hook_field_update_instance().
 *
 * Rebuilds the menu so that new menu items are created under
 * /admin/commerce/add-bulk if the product type is now
 * available for bulk creation.
 */
function commerce_bpc_field_update_instance($instance) {
  if ($instance['entity_type'] == 'commerce_product') {
    variable_set('menu_rebuild_needed', TRUE);
  }
}

/**
 * Implements hook_field_delete_instance().
 *
 * Rebuilds the  menu so that the menu item under /admin/commerce/add-bulk is
 * removed if the product type is no longer available for bulk creation.
 */
function commerce_bpc_field_delete_instance($instance) {
  if ($instance['entity_type'] == 'commerce_product') {
    variable_set('menu_rebuild_needed', TRUE);
  }
}

/**
 * Implements  hook_form_BASE_FORM_ID_alter().
 *
 * Prepopulate product reference fields in the node creation form after
 * bulk creation.
 */
function commerce_bpc_form_node_form_alter(&$form, &$form_state, $form_id) {

  // If we reach the node form after being redirected from the bulk
  // creation form, there will be a GET parameter that contains a
  // unique ID telling us where to find the IDs of the created products
  // in the session array.
  if (isset($_GET['bulk_creation_id']) && isset($_SESSION['bulk_product_ids'][$_GET['bulk_creation_id']])) {
    $ids = $_SESSION['bulk_product_ids'][$_GET['bulk_creation_id']];
    $form['title']['#default_value'] = $_SESSION['bulk_title'][$_GET['bulk_creation_id']];
    $field_name = _commerce_bpc_get_reference_field_name($form['type']['#value']);
    $field = field_info_field($field_name);
    switch ($form[$field_name][LANGUAGE_NONE]['#type']) {
      case 'textfield':

        // The autocomplete text field uses SKUs, so we need to retrieve those.
        $products = commerce_product_load_multiple($ids);
        $skus = array();
        foreach ($products as $product) {
          $skus[] = $product->sku;
        }
        $form[$field_name][LANGUAGE_NONE]['#default_value'] = implode(', ', $skus);
        break;
      case 'select':
      case 'checkboxes':
        $form[$field_name][LANGUAGE_NONE]['#default_value'] = $ids;
        break;
    }
  }
}

// ======================================
// API Functions
// ======================================

/**
 * API function to create bulk products based on the given parameters
 *
 * @param string $product_type
 *   The name of the product type for which products should be created.
 * @param array $combinations
 *   An array of arrays, each corresponding to one combination of values
 *   for which a product should be created. Each such array should have
 *   field names as keys and field value-arrays as values. Example for
 *   a non-translatable field with one value (for the current "combination"):
 *   @code
 *   'field_list1' => array(
 *      LANGUAGE_NONE => array(
 *        0 => array(
 *          'value' => 'foo'
 *        ),
 *      ),
 *    )
 *    @endcode
 *    This is the format returned by field_ui's forms and by the field api
 *    in general.
 * @param array $static_values
 *   A single array of the same format as the combination-arrays above,
 *   representing the fields that should have identical values across all
 *   combinations.
 * @param array $extras
 *   An array for the values of  'extra fields' defined for the product type
 *   entity, or patterns for these. Recognized keys are:
 *   - sku_pattern
 *   - title_pattern
 *   - status
 *   - uid
 *   Note that the values do NOT come in the form of complex arrays (as they
 *   are not translatable, and can only have single values) like the previous
 *   two parameters.
 *
 * @return array
 *   An array of the IDs of new products that were created.
 */
function commerce_bpc_create_bulk_products($product_type, $combinations = array(), $static_values = array(), $extras = array()) {
  $extras += array(
    'uid' => 1,
  );
  $bulk_products = array();

  // For each combination, create a product.
  foreach ($combinations as $combination) {
    $bulk_products[] = commerce_bpc_create_product($product_type, $combination, $static_values, $extras);
  }
  return $bulk_products;
}

/**
 * API function to a bulk product based on the given parameters
 *
 * @param string $product_type
 *   The name of the product type for which products should be created.
 * @param array $combination
 *   An corresponding to one combination of values
 *   for which a product should be created. This array should have
 *   field names as keys and field value-arrays as values. Example for
 *   a non-translatable field with one value (for the current "combination"):
 *   @code
 *   'field_list1' => array(
 *      LANGUAGE_NONE => array(
 *        0 => array(
 *          'value' => 'foo'
 *        ),
 *      ),
 *    )
 *    @endcode
 *    This is the format returned by field_ui's forms and used by the field api
 *    in general.
 * @param array $static_values
 *   An array of the same format as the combination-array above,
 *   representing the fields that should have identical values across all
 *   combinations.
 * @param array $extras
 *   An array for the values of  'extra fields' defined for the product type
 *   entity, or patterns for these. Recognized keys are:
 *   - sku_pattern
 *   - title_pattern
 *   - status
 *   - uid
 *   Note that the values do NOT come in the form of complex arrays (as they
 *   are not translatable, and can only have single values) like the previous
 *   two parameters.
 *
 * @return int
 *   The ID of the created product.
 */
function commerce_bpc_create_product($product_type, $combination, $static_values, $extras) {
  $form_state = array();
  $form_state['values'] = $static_values;
  $form = array();
  $form['#parents'] = array();

  // Generate a new product object.
  $new_product = commerce_product_new($product_type);
  $new_product->status = $extras['status'];
  $new_product->uid = $extras['uid'];
  $new_product->language = LANGUAGE_NONE;

  // Replace the tokens for the SKU and Title.
  $data = array();
  $data['bulk_data'] = array(
    'combination' => $combination,
    'static_values' => $static_values,
    'product_type' => $product_type,
  );
  $new_product->sku = token_replace($extras['sku_pattern'], $data, array(
    'sanitize' => FALSE,
  ));
  $new_product->title = token_replace($extras['title_pattern'], $data, array(
    'sanitize' => FALSE,
  ));

  // Set the proper values in the form_state for this product's combination of
  // fields.
  foreach ($combination as $field => $value) {
    $form_state['values'][$field] = $value;
  }

  // Notify field widgets to save their field data.
  field_attach_submit('commerce_product', $new_product, $form, $form_state);
  commerce_product_save($new_product);
  return $new_product->product_id;
}

/**
 * Build combinations from a submitted form.
 *
 * @param array $form
 *   The form structure of the bulk creation form, after it has been
 *   altered by the bulk creation hooks.
 * @param array $form_state
 *   The form state after form submission. Modules that have altered
 *   the form through the bulk creation hooks will have recorded what
 *   they did in $form_state['commerce_bpc'][MODULE_NAME].
 *
 * @return array
 *   An array describing the combinations.
 */
function commerce_bpc_get_combinations($form, &$form_state) {

  // We need the cartesian product of the arrays in $options.
  // In order to do that, we loop through these arrays, at every
  // stage taking the cartesian product of the actual array with
  // what is there already. So after the first iteration,
  // $combinations will just be the first member of $options.
  // After the second, it will be the product of this with the
  // second member, after the third, we will have the product
  // of the previous product with the third member, and so on.
  //
  // Initialize with an empty array as sole member, so that the first
  // iteration has something to work with.
  $combinations = array(
    array(),
  );
  $hook = 'commerce_bpc_get_combinations';
  foreach (module_implements($hook) as $module) {
    $function = $module . '_' . $hook;
    $function($form, $form_state, $combinations);
  }
  return $combinations;
}

/**
 * Check if a product_type is a valid candidate for bulk creation.
 *
 * @param string $type
 *   The machine name of the type for which to check.
 *
 * @return bool
 *   Boolean indicating whether $type can be used in bulk creation.
 */
function commerce_bpc_valid_product_type($type) {
  $instances = field_info_instances('commerce_product', $type);
  foreach ($instances as $field_name => $instance) {
    if (commerce_bpc_is_combination_field($instance)) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Determine node types that can serve as display nodes.
 *
 * @param string $product_type
 *   The product type that should be referenced. Pass FALSE to only retrieve
 *   those node types that can reference all product types.
 *
 * @return array
 *   An array of node types that can reference $product_type. Keys are
 *   the bundle name of the node type, values are their human readable names.
 */
function commerce_bpc_get_node_types($product_type = FALSE) {
  $allowed_types = array();
  $node_types = node_type_get_types();
  foreach ($node_types as $type => $node_type) {
    if ($field_name = _commerce_bpc_get_reference_field_name($type)) {
      $instance = field_info_instance('commerce_product', $field_name, $type);
      $referenceable = isset($instance['settings']['referenceable_types']) ? $instance['settings']['referenceable_types'] : array();

      // Deselected fields will show up here as keys with value
      // 0. In order to correctly determine whether no value was
      // set (== all product types are referenceable), we filter
      // out these values.
      $referenceable = array_filter($referenceable);
      if (empty($referenceable) || !empty($product_type) && isset($referenceable[$product_type])) {
        $allowed_types[$type] = $node_type->name;
      }
    }
  }
  return $allowed_types;
}

/**
 * Access callback: determines if there is any bulk-able product type that
 * the user is allowed to create.
 */
function commerce_bpc_product_add_any_access() {

  // Grant automatic access to users with administer products permission.
  if (user_access('administer commerce_product entities')) {
    return TRUE;
  }

  // Check the user's access on a product type basis.
  foreach (commerce_product_types() as $type => $product_type) {
    if (commerce_product_access('create', commerce_product_new($type)) && commerce_bpc_valid_product_type($type)) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_bpc_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  $settings = $form['#instance'];
  if ($form['#instance']['entity_type'] == 'commerce_product') {
    $form['instance']['commerce_bpc'] = array(
      '#type' => 'fieldset',
      '#title' => t('Bulk product creation'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    $form['instance']['commerce_bpc']['show_field'] = array(
      '#type' => 'checkbox',
      '#title' => t('Show this field on the bulk product creation form'),
      '#default_value' => !isset($settings['commerce_bpc']['show_field']) ? TRUE : $settings['commerce_bpc']['show_field'],
    );
    if ($form['#field']['module'] == 'list') {
      $form['instance']['commerce_bpc']['is_static'] = array(
        '#type' => 'checkbox',
        '#title' => t('Treat this field as static'),
        '#default_value' => !isset($settings['commerce_bpc']['is_static']) ? FALSE : $settings['commerce_bpc']['is_static'],
        '#description' => t('List fields can either be used to create combinations, or can be set to be static, in which case all bulk-created products will share the same value for the field.'),
      );
    }
  }
}

/**
 * Retrieve nested value in and array, providing a default.
 *
 * This helper function allows to retrieve a nested value (of varying
 * depth) from an array, providing a default in case the path does not
 * exists.
 *
 * @param array $array
 *   The array from which to retrieve the value.
 * @parram array $path
 *   Array of keys leading to the value to be retrieved. For example, to
 *   retrieve $array['settings']['storage'], $path should be
 *   array('settings', 'storage).
 * @param mixed $default
 *   The value to return if the given path does not exist in $array.
 *
 * @return mixed
 *   The value of the array at $path, or $default if this path does not
 *   exist.
 */
function _commerce_bpc_get_value($array, $path, $default) {
  $return = drupal_array_get_nested_value($array, $path, $key_exists);
  if (!$key_exists) {
    $return = $default;
  }
  return $return;
}

/**
 * Displays tokens in a table.
 *
 * @param string $type
 *   The type of the tokens to be displayed, as defined in hook_token_info().
 * @param array $avail_tokens
 *   The tokens available for the current replacement.
 *
 * @return array
 *   A renderable array displaying the tokens.
 */
function _commerce_bpc_show_tokens($type, $avail_tokens = array()) {
  $rows = array();
  foreach ($avail_tokens as $token_name => $info) {
    $rows[] = array(
      '[' . $type . ':' . $token_name . ']',
      $info['name'],
      $info['description'],
    );
  }
  $tokens = array(
    '#type' => 'fieldset',
    '#title' => t('Tokens'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $tokens_table = array(
    'header' => array(
      t('Token'),
      t('Label'),
      t('Desription'),
    ),
    'rows' => $rows,
    'attributes' => array(),
    'caption' => t('Available Tokens'),
    'colgroups' => array(),
    'sticky' => FALSE,
    'empty' => '',
  );
  $tokens['available_tokens'] = array(
    '#type' => 'item',
    '#title' => t('You may use the following tokens:'),
    '#markup' => theme_table($tokens_table),
  );
  return $tokens;
}

/**
 * Determine whether a field instance is suitable for combination-generation.
 *
 * @param array $instance
 *   The instance array describing the instance.
 *
 * @return bool
 *   TRUE if the instance can be used to generate combinations, FALSE
 *   otherwise.
 *
 * @see hook_commerce_bpc_is_combination_field()
 */
function commerce_bpc_is_combination_field($instance) {
  return in_array(TRUE, module_invoke_all('commerce_bpc_is_combination_field', $instance));
}

/**
 * Determine the name of the product reference field (if any) on a node type.
 *
 * This is used, for example, to determine which field to prepopulate
 * in the node form after bulk creation.
 *
 * @param object $node_type
 *   The bundle name of the node type for which the product reference field
 *   is to be determined.
 *
 * @return string|NULL
 *   The machine name of the product reference field, if there is one,
 *   NULL otherwise.
 *
 * @todo
 *  Right now, we simply return the first product reference field we find.
 *  What is the best way to deal with multiple product reference fields?
 */
function _commerce_bpc_get_reference_field_name($node_type) {
  if ($fields = field_info_instances('node', $node_type)) {
    foreach ($fields as $name => $field) {
      $field_info = field_read_field($name);
      if ($field_info['type'] == 'commerce_product_reference') {
        return $name;
      }
    }
  }
}

Functions

Namesort descending Description
commerce_bpc_create_bulk_products API function to create bulk products based on the given parameters
commerce_bpc_create_product API function to a bulk product based on the given parameters
commerce_bpc_field_create_instance Implements hook_field_create_instance().
commerce_bpc_field_delete_instance Implements hook_field_delete_instance().
commerce_bpc_field_update_instance Implements hook_field_update_instance().
commerce_bpc_form_field_ui_field_edit_form_alter Implements hook_form_FORM_ID_alter().
commerce_bpc_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter().
commerce_bpc_get_combinations Build combinations from a submitted form.
commerce_bpc_get_node_types Determine node types that can serve as display nodes.
commerce_bpc_help Implements hook_help().
commerce_bpc_is_combination_field Determine whether a field instance is suitable for combination-generation.
commerce_bpc_menu Implements hook_menu().
commerce_bpc_menu_local_tasks_alter Implements hook_menu_local_tasks_alter().
commerce_bpc_permission Implements hook_permission().
commerce_bpc_product_add_any_access Access callback: determines if there is any bulk-able product type that the user is allowed to create.
commerce_bpc_valid_product_type Check if a product_type is a valid candidate for bulk creation.
_commerce_bpc_get_reference_field_name Determine the name of the product reference field (if any) on a node type.
_commerce_bpc_get_value Retrieve nested value in and array, providing a default.
_commerce_bpc_show_tokens Displays tokens in a table.