You are here

commerce_stock.module in Commerce Stock 7

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

Allow commerce products to have stock levels associated with their SKU

Commerce Stock enables Commerce to manage stock for products. Store admins can set the stock levels and a threshold on a product edit page. When that threshold is reached admins can be optionally notified about the current stock level. Store admins can view all stock levels using views.

File

commerce_stock.module
View source
<?php

/**
 * @file
 * Allow commerce products to have stock levels associated with their SKU
 *
 * Commerce Stock enables Commerce to manage stock for products. Store admins
 * can set the stock levels and a threshold on a product edit page.
 * When that threshold is reached admins can be optionally notified about the
 * current stock level. Store admins can view all stock levels using views.
 */

/**
 * 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',
  );
  return $items;
}

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

/**
 * 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) {
  if (strpos($form_id, "commerce_cart_add_to_cart_form") === 0) {
    $stock = array();
    $stock_enabled = array();

    // 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'])) {
      $options = $form['product_id']['#options'];

      // Stock validation (for options):
      // Stock validation only true if stock field exist
      // @todo: for version 2 we need a better way to determine if
      // stock enabled and not rely on the existence of the stock field
      $stock_validation = FALSE;
      foreach ($options as $key => $value) {
        $product = commerce_product_load($key);
        $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
        if (!empty($product_wrapper->commerce_stock)) {
          $stock_validation = TRUE;

          // Set stock array
          $stock[$key] = $product_wrapper->commerce_stock
            ->value();

          // set stock enabled array
          if (isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
            ->value() == 1) {
            $stock_enabled[$key] = FALSE;
          }
          else {
            $stock_enabled[$key] = TRUE;
          }
        }
        else {

          // stock is disabled or the product has no value (we will handle it as 0)
          // @todo if stock field had a default of 0 this wont be needed
          $stock_enabled[$key] = TRUE;
          $stock[$key] = 0;
        }
      }

      // Stock validation actions (for options):
      if ($stock_validation) {

        // set "out of stock"  Message
        foreach ($options as $key => $value) {
          if ($stock_enabled[$key] && $stock[$key] <= 0) {
            $form['product_id']['#options'][$key] .= ' - ' . t('Out of Stock');
          }
        }

        // Set validation
        $form['#after_build'][] = 'commerce_stock_form_after_build';
        $form['product_id']['#element_validate'][] = 'commerce_stock_option_validate';
        $form['product_id']['#stock'] = $stock;
        $form['product_id']['#stock_enabled'] = $stock_enabled;
      }
    }
    elseif (isset($form['product_id']['#value'])) {
      $product = commerce_product_load($form['product_id']['#value']);
      $product_wrapper = entity_metadata_wrapper('commerce_product', $product);

      // Add validation to the add to cart
      $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
      if (isset($product_wrapper->commerce_stock)) {
        if (!(isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
          ->value() == 1)) {
          if ($product_wrapper->commerce_stock
            ->value() <= 0) {
            $form['submit']['#value'] = t('Out of Stock');
            $form['submit']['#disabled'] = TRUE;
            $form['submit']['#attributes'] = array(
              'class' => array(
                'out-of-stock',
              ),
            );
          }
        }
      }
    }
  }
  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().
 *
 * Creates the stock checkout pane:
 * Should be placed on the first stage of checkout.
 * when loads checks if all items in stock and if not redirects
 * you to the 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 validate handler: validate checkout form.
 *
 * Make sure all items in the cart are in stock
 */
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);
}

/***[ pane validation ]****/

/**
 * The stock checkout pane form: call stock the validation when
 * displaying the form this will alow us to redirect the user
 * before he starts 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);
}
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);
      $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
      if (!(isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
        ->value() == 1)) {
        $desired_purchase = $line_item_wrapper->quantity
          ->value();
        if (commerce_stock_product_check_out_of_stock($product_id, $desired_purchase, $remaining_stock)) {
          form_set_error("out_of_stock_{$index}", t('The maximum quantity of %title that can be purchased is %max.', array(
            '%title' => $product->title,
            '%max' => $remaining_stock,
          )));
          $found_errors = TRUE;
        }
      }
    }
  }

  // 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);
  }
}

/**
 * Form validate handler: validate checkout form.
 *
 * Make sure all items in the cart are in stock
 */
function commerce_stock_add_to_cart_validate($form, &$form_state) {
  $qty = $form_state['input']['quantity'];
  $product_id = $form_state['input']['product_id'];
  $product = commerce_product_load($product_id);
  $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
  $qty_ordered = commerce_stock_check_cart_product_level($product_id);
  $qty_total = $qty + $qty_ordered;
  if (!(isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
    ->value() == 1)) {
    if (commerce_stock_product_check_out_of_stock($product_id, $qty_total, $remaining_stock)) {
      form_set_error("stock_error", t('The maximum quantity of %title that can be purchased is %max.', array(
        '%title' => $product->title,
        '%max' => $remaining_stock,
      )));
    }
  }
}

