You are here

uc_out_of_stock.module in Ubercart Out of stock Notification 6.2

File

uc_out_of_stock.module
View source
<?php

define('UC_OUT_OF_STOCK_DEFAULT_HTML', t('<span style="color: red;">Out of stock</span>'));

/****************************
 * Drupal hooks
 ***************************/

/**
 * Implementation of hook_form_alter()
 */
function uc_out_of_stock_form_alter(&$form, $form_state, $form_id) {
  $forms = array(
    'uc_product_add_to_cart_form',
    'uc_catalog_buy_it_now_form',
  );
  foreach ($forms as $id) {
    if (substr($form_id, 0, strlen($id)) == $id) {
      drupal_add_js(drupal_get_path('module', 'uc_out_of_stock') . '/uc_out_of_stock.js');
      drupal_add_css(drupal_get_path('module', 'uc_out_of_stock') . '/uc_out_of_stock.css');
      $form['#validate'][] = 'uc_out_of_stock_validate_form_addtocart';
    }
  }
  if ($form_id == 'uc_cart_view_form') {
    $form['#validate'][] = 'uc_out_of_stock_validate_form_cart';
  }
  if ($form_id == 'uc_cart_checkout_form' || $form_id == 'uc_cart_checkout_review_form') {
    $form['#validate'][] = 'uc_out_of_stock_validate_form_checkout';
  }
}

/**
 * Implementation of hook_menu()
 */
