You are here

commerce_shipping_weight_tariff.module in Commerce Shipping Weight Tariff 7

commerce_shipping_weight_tariff.module

Allows the creation of complex matrices of shipping tariffs by order weight, across multiple shipping services. Each tariff is stored as a product entity with:

  • a weight field for the maximum weight for the tariff
  • a shipping service field to indicate the service this belongs to
  • the usual Commerce price fields, to give the price of the tariff.

These products should not be referenced by nodes, and will not be added to the cart; rather, their price is returned to Commerce Shipping as the shipping service rate.

File

commerce_shipping_weight_tariff.module
View source
<?php

/**
 * @file commerce_shipping_weight_tariff.module
 *
 * Allows the creation of complex matrices of shipping tariffs by order weight,
 * across multiple shipping services.
 * Each tariff is stored as a product entity with:
 *  - a weight field for the maximum weight for the tariff
 *  - a shipping service field to indicate the service this belongs to
 *  - the usual Commerce price fields, to give the price of the tariff.
 * These products should not be referenced by nodes, and will not be added to
 * the cart; rather, their price is returned to Commerce Shipping as the
 * shipping service rate.
 */
function commerce_shipping_weight_tariff_block_info() {
  return array(
    'shipping_matrix' => array(
      'info' => t('Weight tariff shipping matrix'),
      'cache' => DRUPAL_CACHE_GLOBAL,
    ),
  );
}
function commerce_shipping_weight_tariff_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'shipping_matrix':
      $block['subject'] = t('Shipping table');
      $block['content'] = array(
        '#theme' => 'commerce_shipping_weight_tariff_shipping_table',
        '#services' => commerce_shipping_services('weight_tariff_shipping'),
        '#weight_unit' => variable_get('commerce_shipping_weight_tariff_block_weight_unit', 'kg'),
        '#order_total_weight' => NULL,
      );
      break;
  }
  return $block;
}
function commerce_shipping_weight_tariff_block_configure($delta = '') {
  $form = array();
  switch ($delta) {
    case 'shipping_matrix':
      $units = array();
      foreach (physical_weight_units() as $ukey => $unit) {
        $units[$ukey] = "{$unit['name']} ({$unit['abbreviation']})";
      }
      asort($units);
      $form['commerce_shipping_weight_tariff_block_weight_unit'] = array(
        '#type' => 'select',
        '#title' => t('Weight Unit'),
        '#options' => $units,
        '#default_value' => variable_get('commerce_shipping_weight_tariff_block_weight_unit', 'kg'),
      );
      break;
  }
  return $form;
}
function commerce_shipping_weight_tariff_block_save($delta = '', $edit = array()) {
  switch ($delta) {
    case 'shipping_matrix':
      variable_set('commerce_shipping_weight_tariff_block_weight_unit', $edit['commerce_shipping_weight_tariff_block_weight_unit']);
      break;
  }
  return;
}
function commerce_shipping_weight_tariff_theme($existing, $type, $theme, $path) {
  return array(
    'commerce_shipping_weight_tariff_shipping_table' => array(
      'variables' => array(
        'services' => array(),
        'weight_unit' => 'kg',
        'order_total_weight' => NULL,
      ),
    ),
  );
}
function theme_commerce_shipping_weight_tariff_shipping_table($variables) {
  $weights = array();
  $prices = array();
  $rows = array();
  foreach ($variables['services'] as $skey => $service) {
    $products = _commerce_shipping_weight_tariff_fetch_tariff_products($service['name']);
    $rows[$skey] = array(
      $service['display_title'],
    );
    foreach ($products as $product) {
      $wrapper = entity_metadata_wrapper('commerce_product', $product);
      $weight = $wrapper->commerce_product_tariff_weight->weight
        ->value();
      $unit = $wrapper->commerce_product_tariff_weight->unit
        ->value();
      $weight = physical_weight_convert(array(
        'weight' => $weight,
        'unit' => $unit,
      ), $variables['weight_unit']);
      $weight = $weight['weight'];
      $price = $wrapper->commerce_price
        ->value();
      $prices[$weight][$skey] = commerce_currency_format($price['amount'], $price['currency_code']);
      $service = $wrapper->commerce_product_tariff_service
        ->value();
      $weights[$weight] = $weight;
    }
  }
  asort($weights);
  $table = array(
    'rows' => $rows,
    'header' => array(
      'shipping service',
    ),
  );
  $order_weight = array(
    '0',
    'kg',
  );
  if (arg(0) === 'checkout' && arg(2) !== 'complete') {
    global $user;
    $order = commerce_cart_order_load($user->uid);

    // Get the weight from the order.
    $order_weight = commerce_physical_order_weight($order, 'kg');
  }
  $table['header'][] = 'Σ';
  foreach ($table['rows'] as $skey => &$row) {
    $row[] = implode(' ', $order_weight) . '.';
  }
  foreach ($weights as $weight) {
    $table['header'][] = '< ' . number_format($weight, 3) . " {$variables['weight_unit']}";
    foreach ($table['rows'] as $skey => &$row) {
      if (isset($prices[$weight][$skey])) {
        $row[] = $prices[$weight][$skey];
      }
      else {
        $row[] = '';
      }
    }
  }
  return theme('table', $table);
}

