You are here

commerce_stock.module in Commerce Stock 7.2

Same filename and directory in other branches
  1. 7 commerce_stock.module

Commerce Stock module.

Provides an rules based api for implementing order quantity and stock validation.

This module provides the events to trigger the stock checks and configurable actions to take if the check fails "out of stock: It is the job of sub modules to implement the stock check by configuring rules and their conditions.

File

commerce_stock.module
View source
<?php

/**
 * @file
 * Commerce Stock module.
 *
 * Provides an rules based api for implementing order quantity
 * and stock validation.
 *
 * This module provides the events to trigger the stock checks and
 * configurable actions to take if the check fails "out of stock:
 * It is the job of sub modules to implement the stock check by
 * configuring rules and their conditions.
 */

/**
 * Implements hook_menu().
 */
function commerce_stock_menu() {
  $items = array();
  $items['admin/commerce/config/stock'] = array(
    'title' => 'Stock management',
    'description' => 'Configure stock management.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_stock_admin_form',
    ),
    'access arguments' => array(
      'administer commerce_stock settings',
    ),
    'file' => 'includes/commerce_stock.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/commerce/config/stock/api'] = array(
    'title' => 'Stock management API',
    'description' => 'Configure stock management API.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_stock_admin_form',
    ),
    'access arguments' => array(
      'administer commerce_stock settings',
    ),
    'file' => 'includes/commerce_stock.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function commerce_stock_permission() {
  return array(
    'administer commerce_stock settings' => array(
      'title' => t('Administer commerce stock settings'),
    ),
    'make rule based changes to commerce_stock' => array(
      'title' => t('Make rule based changes to commerce stock'),
    ),
  );
}

/**
 * Implements hook_form_alter().
 *
 * Alters the add-to-cart form to show out-of-stock items and add a validator.
 */