/**
 * Form after_build handler: validate 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;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Alters the cart form to provide a validator for it.
 */
function commerce_stock_form_views_form_commerce_cart_form_default_alter(&$form, &$form_state) {
  $form['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
}

/**
 * Form validator function for cart form.
 *
 * Checks each line item to make sure that they have not requested more items
 * than we have in stock.
 */
function commerce_stock_form_commerce_cart_validate($form, &$form_state) {

  // if user chose to remove an item then valid
  if ($form_state['triggering_element']['#value'] == t('Remove')) {
    return;
  }
  $line_item_index = array_keys($form_state['line_items']);
  foreach ($form_state['input']['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();
      $product = commerce_product_load($product_id);
      $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
      if (!(isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
        ->value() == 1)) {
        if (commerce_stock_product_check_out_of_stock($product_id, $qty, $remaining_stock)) {
          form_set_error("edit_quantity][{$index}", t('The maximum stock of %title that can be purchased is %max.', array(
            '%title' => $product->title,
            '%max' => $remaining_stock,
          )));
        }
      }
    }
  }
}

/**
 * Form validate handler: validate the product and quantity to add to the cart.
 */
function commerce_stock_option_validate($element, &$form_state) {

  // validate the stock level
  // @todo we really dont need to cycle and load all the products
  // only $form_state['values']['product_id']
  $options = $element['#options'];
  foreach ($options as $key => $value) {
    $product = commerce_product_load($key);
    $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
    if (isset($product_wrapper->commerce_stock)) {

      // if stock override set stock to be the same as quantity so will validate
      if (isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
        ->value() == 1) {
        $element['#stock'][$key] = $form_state['values']['quantity'];
        continue;
      }
    }
  }
  $stock = $element['#stock'][$form_state['values']['product_id']];
  $stock_enabled = $element['#stock_enabled'][$form_state['values']['product_id']];
  if ($stock < 1 && $stock_enabled) {
    form_error($element, t('Product is out of stock'));
  }
  elseif ($stock_enabled) {
    $product_id = $form_state['values']['product_id'];
    $qty = $form_state['values']['quantity'];
    $qty_ordered = commerce_stock_check_cart_product_level($product_id);
    if ($qty + $qty_ordered > $stock) {
      form_error($element, t("You can only order a maximum of @stock for this item.", array(
        '@stock' => $stock,
      )));
    }
  }
}

/**
 * Determine whether an order has items which are out of stock.
 *
 * @param $order
 *   An order object
 *
 * @return
 *   Boolean: TRUE if the order has items which are out of stock and
 *   FALSE otherwise.
 */
function commerce_stock_order_has_out_of_stock($order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  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);
      $qry = $line_item_wrapper->quantity
        ->value();
      if (commerce_stock_product_check_out_of_stock($product_id, $qry, $remaining_stock) != FALSE) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/**
 * Check whether a purrchase quantity exceeds what is available.
 *
 * @param $product_id
 *   The numeric product ID
 * @param $quantity
 *   Quantity to be purchased.
 * @param $remaining_stock
 *   the level of stock that can be ordered if
 *   commerce_stock_override is not enabled will not be less then 0.
 *
 * @return
 *   FALSE if the product request can be satisfied.
 *   The number available otherwise.
 */
function commerce_stock_product_check_out_of_stock($product_id, $quantity, &$remaining_stock) {
  $product = commerce_product_load($product_id);
  $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
  if (isset($product_wrapper->commerce_stock)) {
    if (isset($product_wrapper->commerce_stock_override) && $product_wrapper->commerce_stock_override
      ->value() == 1) {
      return FALSE;
    }

    // how many do we have for sale, cant be less than zero
    $remaining_stock = $product_wrapper->commerce_stock
      ->value();
    if ($remaining_stock < 0) {
      $remaining_stock = 0;
    }
    if ($remaining_stock < $quantity) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  else {
    return FALSE;
  }
}

/**
 * Ensures a stock field is present on a product type bundle.
 */
function commerce_stock_configure_product_type($type) {
  commerce_stock_create_instance('commerce_stock', 'number_integer', TRUE, 'commerce_product', $type, t('Stock'));
}

/**
 * Creates a required instance of a stock field on the specified bundle.
 *
 * @param $field_name
 *   The name of the field; if it already exists, a new instance of the existing
 *   field will be created. For fields governed by the Commerce modules, this
 *   should begin with commerce_.
 * @param $entity_type
 *   The type of entity the field instance will be attached to.
 * @param $bundle
 *   The bundle name of the entity the field instance will be attached to.
 * @param $label
 *   The label of the field instance.
 * @param $weight
 *   The default weight of the field instance widget and display.
 */
function commerce_stock_create_instance($field_name, $field_type, $required, $entity_type, $bundle, $label, $description = NULL, $weight = 0) {

  // If a field type we know should exist isn't found, clear the Field cache.
  //  if (!field_info_field_types('commerce_stock')) {
  //    field_cache_clear();
  //  }
  // Look for or add the specified stock field to the requested entity bundle.
  $field = field_info_field($field_name);
  $instance = field_info_instance($entity_type, $field_name, $bundle);
  if (empty($field)) {
    $field = array(
      'field_name' => $field_name,
      'type' => $field_type,
      'cardinality' => 1,
      'entity_types' => array(
        $entity_type,
      ),
      'translatable' => FALSE,
      'locked' => FALSE,
    );
    if ($field_type == 'list_boolean') {
      $field['settings'] = array(
        'allowed_values' => array(
          0,
          1,
        ),
        'allowed_values_function' => '',
      );
    }
    $field = field_create_field($field);
  }
  if (empty($instance)) {
    $instance = array(
      'field_name' => $field_name,
      'entity_type' => $entity_type,
      'bundle' => $bundle,
      'label' => $label,
      'required' => $required,
      'settings' => array(),
      'display' => array(),
      'description' => $description,
      'default_value' => array(
        array(
          'value' => "0",
        ),
      ),
    );
    if ($field_type == 'list_boolean') {
      $instance['widget'] = array(
        'type' => 'options_onoff',
        'settings' => array(
          'display_label' => TRUE,
        ),
      );
    }
    $entity_info = entity_get_info($entity_type);

    // Spoof the default view mode so its display type is set.
    $entity_info['view modes']['default'] = array();
    field_create_instance($instance);
  }
}

/**
 * Given a product type, determine whether stock management is enabled on that
 * product type.
 *
 * @param $type
 *   The product type.
 *
 * @return
 *   Boolean: TRUE if stock management is enabled.
 */
function commerce_stock_product_type_enabled($type) {
  $instance = field_info_instance('commerce_product', 'commerce_stock', $type);
  return !empty($instance);
}

/**
 * Given a product type, determine whether stock management override is enabled
 * on that product type.
 *
 * @param $type
 *   The product type.
 *
 * @return
 *   Boolean: TRUE if stock management override is enabled.
 */
function commerce_stock_product_type_override_enabled($type) {
  $instance = field_info_instance('commerce_product', 'commerce_stock_override', $type);
  return !empty($instance);
}

/**
 * Given a product, determine whether stock management is enabled for that
 * product.
 *
 * @param $product
 *   The product to check.
 *
 * @return
 *   Boolean: TRUE if stock management is enabled on the product's product type.
 */
function commerce_stock_product_enabled($product) {
  return commerce_stock_product_type_enabled($product->type);
}

/**
 * Given a line item, determine whether stock management is enabled for that
 * line item.
 *
 * @param $line_item
 *   The line item to check.
 *
 * @return
 *   Boolean: TRUE if stock management is enabled on the product's product type.
 */
function commerce_stock_line_item_product_enabled($line_item) {
  return commerce_stock_product_type_enabled($line_item->product->type);
}

/**
 *
 * check if product is in the cart and return the quentity if it is
 *
 */
function commerce_stock_check_cart_product_level($product_id) {
  $cart_qty = 0;
  global $user;

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

    // 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();
        }
      }
    }
  }
  return $cart_qty;
}