/**
 * Implements hook_menu().
 */
function commerce_shipping_weight_tariff_menu() {

  // This is picked up magically by commerce_shipping_ui_overview().
  $items['admin/commerce/config/shipping/methods/weight-tariff-shipping/matrix'] = array(
    'title' => 'View tariff matrix',
    'description' => 'View the overview matrix of shipping weight tariffs.',
    'page callback' => 'commerce_shipping_weight_tariff_admin_matrix',
    'access arguments' => array(
      'administer shipping',
    ),
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_INLINE,
    'file' => 'commerce_shipping_weight_tariff.admin.inc',
    'weight' => 8,
  );
  $items['admin/commerce/config/shipping/services/weight-tariff-shipping/add'] = array(
    'title' => 'Add a weight tariff service',
    'description' => 'Create a new weight tariff shipping service.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_shipping_weight_tariff_service_form',
      commerce_shipping_weight_tariff_service_new(),
    ),
    'access callback' => 'commerce_shipping_weight_tariff_service_access',
    'access arguments' => array(
      'create',
    ),
    'type' => MENU_LOCAL_ACTION,
    'context' => MENU_CONTEXT_PAGE,
    'file' => 'commerce_shipping_weight_tariff.admin.inc',
  );
  foreach (commerce_shipping_services('weight_tariff_shipping') as $name => $shipping_service) {

    // Convert underscores to hyphens for the menu item argument.
    $service_name_arg = 'weight-tariff-shipping-' . strtr($name, '_', '-');
    $items['admin/commerce/config/shipping/services/' . $service_name_arg . '/edit'] = array(
      'title' => 'Edit',
      'description' => 'Edit this weight tariff service.',
      'page callback' => 'commerce_shipping_weight_tariff_service_edit_page',
      'page arguments' => array(
        $name,
      ),
      'access callback' => 'commerce_shipping_weight_tariff_service_access',
      'access arguments' => array(
        'update',
      ),
      'type' => MENU_LOCAL_TASK,
      'context' => MENU_CONTEXT_INLINE,
      'weight' => 0,
      'file' => 'commerce_shipping_weight_tariff.admin.inc',
    );
    $items['admin/commerce/config/shipping/services/' . $service_name_arg . '/delete'] = array(
      'title' => 'Delete',
      'description' => 'Delete this weight tariff service.',
      'page callback' => 'commerce_shipping_weight_tariff_service_delete_page',
      'page arguments' => array(
        $name,
      ),
      'access callback' => 'commerce_shipping_weight_tariff_service_access',
      'access arguments' => array(
        'delete',
      ),
      'type' => MENU_LOCAL_TASK,
      'context' => MENU_CONTEXT_INLINE,
      'weight' => 10,
      'file' => 'commerce_shipping_weight_tariff.admin.inc',
    );
  }
  return $items;
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function commerce_shipping_weight_tariff_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  if ($root_path == 'admin/commerce/config/shipping/methods/weight-tariff-shipping/matrix') {
    if (user_access('create commerce_product entities of bundle shipping_tariff')) {

      // Add an action linking to add a new tariff product.
      // Get the return destination first, as that gives us a query array.
      $query = drupal_get_destination();
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_task',
        '#link' => array(
          'title' => t('Add new shipping tariff'),
          'href' => 'admin/commerce/products/add/shipping-tariff',
          'localized_options' => array(
            'query' => $query,
          ),
        ),
      );
    }
  }
}

