'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); } // A single product or uses attributes (like colour & size). 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. 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'; 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. 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. 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())); }