You are here

commerce_stock_enforcement.module in Commerce Stock 8

Commerce stock enforcement module file.

File

modules/enforcement/commerce_stock_enforcement.module
View source
<?php

/**
 * @file
 * Commerce stock enforcement module file.
 */
use Drupal\commerce\Context;
use Drupal\commerce\PurchasableEntityInterface;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\views\Form\ViewsForm;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Implements hook_form_alter().
 */
function commerce_stock_enforcement_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // Add to cart form.
  if (strpos($form_id, "commerce_order_item_add_to_cart_form") !== FALSE || strpos($form_id, "commerce_order_item_dc_ajax_add_cart_form") !== FALSE) {

    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
    $product = $form_state
      ->get('product');

    // Get the product variation.
    $selected_variation_id = $form_state
      ->get('selected_variation');
    if (!empty($selected_variation_id)) {
      $selected_variation = ProductVariation::load($selected_variation_id);
    }
    else {
      $selected_variation = $product
        ->getDefaultVariation();
    }

    // Get the context.
    $context = commerce_stock_enforcement_get_context($selected_variation);

    // Add a form validate needed for the add to cart action.
    $form['#validate'] = array_merge($form['#validate'], [
      'commerce_stock_enforcement_add_to_cart_form_validate',
    ]);

    // Check if in stock.
    $instock = commerce_stock_enforcement_check($selected_variation, 1, $context);
    if (!$instock) {
      $form['actions']['submit']['#value'] = t('Out of stock');
      $form['actions']['submit']['#disabled'] = TRUE;

      // If quantity is visible.
      if (isset($form['quantity'])) {
        $form['quantity']['#disabled'] = TRUE;
      }
    }
  }

  // Cart page.
  if ($form_state
    ->getFormObject() instanceof ViewsForm) {

    /** @var \Drupal\views\ViewExecutable $view */
    $view = reset($form_state
      ->getBuildInfo()['args']);

    // Only add the Checkout button if the cart form view has order items.
    if ($view->storage
      ->get('tag') == 'commerce_cart_form' && !empty($view->result)) {

      // Get the order ID from the view argument.
      $order_id = $view->args[0];

      /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
      $order = \Drupal::entityTypeManager()
        ->getStorage('commerce_order')
        ->load($order_id);

      // Force a check to display the stock state to the user.
      $request_method = \Drupal::requestStack()
        ->getCurrentRequest()
        ->getMethod();

      // If a GET e.g. not a submit/post.
      if ($request_method == 'GET') {

        // Perform a check to display the stock state to the user.
        commerce_stock_enforcement_is_order_in_stock($order, TRUE);
      }

      // Add a form validate needed for the add to cart action.
      $form['#validate'] = array_merge($form['#validate'], [
        'commerce_stock_enforcement_cart_order_item_views_form_validate',
      ]);
    }
  }

  // Checkout.
  if (strpos($form_id, "commerce_checkout_flow") !== FALSE && $form_state
    ->getFormObject()
    ->getBaseFormId() == 'commerce_checkout_flow') {

    /** @var Drupal\Core\Form\FormInterface $form_object */
    $form_object = $form_state
      ->getFormObject();

    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
    $order = $form_object
      ->getOrder();
    if ($form['#step_id'] != 'complete' && !commerce_stock_enforcement_is_order_in_stock($order, FALSE)) {

      // Redirect back to cart.
      $response = new RedirectResponse('/cart');
      $response
        ->send();
    }

    // Add a submit validate.
    $form['#validate'] = array_merge($form['#validate'], [
      'commerce_stock_enforcement_checkout_form_validate',
    ]);
  }
}

/**
 * Validates the add to cart form submit.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 */
