uc_out_of_stock.module in Ubercart Out of stock Notification 7
Same filename and directory in other branches
The Out of stock notification module file
It provides both client side and server side stock validation.
File
uc_out_of_stock.moduleView source
<?php
/**
* @file
* The Out of stock notification module file
*
* It provides both client side and server side stock validation.
*/
/**
* Implements of hook_form_alter()
*/
function uc_out_of_stock_form_alter(&$form, &$form_state, $form_id) {
static $settings_js = array();
$forms = array(
'uc_product_add_to_cart_form',
'uc_catalog_buy_it_now_form',
);
foreach ($forms as $id) {
if (drupal_substr($form_id, 0, drupal_strlen($id)) == $id) {
// Only add Javascript if it was enabled
if (!variable_get('uc_out_of_stock_disable_js', FALSE)) {
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');
if (empty($settings_js)) {
$settings_js['uc_out_of_stock']['path'] = url('uc_out_of_stock/query');
$settings_js['uc_out_of_stock']['throbber'] = variable_get('uc_out_of_stock_throbber', TRUE);
$settings_js['uc_out_of_stock']['instock'] = variable_get('uc_out_of_stock_instock', TRUE);
$uc_out_of_stock_text = variable_get('uc_out_of_stock_text', array(
'value' => '<span style="color: red;">Out of stock</span>',
'format' => NULL,
));
$settings_js['uc_out_of_stock']['msg'] = check_markup($uc_out_of_stock_text['value'], $uc_out_of_stock_text['format']);
drupal_add_js($settings_js, 'setting');
}
}
$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 Notification',
'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) {
$stockinfo = array();
$stock = db_query("SELECT ups.stock FROM {uc_product_stock} ups WHERE ups.active = 1 AND ups.sku = :sku", array(
':sku' => $model,
))
->fetchField();
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) {
$stockinfo = array();
// 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 = "FROM {uc_product_adjustments} upa LEFT JOIN {uc_product_stock} ups ON ups.sku = upa.model WHERE upa.nid = :nid";
$db_attrs = db_query('SELECT COUNT(*) ' . $sql, array(
':nid' => $nid,
))
->fetchField();
if ($post_attrs && $db_attrs > 0) {
$result = db_query('SELECT * ' . $sql, array(
':nid' => $nid,
));
foreach ($result as $row) {
$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 = :nid AND ups.active = 1", array(
':nid' => $nid,
));
foreach ($result as $row) {
$stockinfo['stock'] = $row->stock;
$stockinfo['model'] = $row->model;
}
}
return $stockinfo;
}
function uc_out_of_stock_query() {
if (!isset($_POST['form_ids']) || !isset($_POST['node_ids'])) {
print 'Missing form_ids and/or node_ids.';
exit;
}
if (count($_POST['form_ids']) != count($_POST['node_ids'])) {
print 'Different amount of form_ids than node_ids.';
exit;
}
if (count($_POST['node_ids']) == 0) {
print 'No node_ids sent.';
exit;
}
$return = array_combine($_POST['form_ids'], array_fill(0, count($_POST['form_ids']), NULL));
// If no attributes given we can do one query to fetch everything
$filtered_attrs = isset($_POST['attr_ids']) ? array_filter($_POST['attr_ids']) : '';
if (empty($filtered_attrs)) {
$result = db_query('SELECT ups.stock, up.nid FROM {uc_products} up
LEFT JOIN {uc_product_stock} ups ON ups.sku = up.model
WHERE up.nid IN(:nids)
AND ups.active = 1', array(
':nids' => $_POST['node_ids'],
));
foreach ($result as $product) {
$key = array_search($product->nid, $_POST['node_ids']);
$return[$_POST['form_ids'][$key]] = $product->stock;
}
}
else {
$attribs = array();
foreach ($_POST['attr_ids'] as $value) {
list($nid, $attr_id, $attr_val) = explode(':', $value);
$attribs[$nid][$attr_id] = $attr_val;
}
foreach ($_POST['node_ids'] as $key => $nid) {
$stockinfo = uc_out_of_stock_getstockinfo($nid, isset($attribs[$nid]) ? $attribs[$nid] : array());
if ($stockinfo) {
$return[$_POST['form_ids'][$key]] = $stockinfo['stock'];
}
}
}
// if there is some response, print it
print drupal_json_output($return);
exit;
}
function uc_out_of_stock_settings() {
$form['uc_out_of_stock_throbber'] = array(
'#type' => 'checkbox',
'#title' => t('Display throbber'),
'#default_value' => variable_get('uc_out_of_stock_throbber', TRUE),
'#description' => t('Display a throbber (animated wheel) next to add to cart button. This can be themed/styled and removed via CSS as well, but you can just disable it with this setting.'),
);
$form['uc_out_of_stock_instock'] = array(
'#type' => 'checkbox',
'#title' => t('Display in stock information'),
'#default_value' => variable_get('uc_out_of_stock_instock', TRUE),
'#description' => t('Display how many items are in stock above the add to cart button or below the qty field if it is present. It can be themed and translated as normal. Please see the uc_out_of_stock.js file for hints. If product has attributes, it may be impossible to know the stock until the attribute is selected, so it will simply display a default "In stock" message.'),
);
$uc_out_of_stock_text = variable_get('uc_out_of_stock_text', array(
'value' => '<span style="color: red;">Out of stock</span>',
'format' => NULL,
));
$text = check_markup($uc_out_of_stock_text['value'], $uc_out_of_stock_text['format']);
$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(
'#markup' => $text,
);
$form['uc_out_of_stock_text'] = array(
'#type' => 'text_format',
'#title' => t('Out of stock replacement HTML'),
'#default_value' => $uc_out_of_stock_text['value'],
'#format' => $uc_out_of_stock_text['format'],
'#description' => t('The HTML that will replace the Add To Cart button if no stock is available.'),
);
$form['advanced'] = array(
'#type' => 'fieldset',
'#title' => t('Advanced settings'),
'#collapsible' => TRUE,
'#collapsed' => !variable_get('uc_out_of_stock_disable_js', FALSE),
);
$form['advanced']['uc_out_of_stock_disable_js'] = array(
'#type' => 'checkbox',
'#title' => t('Disable javascript'),
'#default_value' => variable_get('uc_out_of_stock_disable_js', FALSE),
'#description' => t('If you disable javascript you will lose real-time checking of stock, support for checking for stock on attribute change and all of the HTML replacement confgured above. Use it with caution. This can be useful if you think this module is in conflict with some other third-party Ubercart module and you want to keep the server side validation of this module.'),
);
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) {
// If there were previews form errors, such as missing attributes,
// prevent validation as infomration might not be complete.
if (form_get_errors()) {
return;
}
if (isset($form_state['clicked_button']['#attributes']['class'])) {
$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 (in_array('node-add-to-cart', $class) || in_array('list-add-to-cart', $class)) {
$attrs = isset($form_state['values']['attributes']) ? $form_state['values']['attributes'] : array();
foreach ((array) $attrs as $aid => $oids) {
$attribute = uc_attribute_load($aid);
// 1 is select and 2 is radio, the only supported attributes that may have stock adjustments
if ($attribute->display != 1 && $attribute->display != 2) {
unset($attrs[$aid]);
}
}
$nid = $form_state['values']['nid'];
$qty = isset($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;
// Only process node products, to prevent discounts or other kind of things
// in the node to mess with this logic
if (!$item->nid) {
continue;
}
// 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)) {
if (isset($stored_cart_items[$item->cart_item_id])) {
$stored_item = $stored_cart_items[$item->cart_item_id];
}
if (isset($stored_item) && $item->nid == $stored_item->nid && $item->data == $stored_item->data) {
$item->model = $stored_item->model;
}
else {
foreach ($stored_cart_items as $stored_item) {
if ($item->nid == $stored_item->nid && $item->data == $stored_item->data) {
$item->model = $stored_item->model;
}
}
}
}
if (isset($item->model)) {
$cart_items[$item->model]['item'] = $item;
if (!isset($cart_items[$item->model]['qty'])) {
$cart_items[$item->model]['qty'] = 0;
}
$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'];
// Only validates if there are items on the cart, otherwise it's likely
// it's being tried to be removed by setting the qty to 0.
if ($cart_item['qty'] > 0) {
$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'];
}
if ($stockinfo['stock'] <= 0) {
$error = _uc_out_of_stock_get_error('out_of_stock', $item->nid, isset($item->data['attributes']) ? $item->data['attributes'] : array(), $stockinfo['stock']);
}
else {
$error = _uc_out_of_stock_get_error('not_enough', $item->nid, isset($item->data['attributes']) ? $item->data['attributes'] : array(), $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'];
if (substr($form_state['clicked_button']['#name'], 0, 7) != 'remove-' && $form_state['clicked_button']['#id'] != 'edit-empty') {
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 only have @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 only have @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'),
));
}
}
// Invoke hook_uc_out_of_stock_error_alter() to allow all modules to alter the resulting error message.
drupal_alter('uc_out_of_stock_error', $error, $product);
return $error;
}
Functions
Name![]() |
Description |
---|---|
uc_out_of_stock_form_alter | Implements 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. |