/**
 * Rate callback for hook_commerce_shipping_service_info().
 *
 * Retrieves all the tariff products that point to the given shipping service,
 * and finds the one whose maximum weight is nearest to the order weight. The
 * price on this tariff product is returned as the shipping service rate.
 *
 * @param $shipping_service
 *  A shipping service definition.
 * @param $order
 *  The current order object.
 */
function commerce_shipping_weight_tariff_service_rate($shipping_service, $order) {

  // Get the weight from the order.
  $order_weight = commerce_physical_order_weight($order, 'kg');
  if (isset($order_weight['weight'])) {
    $order_weight = $order_weight['weight'];
  }
  else {
    $order_weight = 0;
  }
  $products = _commerce_shipping_weight_tariff_fetch_tariff_products($shipping_service['name']);

  // Create an array of max weight => product id, for all the weights that are
  // applicable. We then take the lowest one.
  // (There is potential for clobbering here, but if you define two tariffs
  // with the same max weight, then you have a problem anyway.)
  $product_max_weights = array();
  foreach ($products as $product) {
    try {
      $wrapper = entity_metadata_wrapper('commerce_product', $product);
      $weight = $wrapper->commerce_product_tariff_weight->weight
        ->value();
      $unit = $wrapper->commerce_product_tariff_weight->unit
        ->value();
      if ($unit !== 'kg') {
        $weight = physical_weight_convert(array(
          'weight' => $weight,
          'unit' => $unit,
        ), 'kg');
        $weight = $weight['weight'];
      }
    } catch (EntityMetadataWrapperException $e) {
      continue;
    }

    // Skip a tariff if the order weighs more than the tariff's max weight.
    if ($weight < $order_weight) {
      continue;
    }

    // Build the array of weights => product ids.
    $product_max_weights["{$weight}"] = $product->product_id;
  }

  // If we found no tariffs (because the product is too heavy, perhaps), return nothing.
  if (empty($product_max_weights)) {
    return;
  }
  ksort($product_max_weights);

  // Sort the weights array by weight, then take the lowest item.
  $tariff_product_id = array_shift($product_max_weights);

  // Get our desired tariff product.
  $tariff_product = $products[$tariff_product_id];

  //dsm($tariff_product);

  // Return the price from the tariff product.
  $wrapper = entity_metadata_wrapper('commerce_product', $tariff_product);
  $price = $wrapper->commerce_price
    ->value();

  // Hack: set the tariff product id in the price, which AFAICT is the only
  // place to put it so that hook_commerce_shipping_method_collect_rates() can
  // see it and put it in the line item.
  // @see http://drupal.org/node/1804510
  $price['tariff_product_id'] = $tariff_product_id;
  if (isset($price['data']['components'][0]['price'])) {
    $price['data']['components'] = array();
  }
  return $price;
}

/**
 * Options list callback for the service method field on tariff products.
 */
function commerce_shipping_weight_tariff_methods_options_list() {

  // Get the services for our method.
  $services = commerce_shipping_services('weight_tariff_shipping');
  $options = array();
  foreach ($services as $service_name => $service_info) {
    $options[$service_name] = $service_info['display_title'];
  }
  return $options;
}

/**
 * Returns an initialized shipping service array for forms.
 */
function commerce_shipping_weight_tariff_service_new() {
  return array(
    'name' => '',
    'title' => '',
    'display_title' => '',
    'description' => '',
    'is_new' => TRUE,
  );
}

/**
 * Implements hook_permission().
 */