function commerce_stock_form_alter(&$form, &$form_state, $form_id) {
  $commerce_cart_form = 'commerce_cart_form';
  if (module_exists('commerce_cart_view_override')) {
    $commerce_cart_form = variable_get('commerce_cart_view_override_page_view', 'commerce_cart_form');
  }
  if (strpos($form_id, "commerce_cart_add_to_cart_form") === 0) {

    // Check if product is disabled.
    if (isset($form['submit']['#attributes']['disabled']) && $form['submit']['#attributes']['disabled'] == 'disabled') {
      return;
    }

    // Check to see if product has options (multiple products using
    // the default dropdown).
    if (isset($form['product_id']['#options'])) {

      // Set validation.
      $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
      commerce_stock_cart_state_validate_options($form_id, $form, $form_state);
    }
    elseif (isset($form['product_id']['#value'])) {

      // @todo new rules event for handling options - do we need it?
      // Add validation to the add to cart
      $form['#validate'][] = 'commerce_stock_add_to_cart_validate';

      // Check if the add to cart form should be enabled (in stock).
      commerce_stock_cart_state_validate($form_id, $form, $form_state);
    }
  }
  elseif (strpos($form_id, "views_form_{$commerce_cart_form}_") === 0) {
    $view = reset($form_state['build_info']['args']);

    // Only alter buttons if the cart form View shows line items.
    if (!empty($view->result)) {

      // Add validate function to the cart form.
      if (empty($form['actions']['submit']['#validate'])) {
        $form['actions']['submit']['#validate'] = array_merge($form['#validate'], array(
          'commerce_stock_form_commerce_cart_validate',
        ));
      }
      else {
        $form['actions']['submit']['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
      }
      if (empty($form['actions']['checkout']['#validate'])) {
        $form['actions']['checkout']['#validate'] = array_merge($form['#validate'], array(
          'commerce_stock_form_commerce_cart_validate',
        ));
      }
      else {
        $form['actions']['checkout']['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
      }
    }
  }
  elseif ($form_id == 'commerce_checkout_form_checkout') {

    // Add validate function to the checkout form.
    $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
  }
  elseif ($form_id == 'commerce_checkout_form_review') {

    // Add validate function to the review form.
    // @todo: would be good to prompt the user with some contextual info
    // as he was about to pay.
    $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
  }
}

/**
 * Implements hook_commerce_checkout_pane_info().
 *
 * This creates the stock checkout pane. It should be placed on the first stage
 * of checkout. It checks if all items are in stock and if not redirects the
 * user to their cart.
 */
function commerce_stock_commerce_checkout_pane_info() {
  $checkout_panes = array();
  $checkout_panes['stock_validation_checkout_pane'] = array(
    'title' => t('check if all items are in stock at checkout'),
    'base' => 'commerce_stock_commerce_checkout_pane',
    'page' => 'checkout',
    'fieldset' => FALSE,
  );
  return $checkout_panes;
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * For products with options (product dropdown) checks if the add to cart form
 * should be enabled (in stock).
 *
 * @see commerce_cart_add_to_cart_form()
 */
function commerce_stock_cart_state_validate_options($form_id, &$form, &$form_state) {
  $product_id = $form['product_id']['#default_value'];
  $product = commerce_product_load($product_id);
  $qty_ordered = commerce_stock_check_cart_product_level($product_id);

  // Initialize the form.
  $form['submit']['#value'] = $form['submit']['#value'];
  $form['submit']['#disabled'] = FALSE;
  $form['#attributes']['class']['stock'] = 'in-stock';

  // Set global form for stock actions.
  // @codingStandardsIgnoreLine
  global $stock_cart_check_data;
  $stock_cart_check_data = array(
    'form' => &$form,
  );

  // Integration with rules_form_alter().
  if (module_exists('rules_form_alter')) {

    // Make sure rules_form_alter actions work from the stock event.
    $rules_form_alter_data =& drupal_static('rules_form_alter_data', array());

    // Set the form data that will be used by rules.
    $rules_form_alter_data['id'] = $form_id;
    $rules_form_alter_data['form'] =& $form;
    $rules_form_alter_data['state'] =& $form_state;
  }

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered, $form);
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * For product display with one product or attributes. Validates the product and
 * quantity to add to the cart. Also checks if the add to cart form should be
 * enabled (in stock).
 *
 * @see commerce_cart_add_to_cart_form()
 */
function commerce_stock_add_to_cart_validate($form, &$form_state) {
  if ($form_state['submitted']) {

    // Get product and quantity.
    $qty = $form_state['values']['quantity'];
    $product_id = $form_state['values']['product_id'];
    $product = commerce_product_load($product_id);
    $qty_ordered = commerce_stock_check_cart_product_level($product_id);

    // Check using rules.
    commerce_stock_check_product_rule($product, $qty, $qty_ordered, $stock_state, $message);

    // Action.
    if ($stock_state == 1) {
      form_set_error("stock_error", $message);
    }
    elseif ($stock_state == 2) {
      $form_state['values']['quantity'] = $qty;
      drupal_set_message($message);
    }
  }
}

/**
 * Form validation handler for views_form_commerce_cart_form_default().
 *
 * Checks each line item to make sure that they have not requested more items
 * than are in stock.
 */
function commerce_stock_form_commerce_cart_validate($form, &$form_state) {
  $line_item_index = array_keys($form_state['line_items']);
  if (isset($form_state['input']['edit_quantity'])) {
    $products = array();
    foreach ($form_state['values']['edit_quantity'] as $index => $qty) {
      $line_item = $form_state['line_items'][$line_item_index[$index]];
      $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
      if (in_array($line_item_wrapper
        ->getBundle(), commerce_product_line_item_types())) {
        $product_id = $line_item_wrapper->commerce_product->product_id
          ->value();
        $products[$product_id]['qty'] = isset($products[$product_id]) ? $products[$product_id]['qty'] + $qty : $qty;
        $products[$product_id]['line'] = $index;
      }
    }
    foreach ($products as $product_id => $product) {
      $prod = commerce_product_load($product_id);

      // Check using rules.
      commerce_stock_check_product_checkout_rule($prod, $product['qty'], $stock_state, $message);

      // @todo: TEST and update error structure.
      if ($stock_state == 1) {
        form_set_error('edit_quantity][' . $product['line'], $message);
      }
      elseif ($stock_state == 2) {
        drupal_set_message($message);
      }
    }
  }
}

/**
 * Form validation handler for commerce_checkout_form_checkout().
 *
 * Make sure all items in the cart are in stock before continuing. This should
 * not be reached as this is now handled by the stock checkout pane, but as that
 * can be disabled it may be safe to keep this extra check.
 */
function commerce_stock_checkout_form_validate($form, &$form_state) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
  commerce_stock_checkout_validate($order_wrapper);
}

/**
 * Form constructor for the stock checkout pane form.
 *
 * Validating the stock when displaying this form will allow redirecting the
 * user before they start checkout.
 */
function commerce_stock_commerce_checkout_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
  commerce_stock_checkout_validate($order_wrapper);
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * Checks if the add to cart form should be enabled (in stock).
 */
function commerce_stock_cart_state_validate($form_id, &$form, &$form_state) {
  $product_id = $form['product_id']['#value'];
  $product = commerce_product_load($product_id);
  $qty_ordered = commerce_stock_check_cart_product_level($product_id);

  // Initialize the form.
  $form['submit']['#value'] = $form['submit']['#value'];
  $form['submit']['#disabled'] = FALSE;
  $form['#attributes']['class']['stock'] = 'in-stock';

  // @codingStandardsIgnoreLine
  global $stock_cart_check_data;
  $stock_cart_check_data = array(
    'form' => &$form,
  );

  // Integration with rules_form_alter().
  if (module_exists('rules_form_alter')) {

    // Make sure rules_form_alter actions work from the stock event.
    $rules_form_alter_data =& drupal_static('rules_form_alter_data', array());

    // Set the form data that will be used by rules.
    $rules_form_alter_data['id'] = $form_id;
    $rules_form_alter_data['form'] =& $form;
    $rules_form_alter_data['state'] =& $form_state;
  }

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered);
}

/**
 * Implements hook_token_info().
 */
function commerce_stock_token_info() {
  $info['tokens']['commerce-product']['user-quantity-ordered'] = array(
    'name' => t('Quantity already ordered'),
    'description' => t('The quantity already ordered (in the basket) for the user'),
  );
  return $info;
}

/**
 * Implements hook_tokens().
 */
function commerce_stock_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'commerce-product' && !empty($data['commerce-product'])) {
    $product = entity_metadata_wrapper('commerce_product', $data['commerce-product']);
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'user-quantity-ordered':
          $replacements[$original] = commerce_stock_check_cart_product_level($product->product_id
            ->value());
          break;
      }
    }
  }
  return $replacements;
}