function commerce_stock_enforcement_add_to_cart_form_validate(array $form, FormStateInterface $form_state) {

  // Get add to cart quantity.
  $values = $form_state
    ->getValues();
  if (isset($values['quantity'])) {
    $quantity = $values['quantity'][0]['value'];
  }
  else {
    $quantity = 1;
  }

  // Load the product variation.
  if (isset($values['purchased_entity'][0]['variation'])) {
    $variation_id = $values['purchased_entity'][0]['variation'];

    /** @var \Drupal\commerce\PurchasableEntityInterface $purchased_entity */
    $purchased_entity = ProductVariation::load($variation_id);
  }
  else {
    $purchased_entity = ProductVariation::load($values['purchased_entity'][0]['target_id']);
  }

  // ** @var Drupal\Core\Form\FormInterface $form_object */
  // $entity_form = $form_state->getFormObject();
  //
  // $order_item = $entity_form->getEntity();
  // ** @var \Drupal\commerce\PurchasableEntityInterface $purchased_entity */
  // $purchased_entity = $order_item->getPurchasedEntity();
  $context = commerce_stock_enforcement_get_context($purchased_entity);

  // Get the available stock level.
  $stock_level = commerce_stock_enforcement_get_stock_level($purchased_entity, $context);

  // Get the already ordered quantity.
  $already_ordered = commerce_stock_enforcement_get_ordered_quantity($purchased_entity, $context);
  $total_requested = $already_ordered + $quantity;
  if ($total_requested <= $stock_level) {
    return;
  }
  if ($already_ordered === 0) {
    $message_text = Drupal::config('commerce_stock_enforcement.settings')
      ->get('insufficient_stock_add_to_cart_zero_in_cart');
    $message_text = Xss::filter($message_text);
    $message = t($message_text, [
      '%qty' => $stock_level,
      '%qty_asked' => $quantity,
    ]);
  }
  else {
    $message_text = Drupal::config('commerce_stock_enforcement.settings')
      ->get('insufficient_stock_add_to_cart_quantity_in_cart');
    $message_text = Xss::filter($message_text);
    $message = t($message_text, [
      '%qty' => $stock_level,
      '%qty_o' => $already_ordered,
    ]);
  }
  $form_state
    ->setError($form, $message);
}

/**
 * Validate the cart page submit.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 */
function commerce_stock_enforcement_cart_order_item_views_form_validate(array $form, FormStateInterface $form_state) {
  $triggering_element = $form_state
    ->getTriggeringElement();

  // If triggered by a line item delete.
  if (isset($triggering_element['#remove_order_item']) && $triggering_element['#remove_order_item']) {

    // No need to validate.
    return;
  }
  $values = $form_state
    ->getValues();
  if (isset($values['edit_quantity'])) {
    $quantities = $values['edit_quantity'];
  }
  else {
    $quantities = [];
  }

  /** @var \Drupal\views\ViewExecutable $view */
  $view = reset($form_state
    ->getBuildInfo()['args']);

  // Get the order ID from the view argument.
  $order_id = $view->argument['order_id']->value[0];

  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
  $order = \Drupal::entityTypeManager()
    ->getStorage('commerce_order')
    ->load($order_id);
  foreach ($order
    ->getItems() as $id => $order_item) {
    $purchased_entity = $order_item
      ->getPurchasedEntity();
    if (!$purchased_entity) {

      // Not every order item has a purchased entity.
      continue;
    }
    $name = $purchased_entity
      ->getTitle();
    if (isset($quantities) && isset($quantities[$id])) {
      $qty = $quantities[$id];
    }
    else {
      $qty = 1;
    }
    $context = commerce_stock_enforcement_get_context($purchased_entity);
    $stock_level = commerce_stock_enforcement_get_stock_level($purchased_entity, $context);

    // Get the already ordered quantity.
    $already_ordered = commerce_stock_enforcement_get_ordered_quantity($purchased_entity, $context);
    if ($qty > $stock_level) {
      $message_text = Drupal::config('commerce_stock_enforcement.settings')
        ->get('insufficient_stock_cart');
      $message_text = Xss::filter($message_text);
      $form_state
        ->setError($form['edit_quantity'][$id], t($message_text, [
        '%name' => $name,
        '%qty' => $stock_level,
      ]));
    }
  }
}