function commerce_shipping_weight_tariff_permission() {
  return array(
    'administer weight tariff services' => array(
      'title' => t('Administer weight tariff shipping services.'),
      'description' => t('Allows users to create, edit and delete weight tariff shipping services.'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Access callback: grants users access to weight tariff service operations if they
 * have the specific permission or generic shipping permission.
 *
 * @param $op
 *   The operation string: of create, update, or delete.
 *
 * @return
 *   Boolean indicating the user's access.
 */
function commerce_shipping_weight_tariff_service_access($op) {
  return user_access('administer shipping') || user_access('administer weight tariff services');
}

/**
 * Saves a service to the database.
 *
 * @param $shipping_service
 *   The shipping service to save.
 * @param $skip_reset
 *   Boolean indicating whether or not this save should result in shipping
 *   services being reset and the menu being rebuilt; defaults to FALSE. This is
 *   useful when you intend to perform many saves at once, as menu rebuilding is
 *   very costly in terms of performance.
 *
 * @return
 *   The return value of the call to drupal_write_record() to save the service;
 *   either FALSE on failure or SAVED_NEW or SAVED_UPDATED indicating
 *   the type of query performed to save the service.
 */
function commerce_shipping_weight_tariff_service_save($shipping_service, $skip_reset = FALSE) {
  $op = drupal_write_record('commerce_shipping_weight_tariff_service', $shipping_service, empty($shipping_service['is_new']) ? 'name' : array());

  // If this is a new service and the insert did not fail...
  if (!empty($shipping_service['is_new']) && $op !== FALSE) {

    // Notify other modules that a new service has been created.
    module_invoke_all('commerce_shipping_weight_tariff_service_insert', $shipping_service, $skip_reset);
  }
  elseif ($op !== FALSE) {

    // Notify other modules that an existing service has been updated.
    module_invoke_all('commerce_shipping_weight_tariff_service_update', $shipping_service, $skip_reset);
  }

  // Clear the necessary caches and rebuild the menu items.
  if (!$skip_reset) {
    commerce_shipping_services_reset();
    entity_defaults_rebuild();
    rules_clear_cache(TRUE);
    menu_rebuild();
  }
  return $op;
}
function _commerce_shipping_weight_tariff_fetch_tariff_products($name = false, $asClass = true) {
  $products = array();
  $query = new EntityFieldQuery();
  $query
    ->entityCondition('entity_type', 'commerce_product')
    ->entityCondition('bundle', 'shipping_tariff');
  if ($name) {
    $query
      ->fieldCondition('commerce_product_tariff_service', 'value', $name, '=');
  }
  $result = $query
    ->execute();
  if (isset($result['commerce_product'])) {
    $products = commerce_product_load_multiple(array_keys($result['commerce_product']));
    if (!$asClass) {
      $products = array_keys($products);
    }
  }
  return $products;
}

/**
 * Deletes a service.
 *
 * @param $name
 *   The machine-name of the service.
 * @param $skip_reset
 *   Boolean indicating whether or not this delete should result in shipping
 *   services being reset and the menu being rebuilt; defaults to FALSE. This is
 *   useful when you intend to perform many deletions at once, as menu
 *   rebuilding is very costly in terms of performance.
 */
function commerce_shipping_weight_tariff_service_delete($name, $skip_reset = FALSE) {
  $shipping_service = commerce_shipping_service_load($name);
  $products = _commerce_shipping_weight_tariff_fetch_tariff_products($name, false);
  commerce_product_delete_multiple($products);
  db_delete('commerce_shipping_weight_tariff_service')
    ->condition('name', $name)
    ->execute();
  rules_config_delete(array(
    'commerce_shipping_service_' . $name,
  ));

  // Clear the necessary caches and rebuild the menu items.
  if (!$skip_reset) {
    commerce_shipping_services_reset();
    entity_defaults_rebuild();
    rules_clear_cache(TRUE);
    menu_rebuild();
  }

  // Notify other modules that this flat rate service has been deleted.
  module_invoke_all('commerce_shipping_weight_tariff_service_delete', $shipping_service, $skip_reset);
}