Functions

Namesort descending Description
commerce_stock_add_to_cart_validate Form validate handler: validate checkout form.
commerce_stock_checkout_form_validate Form validate handler: validate checkout form.
commerce_stock_checkout_validate
commerce_stock_check_cart_product_level check if product is in the cart and return the quentity if it is
commerce_stock_commerce_checkout_pane_checkout_form The stock checkout pane form: call stock the validation when displaying the form this will alow us to redirect the user before he starts checkout
commerce_stock_commerce_checkout_pane_info Implements hook_commerce_checkout_pane_info().
commerce_stock_configure_product_type Ensures a stock field is present on a product type bundle.
commerce_stock_create_instance Creates a required instance of a stock field on the specified bundle.
commerce_stock_form_after_build Form after_build handler: validate the product is in stock.
commerce_stock_form_alter Implements hook_form_alter().
commerce_stock_form_commerce_cart_validate Form validator function for cart form.
commerce_stock_form_views_form_commerce_cart_form_default_alter Implements hook_form_FORM_ID_alter().
commerce_stock_line_item_product_enabled Given a line item, determine whether stock management is enabled for that line item.
commerce_stock_menu Implements hook_menu().
commerce_stock_option_validate Form validate handler: validate the product and quantity to add to the cart.
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_product_check_out_of_stock Check whether a purrchase quantity exceeds what is available.
commerce_stock_product_enabled Given a product, determine whether stock management is enabled for that product.
commerce_stock_product_type_enabled Given a product type, determine whether stock management is enabled on that product type.
commerce_stock_product_type_override_enabled Given a product type, determine whether stock management override is enabled on that product type.