/**
 * Validate the checkout form submit.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 *
 * @throws \Drupal\commerce\Response\NeedsRedirectException
 */
function commerce_stock_enforcement_checkout_form_validate(array $form, FormStateInterface $form_state) {
  $triggering_element = $form_state
    ->getTriggeringElement();

  /** @var Drupal\Core\Form\FormInterface $form_object */
  $form_object = $form_state
    ->getFormObject();

  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
  $order = $form_object
    ->getOrder();
  if ($form['#step_id'] != 'complete' && !commerce_stock_enforcement_is_order_in_stock($order, FALSE)) {
    $cart_page = Url::fromRoute('commerce_cart.page', [], [
      'absolute' => TRUE,
    ]);
    \Drupal::messenger()
      ->addError('One or more Items are out of stock. Checkout canceled!');
    throw new NeedsRedirectException($cart_page
      ->toString());
  }
}

/**
 * Get the context for the provided Purchasable Entity.
 *
 * @param \Drupal\commerce\PurchasableEntityInterface $entity
 *   The purchasable entity.
 *
 * @return \Drupal\commerce\Context
 *   The context.
 *
 * @see \Drupal\commerce_stock\ContextCreatorTrait::getContextDetails()
 * @see \Drupal\commerce_cart\Form\AddToCartForm::selectStore()
 */
function commerce_stock_enforcement_get_context(PurchasableEntityInterface $entity) {

  // @todo - think about using selectStore() in commerce_cart.module.
  $store_to_use = \Drupal::service('commerce_store.current_store')
    ->getStore();
  $current_user = \Drupal::currentUser();

  // Make sure the current store is in the entity stores.
  $stores = $entity
    ->getStores();
  $found = FALSE;

  // If we have a current store.
  if ($store_to_use) {

    // Make sure it is associated with the curent product.
    foreach ($stores as $store) {
      if ($store
        ->id() == $store_to_use
        ->id()) {
        $found = TRUE;
        break;
      }
    }
  }

  // If not found and we have stores associated with the product.
  if (!$found) {
    if (!empty($stores)) {

      // Get the first store the product is assigned to.
      $store_to_use = array_shift($stores);
    }
  }
  return new Context($current_user, $store_to_use);
}

/**
 * Check if the PurchasableEntity is in stock.
 *
 * @param \Drupal\commerce\PurchasableEntityInterface $entity
 *   The purchasable entity.
 * @param int $quantity
 *   The quantity.
 * @param \Drupal\commerce\Context $context
 *   The context.
 *
 * @return bool
 *   True if entity is in stock, FALSE otherwise.
 */
function commerce_stock_enforcement_check(PurchasableEntityInterface $entity, $quantity, Context $context) {
  if (empty($quantity)) {
    $quantity = 1;
  }
  $stock_level = commerce_stock_enforcement_get_stock_level($entity, $context);
  return $stock_level >= $quantity;
}

/**
 * Check if order is in stock.
 *
 * If order contains products that are out of stock, then error messages will be
 * generated and the user redirected to the cart page.
 *
 * @param \Drupal\commerce_order\Entity\OrderInterface $order
 *   The order.
 * @param bool $show_warnings
 *   Whether to show warning or not.
 *
 * @return bool
 *   True if order is in stock, False if not.
 *
 * @ToDo Needs refactoring. This function does to much. Job is here to check
 * if all purchasable entities are in stock. Factor out the warnings part.
 */