function uc_out_of_stock_menu() {
  $items = array();
  $items['admin/store/settings/uc_out_of_stock'] = array(
    'title' => 'Out of Stock Settings',
    'access arguments' => array(
      'administer store',
    ),
    'description' => 'Configure out of stock settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_out_of_stock_settings',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['uc_out_of_stock/query'] = array(
    'title' => 'stock query',
    'page callback' => 'uc_out_of_stock_query',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/****************************
 * Helper functions
 ***************************/

/**
 * Helper function to retrieve stock information directly from the stock table
 * querying it by model
 *
 * @param string $model
 * @return mixed
 *   Return an array of the stock information or null if NONE
 */
function uc_out_of_stock_getstockinfo_from_model($model) {
  $stock = db_result(db_query("SELECT ups.stock FROM {uc_product_stock} ups WHERE ups.active = 1 AND ups.sku = '%s'", $model));
  if (is_numeric($stock)) {
    $stockinfo['stock'] = $stock;
    $stockinfo['model'] = $model;
  }
  return $stockinfo;
}

/**
 * Helper function to retrieve stock information looked up by nid and attributes
 *
 * @param integer $nid
 * @param array   $attrs
 * @return mixed
 *   Return an array of the stock information or null if NONE
 */
function uc_out_of_stock_getstockinfo($nid, $attrs) {

  // Query main sku is true by default, if some attribute combination is found,
  // it will be set to FALSE
  // If the combination was not found, and all attributes were indeed selected, we are assuming that some
  // combination shares an SKU with the actual product, thus, the product have to be queried as well.
  $query_main_sku = TRUE;
  if (module_exists('uc_attribute')) {

    // if attributes module exists, and product has attributes first search for attributes
    $post_attrs = count($attrs);
    $sql = "SELECT %s FROM {uc_product_adjustments} upa LEFT JOIN {uc_product_stock} ups ON ups.sku = upa.model WHERE upa.nid = %d";
    $db_attrs = db_result(db_query($sql, 'COUNT(*)', $nid));
    if ($post_attrs && $db_attrs > 0) {
      $result = db_query($sql, '*', $nid);
      while ($row = db_fetch_object($result)) {
        $combination = unserialize($row->combination);

        // Apparently, on D6, one entry of the stock table has always the main SKU regardless the adjustments settings
        // Therefor, if the join gives a null record for the stock table, the main sku will be queried as well
        if (isset($row->sku) && $combination == $attrs) {

          // Because a combination is found, don't look for the main SKU
          $query_main_sku = FALSE;

          // Only check if active
          if ($row->active) {
            $stockinfo['stock'] = $row->stock;
            $stockinfo['model'] = $row->model;
          }
        }
      }
    }
    else {

      // If there are attributes for the product, but no attributes were sent, do nothing
      // as it's probably coming from the catalog table list view and we can't
      // disable the add to cart button for products with attributes
      if ($post_attrs == 0 && $db_attrs > 0) {
        $query_main_sku = FALSE;
      }
    }
  }
  if ($query_main_sku) {

    // seach for main product
    $result = db_query("SELECT * FROM {uc_products} up LEFT JOIN {uc_product_stock} ups ON ups.sku = up.model WHERE up.nid = %d AND ups.active = 1", $nid);
    while ($row = db_fetch_object($result)) {
      $stockinfo['stock'] = $row->stock;
      $stockinfo['model'] = $row->model;
    }
  }
  return $stockinfo;
}
function uc_out_of_stock_query() {
  $response = array();
  $attrs = array();
  $nid = $_POST['nid'];
  foreach ($_POST as $key => $value) {
    if (substr($key, 0, 4) == 'attr') {
      $attrs[substr($key, 4)] = $value;
    }
  }
  $stockinfo = uc_out_of_stock_getstockinfo($nid, $attrs);
  if ($stockinfo) {
    $response['stock'] = $stockinfo['stock'];
    if ($response['stock'] <= 0) {
      $response['html'] = check_markup(variable_get('uc_out_of_stock_text', UC_OUT_OF_STOCK_DEFAULT_HTML), variable_get('uc_out_of_stock_format', FILTER_FORMAT_DEFAULT), FALSE);
    }
  }

  // if there is some response, print it
  if (count($response)) {
    print implode('|', $response);
  }
}
function uc_out_of_stock_settings() {
  $text = check_markup(variable_get('uc_out_of_stock_text', UC_OUT_OF_STOCK_DEFAULT_HTML), variable_get('uc_out_of_stock_format', FILTER_FORMAT_DEFAULT), FALSE);
  $description = t('<div class="description">This is the value below rendered as you would expect to see it</div>');
  $text = '<div style="border: 1px solid lightgrey; padding: 10px;">' . $text . '</div>' . $description;
  $form['uc_out_of_stock_demo'] = array(
    '#type' => 'markup',
    '#value' => $text,
  );
  $form['uc_out_of_stock_text'] = array(
    '#type' => 'textarea',
    '#title' => t('Out of stock replacement HTML'),
    '#default_value' => variable_get('uc_out_of_stock_text', UC_OUT_OF_STOCK_DEFAULT_HTML),
    '#description' => t('The HTML that will replace the Add To Cart button if no stock is available.'),
  );
  $form['uc_out_of_stock_format'] = filter_form(variable_get('uc_out_of_stock_format', FILTER_FORMAT_DEFAULT), NULL, array(
    'uc_out_of_stock_format',
  ));
  return system_settings_form($form);
}

/**
 * Shared logic for add to cart validation in both form validation and
 * hook_add_to_cart
 *
 * @param int $nid
 * @param array $attrs
 * @return mixed
 *   FALSE if no error
 *   Error message if error
 */
function uc_out_of_stock_validate_addtocart($nid, $attrs, $qty_add) {
  $error = FALSE;
  $stockinfo = uc_out_of_stock_getstockinfo($nid, $attrs);
  if ($stockinfo) {
    if ($stockinfo['stock'] <= 0) {
      $error = _uc_out_of_stock_get_error('out_of_stock', $nid, $attrs, $stockinfo['stock']);
    }
    else {
      $qty = 0;
      $items = uc_cart_get_contents();
      foreach ($items as $item) {
        if ($item->nid == $nid && $stockinfo['model'] == $item->model) {
          $qty += $item->qty;
        }
      }
      if ($stockinfo['stock'] - ($qty + $qty_add) < 0) {
        $error = _uc_out_of_stock_get_error('not_enough', $nid, $attrs, $stockinfo['stock'], $qty);
      }
    }
  }
  return $error;
}

/**
 * Validate the 'Add To Cart' form of each product preventing the addition of
 * out of stock items or more items that ones currently on stock.
 *
 * Support teaser view, full node view and catalog view
 */
function uc_out_of_stock_validate_form_addtocart($form, &$form_state) {
  $class = $form_state['clicked_button']['#attributes']['class'];

  // Uses the class of the add to cart button of both node view and catalog
  // view to decide if we should validate stock or not
  // i.e. If some other form_alter added another button, do nothing (uc_wishlist)
  if ($class == 'node-add-to-cart' || $class == 'list-add-to-cart') {
    $attrs = $form_state['values']['attributes'];
    $nid = $form_state['values']['nid'];
    $qty = $form_state['values']['qty'] ? $form_state['values']['qty'] : 1;
    $error = uc_out_of_stock_validate_addtocart($nid, $attrs, $qty);
    if ($error !== FALSE) {
      form_set_error('uc_out_of_stock', $error['msg']);
    }
  }
}

/**
 * Helper function that would validate items in the cart referenced in a form
 * Used in @uc_out_of_stock_validate_form_checkout
 * Used in @uc_out_of_stock_validate_form_cart
 *
 * @param array $items
 */
function uc_out_of_stock_validate_cart_items($items, $page = 'cart') {

  // just in the rare case (http://drupal.org/node/496782)
  // that $items is not an array, do nothing
  if (!is_array($items)) {
    return;
  }
  $cart_items = array();
  $stored_cart_items = uc_cart_get_contents();

  // First group by model
  foreach ($items as $k => $item) {

    // Convert it to object just in case is an array (if coming from a form POST)
    $item = (object) $item;

    // Validate unless the item was being removed
    // Starting from Ubercart 2.6, remove are buttons so th
    // I believe this clause should cope with both approaches.
    if (!is_numeric($item->remove) || !$item->remove) {

      // Unserialize data if string
      if (is_string($item->data)) {
        $item->data = unserialize($item->data);
      }

      // If the items comes from the submitted cart, it doesn't have the model
      // set, so we try to get it from the stored cart items which is filled
      // properly with the model.
      // For that, we assume that the sorting is the same, and if not,
      // we provide an alternative method which is probably not
      // very good in terms of performance, but the sorting of both arrays
      // should be the same
      if (!isset($item->model)) {
        $stored_item = $stored_cart_items[$k];
        if ($item->nid == $stored_item->nid && $item->data == $stored_item->data) {
          $model = $stored_item->model;
        }
        else {
          foreach ($stored_cart_items as $stored_item) {
            if ($item->nid == $stored_item->nid && $item->data == $stored_item->data) {
              $model = $stored_item->model;
            }
          }
        }
        $item->model = $model;
      }
      $cart_items[$item->model]['item'] = $item;
      $cart_items[$item->model]['qty'] += $item->qty;
      $cart_items[$item->model]['key'] = $k;
    }
  }

  // Now for each model, check the stock
  foreach ($cart_items as $model => $cart_item) {
    $item = $cart_item['item'];
    $stockinfo = uc_out_of_stock_getstockinfo_from_model($model);
    if ($stockinfo) {
      if ($stockinfo['stock'] - $cart_item['qty'] < 0) {
        $qty = 0;
        if ($page == 'checkout') {
          $qty = $cart_item['qty'];
        }
        $error = _uc_out_of_stock_get_error('not_enough', $item->nid, $item->data['attributes'], $stockinfo['stock'], $qty);
        form_set_error("items][{$cart_item['key']}][qty", $error['msg']);
      }
    }
  }
}

/**
 * Validate the 'Order Checkout' and 'Order Review' form preventing the order
 * going through if the stock information have changed while the user was
 * browsing the site. (i.e. some other client has just bought the same item)
 */
function uc_out_of_stock_validate_form_checkout($form, &$form_state) {
  $items = uc_cart_get_contents();
  uc_out_of_stock_validate_cart_items($items, 'checkout');
}

/**
 * Validate the 'Shopping cart' form preventing the addition of more items that
 * the ones currently in stock.
 */
function uc_out_of_stock_validate_form_cart($form, &$form_state) {
  $items = $form_state['values']['items'];
  uc_out_of_stock_validate_cart_items($items, 'cart');
}

/**
 * Helper function to properly format the form error messages across the
 * different validation hooks.
 *
 * @param <type> $error
 *   type of error
 * @param <type> $nid
 *   node id
 * @param <type> $attrs
 *   attributes array
 * @param <type> $stock
 *   stock of the current item
 * @param <type> $qty
 *  qty in cart
 * @param <type> $cart_item_id
 *   ID on the shopping cart
 */
function _uc_out_of_stock_get_error($type, $nid, $attrs, $stock, $qty = 0) {
  $product = node_load($nid);
  if (count($attrs)) {
    foreach ($attrs as $attr_id => $option_id) {
      $attr = uc_attribute_load($attr_id);
      $option = uc_attribute_option_load($option_id);
      $attr_names[] = $attr->name;
      $option_names[] = $option->name;
    }
  }
  $error['stock'] = $stock;
  $error['qty_in_cart'] = $qty;
  $error['type'] = $type;
  $error['product'] = $product->title;
  if (count($attrs)) {
    $error['attributes'] = implode('/', $attr_names);
    $error['options'] = implode('/', $option_names);
  }
  if ($type == 'out_of_stock') {
    if (count($attrs)) {
      $error['msg'] = t("We're sorry. The product @product (@options) is out of stock. Please consider trying this product with a different @attributes.", array(
        '@product' => $error['product'],
        '@attributes' => $error['attributes'],
        '@options' => $error['options'],
      ));
    }
    else {
      $error['msg'] = t("We're sorry. The product @product is out of stock.", array(
        '@product' => $error['product'],
      ));
    }
  }
  if ($type == 'not_enough') {
    if (count($attrs)) {
      $error['msg'] = t("We're sorry. We have now only @qty of the product @product (@options) left in stock.", array(
        '@qty' => format_plural($error['stock'], '1 unit', '@count units'),
        '@product' => $error['product'],
        '@options' => $error['options'],
      ));
    }
    else {
      $error['msg'] = t("We're sorry. We have now only @qty of the product @product left in stock.", array(
        '@qty' => format_plural($error['stock'], '1 unit', '@count units'),
        '@product' => $error['product'],
      ));
    }
    if ($error['qty_in_cart']) {
      $error['msg'] .= ' ' . t("You have currently @qty in your <a href=\"@cart-url\">shopping cart</a>.", array(
        '@qty' => format_plural($error['qty_in_cart'], '1 unit', '@count units'),
        '@cart-url' => url('cart'),
      ));
    }
  }
  return $error;
}

/**
 * NOTE: I am leaving this function for reference, due to how the hook is invoked
 * and worked out, not only from core but from other modules
 * I think it's better not to have a validation at this point
 * but just support as many modules as we can on form_alter
 *
 * Implementation of hook_add_to_cart()
 *
 * Further prevention of adding out of stock items in case the form validation
 * hooks don't apply.
 * i.e. using a module that uses add_to_cart() directly (uc_wishlist for example)
 */

/*
function uc_out_of_stock_add_to_cart($nid, $qty, $data) {
$error = uc_out_of_stock_validate_addtocart($nid, $data['attributes']);
if ($error !== FALSE) {
  $result[] = array(
     'success' => FALSE,
     'message' => $error,
   );
}
 return $result;
}
*
* /
*/

Functions

Namesort descending Description
uc_out_of_stock_form_alter Implementation of hook_form_alter()
uc_out_of_stock_getstockinfo Helper function to retrieve stock information looked up by nid and attributes
uc_out_of_stock_getstockinfo_from_model Helper function to retrieve stock information directly from the stock table querying it by model
uc_out_of_stock_menu Implementation of hook_menu()
uc_out_of_stock_query
uc_out_of_stock_settings
uc_out_of_stock_validate_addtocart Shared logic for add to cart validation in both form validation and hook_add_to_cart
uc_out_of_stock_validate_cart_items Helper function that would validate items in the cart referenced in a form Used in @uc_out_of_stock_validate_form_checkout Used in @uc_out_of_stock_validate_form_cart
uc_out_of_stock_validate_form_addtocart Validate the 'Add To Cart' form of each product preventing the addition of out of stock items or more items that ones currently on stock.
uc_out_of_stock_validate_form_cart Validate the 'Shopping cart' form preventing the addition of more items that the ones currently in stock.
uc_out_of_stock_validate_form_checkout Validate the 'Order Checkout' and 'Order Review' form preventing the order going through if the stock information have changed while the user was browsing the site. (i.e. some other client has just bought the same item)
_uc_out_of_stock_get_error Helper function to properly format the form error messages across the different validation hooks.

Constants