/**
 * Checks and returns quantity of the product and returns the value.
 *
 * The value is cached as is called more then once (including tokens)
 */
function commerce_stock_check_cart_product_level($product_id) {

  // Cart product levels will be cached keyed by $product_id.
  $cart_product_levels =& drupal_static(__FUNCTION__);
  if (isset($cart_product_levels[$product_id])) {
    return $cart_product_levels[$product_id];
  }

  // First let other modules attempt to provide a valid quantity for the given
  // product id. Instead of invoking
  // hook_commerce_stock_check_cart_product_level()
  // directly, we invoke it in each module implementing the hook and return the
  // first valid quantity returned (if any).
  foreach (module_implements('commerce_stock_check_cart_product_level') as $module) {
    $cart_qty = module_invoke($module, 'commerce_stock_check_cart_product_level', $product_id);

    // If a hook said the product should not have a cart product level, that
    // overrides any other potentially valid cart quantity. Return 0 now.
    if ($cart_qty === FALSE) {
      $cart_product_levels[$product_id] = 0;
      return 0;
    }

    // Otherwise only return a valid cart quantity.
    if (!empty($cart_qty) && is_numeric($cart_qty)) {
      $cart_product_levels[$product_id] = $cart_qty;
      return $cart_qty;
    }
  }
  $cart_qty = 0;
  global $user;

  // Load the current cart if it exists.
  $order = commerce_cart_order_load($user->uid);
  if (!$order) {
    $cart_qty = 0;
  }
  else {
    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);

    // Cycle throw each line item ID.
    foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
      if (in_array($line_item_wrapper
        ->getBundle(), commerce_product_line_item_types())) {
        if ($line_item_wrapper->commerce_product->product_id
          ->value() == $product_id) {
          $cart_qty += $line_item_wrapper->quantity
            ->value();
        }
      }
    }
  }
  $cart_product_levels[$product_id] = $cart_qty;
  return $cart_qty;
}

/**
 * Check the stock using rules.
 *
 * Invokes the rule event and return the result of its action.
 */
function commerce_stock_check_product_rule($product, &$qty, $qty_ordered, &$stock_state, &$message) {

  // Set defaults to the global stock check array.
  // @codingStandardsIgnoreLine
  global $stock_check_data;
  $stock_check_data = array(
    'state' => '0',
    'message' => '',
    'qty' => $qty,
  );

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_add_to_cart_check_product', $product, $qty, $qty_ordered, $qty + $qty_ordered);

  // If state not ok, do nothing then return the value set by the action.
  if ($stock_check_data['state'] != 0) {
    $stock_state = $stock_check_data['state'];
    $message = $stock_check_data['message'];
    $qty = $stock_check_data['qty'];
  }
}

/**
 * Check stock using rules at the point of checkout.
 *
 * Invoke the rule event and return the result of its action.
 */
function commerce_stock_check_product_checkout_rule($product, $qty_ordered, &$stock_state, &$message) {

  // Set defaults to the global stock check array.
  // @codingStandardsIgnoreLine
  global $stock_check_data;
  $stock_check_data = array(
    'state' => '0',
    'message' => '',
    'qty' => $qty_ordered,
  );

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_product_checkout', $product, $qty_ordered);

  // Set return values.
  $stock_state = $stock_check_data['state'];
  $message = $stock_check_data['message'];
}

/**
 * Form after_build handler: Validates that the product is in stock.
 */