function commerce_stock_enforcement_is_order_in_stock(OrderInterface $order, $show_warnings = TRUE) {

  /** @var Drupal\commerce_store\Entity\StoreInterface $order_store */
  $order_store = $order
    ->getStore();

  /** @var Drupal\user\UserInterface $order_user */
  $order_user = $order
    ->getCustomer();
  $order_context = new Context($order_user, $order_store);
  $order_in_stock = TRUE;
  foreach ($order
    ->getItems() as $id => $order_item) {
    $purchased_entity = $order_item
      ->getPurchasedEntity();
    if (!$purchased_entity) {

      // Not every order item has a purchased entity.
      continue;
    }
    $name = $purchased_entity
      ->getTitle();
    $qty = $order_item
      ->getQuantity();
    $stock_level = commerce_stock_enforcement_get_stock_level($purchased_entity, $order_context);
    if ($qty > $stock_level) {
      if ($show_warnings) {
        $message_text = Drupal::config('commerce_stock_enforcement.settings')
          ->get('insufficient_stock_cart');
        $message_text = Xss::filter($message_text);
        \Drupal::messenger()
          ->addError(t($message_text, [
          '%name' => $name,
          '%qty' => $stock_level,
        ]));
      }
      $order_in_stock = FALSE;
    }
  }
  return $order_in_stock;
}

/**
 * Get the available stock level for the PurchasableEntity.
 *
 * @param \Drupal\commerce\PurchasableEntityInterface $entity
 *   The purchasable entity.
 * @param \Drupal\commerce\Context $context
 *   The context object.
 *
 * @return int
 *   The stock level.
 */
function commerce_stock_enforcement_get_stock_level(PurchasableEntityInterface $entity, Context $context) {

  /** @var \Drupal\commerce_stock\StockServiceManagerInterface $stockManager */
  $stockManager = \Drupal::service('commerce_stock.service_manager');

  /** @var \Drupal\commerce_stock\StockServiceInterface $stock_service */
  $stock_service = $stockManager
    ->getService($entity);

  /** @var \Drupal\commerce_stock\StockCheckInterface $stock_checker */
  $stock_checker = $stock_service
    ->getStockChecker();
  if ($stock_checker
    ->getIsAlwaysInStock($entity)) {
    return PHP_INT_MAX;
  }
  $stock_config = $stock_service
    ->getConfiguration();
  $stock_level = $stock_checker
    ->getTotalStockLevel($entity, $stock_config
    ->getAvailabilityLocations($context, $entity));
  return $stock_level;
}

/**
 * Get the quantity already ordered for the specified PurchasableEntity.
 *
 * @param \Drupal\commerce\PurchasableEntityInterface $entity
 *   The purchasable entity.
 * @param \Drupal\commerce\Context $context
 *   The context object.
 *
 * @return int
 *   The ordered quantity.
 */
function commerce_stock_enforcement_get_ordered_quantity(PurchasableEntityInterface $entity, Context $context) {

  // Get the already ordered quantity.
  $already_ordered = 0;

  // Get all the carts.
  $all_carts = \Drupal::service('commerce_cart.cart_provider')
    ->getCarts();

  // Cycle all the carts to get the total stock already ordered.
  // It is unlikely that a product will be in more then one cart, but it is
  // probably safer to check.
  foreach ($all_carts as $cart) {
    foreach ($cart
      ->getItems() as $order_item) {
      $purchased_entity = $order_item
        ->getPurchasedEntity();
      if ($purchased_entity && $purchased_entity
        ->id() == $entity
        ->id()) {
        $already_ordered += $order_item
          ->getQuantity();
      }
    }
  }
  return $already_ordered;
}

Functions

Namesort descending Description
commerce_stock_enforcement_add_to_cart_form_validate Validates the add to cart form submit.
commerce_stock_enforcement_cart_order_item_views_form_validate Validate the cart page submit.
commerce_stock_enforcement_check Check if the PurchasableEntity is in stock.
commerce_stock_enforcement_checkout_form_validate Validate the checkout form submit.
commerce_stock_enforcement_form_alter Implements hook_form_alter().
commerce_stock_enforcement_get_context Get the context for the provided Purchasable Entity.
commerce_stock_enforcement_get_ordered_quantity Get the quantity already ordered for the specified PurchasableEntity.
commerce_stock_enforcement_get_stock_level Get the available stock level for the PurchasableEntity.
commerce_stock_enforcement_is_order_in_stock Check if order is in stock.