function commerce_stock_form_after_build($form, &$form_state) {
  $prod_id = $form['product_id']['#value'];
  if (isset($form['product_id']['#stock_enabled']) && isset($form['product_id']['#stock_enabled'][$prod_id]) && $form['product_id']['#stock_enabled'][$prod_id]) {
    if (isset($form['product_id']['#stock']) && isset($form['product_id']['#stock'][$prod_id])) {
      $prod_stock = $form['product_id']['#stock'][$prod_id];
      if ($prod_stock <= 0) {

        // Remove the add to cart button.
        $form['submit']['#access'] = FALSE;

        // Remove quantity if enabled.
        if (isset($form['submit'])) {
          $form['quantity']['#access'] = FALSE;
        }
      }
    }
  }
  return $form;
}

/**
 * Common stock validation function.
 */
function commerce_stock_checkout_validate($order_wrapper) {
  $found_errors = FALSE;

  // Check each line item.
  foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
    if (in_array($line_item_wrapper
      ->getBundle(), commerce_product_line_item_types())) {
      $product_id = $line_item_wrapper->commerce_product->product_id
        ->value();
      $product = commerce_product_load($product_id);
      $qty_ordered = commerce_stock_check_cart_product_level($product_id);

      // Check using rules.
      commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);

      // @todo: TEST and update error structure
      if ($stock_state == 1) {
        form_set_error("stock_error", $message);
        $found_errors = TRUE;
      }
      elseif ($stock_state == 2) {
        drupal_set_message($message);
      }
    }
  }

  // If out of stock items send back to the cart form.
  if ($found_errors) {
    drupal_set_message(t('Please adjust quantities before continuing to checkout.'));
    $cart_url = url('cart', array(
      'absolute' => TRUE,
    ));
    drupal_goto($cart_url);
  }
}

/**
 * Determine whether an order has items which are out of stock.
 *
 * @return bool
 *   TRUE if the order has items which are out of stock, FALSE otherwise.
 */
function commerce_stock_order_has_out_of_stock($order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $outofstock = FALSE;

  // Check each line item.
  foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
    if (in_array($line_item_wrapper
      ->getBundle(), commerce_product_line_item_types())) {
      $product_id = $line_item_wrapper->commerce_product->product_id
        ->value();
      $product = commerce_product_load($product_id);
      $qty_ordered = commerce_stock_check_cart_product_level($product_id);

      // Check using rules.
      commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);

      // Both 1 and 2 are errors.
      if ($stock_state == 1 || $stock_state == 2) {
        $outofstock = TRUE;
        break;
      }
    }
  }
  return $outofstock;
}

/**
 * A demo action for the "Advanced configuration of the add to cart form".
 *
 * Demonstrates how you can write your own custom actions to handle the add to
 * cart.
 */
function commerce_stock_test_cart_action($form, &$form_state) {
  $product_id = $form_state['values']['product_id'];
  $product = commerce_product_load($product_id);
  drupal_set_message(t('%title was not added to your cart as this is a test action only.', array(
    '%title' => $product->title,
  )), 'error');

  // Ensure that page redirects back to its original URL without losing query
  // parameters, such as pagers.
  // @todo Remove when http://drupal.org/node/171267 is fixed.
  $form_state['redirect'] = array(
    current_path(),
    array(
      'query' => drupal_get_query_parameters(),
    ),
  );
}

Functions

Namesort descending Description
commerce_stock_add_to_cart_validate Form validation handler for commerce_cart_add_to_cart_form().
commerce_stock_cart_state_validate Form validation handler for commerce_cart_add_to_cart_form().
commerce_stock_cart_state_validate_options Form validation handler for commerce_cart_add_to_cart_form().
commerce_stock_checkout_form_validate Form validation handler for commerce_checkout_form_checkout().
commerce_stock_checkout_validate Common stock validation function.
commerce_stock_check_cart_product_level Checks and returns quantity of the product and returns the value.
commerce_stock_check_product_checkout_rule Check stock using rules at the point of checkout.
commerce_stock_check_product_rule Check the stock using rules.
commerce_stock_commerce_checkout_pane_checkout_form Form constructor for the stock checkout pane form.
commerce_stock_commerce_checkout_pane_info Implements hook_commerce_checkout_pane_info().
commerce_stock_form_after_build Form after_build handler: Validates that the product is in stock.
commerce_stock_form_alter Implements hook_form_alter().
commerce_stock_form_commerce_cart_validate Form validation handler for views_form_commerce_cart_form_default().
commerce_stock_menu Implements hook_menu().
commerce_stock_order_has_out_of_stock Determine whether an order has items which are out of stock.
commerce_stock_permission Implements hook_permission().
commerce_stock_test_cart_action A demo action for the "Advanced configuration of the add to cart form".
commerce_stock_tokens Implements hook_tokens().
commerce_stock_token_info Implements hook_token_info().