array( 'group' => 'commerce', ), 'commerce_shipping_method_info_alter' => array( 'group' => 'commerce', ), 'commerce_shipping_service_info' => array( 'group' => 'commerce', ), 'commerce_shipping_service_info_alter' => array( 'group' => 'commerce', ), 'commerce_shipping_method_collect_rates' => array( 'group' => 'commerce', ), 'commerce_shipping_service_calculate_rate' => array( 'group' => 'commerce', ), ); return $hooks; } /** * Implements hook_permission(). */ function commerce_shipping_permission() { return array( 'administer shipping' => array( 'title' => t('Administer shipping methods and services'), 'description' => t('Allows users to configure enabled shipping methods and their available services.'), 'restrict access' => TRUE, ), ); } /** * Implements hook_commerce_customer_profile_type_info(). */ function commerce_shipping_commerce_customer_profile_type_info() { $profile_types = array(); $profile_types['shipping'] = array( 'type' => 'shipping', 'name' => t('Shipping information'), 'description' => t('The profile used to collect shipping information on the checkout and order forms.'), 'help' => '', 'checkout_pane_weight' => 0, ); return $profile_types; } /** * Implements hook_commerce_checkout_page_info(). */ function commerce_shipping_commerce_checkout_page_info() { $checkout_pages = array(); $checkout_pages['shipping'] = array( 'title' => t('Shipping'), 'weight' => 5, ); return $checkout_pages; } /** * Implements hook_commerce_checkout_pane_info(). */ function commerce_shipping_commerce_checkout_pane_info() { $checkout_panes = array(); $checkout_panes['commerce_shipping'] = array( 'title' => t('Shipping service'), 'base' => 'commerce_shipping_pane', 'file' => 'includes/commerce_shipping.checkout_pane.inc', 'page' => 'shipping', 'weight' => 2, 'review' => FALSE, ); return $checkout_panes; } /** * Implements hook_commerce_price_component_type_info(). */ function commerce_shipping_commerce_price_component_type_info() { $components = array(); // Define a generic shipping price component type. $components['shipping'] = array( 'title' => t('Shipping'), 'weight' => 20, ); // Add a price component type for each shipping service that specifies it. foreach (commerce_shipping_services() as $name => $shipping_service) { if ($shipping_service['price_component'] && empty($components[$shipping_service['price_component']])) { $components[$shipping_service['price_component']] = array( 'title' => $shipping_service['title'], 'display_title' => $shipping_service['display_title'], 'shipping_service' => $name, 'weight' => 20, ); } } return $components; } /** * Implements hook_modules_enabled(). */ function commerce_shipping_modules_enabled($modules) { commerce_shipping_methods_reset(); commerce_shipping_services_reset(); _commerce_shipping_default_rules_reset($modules); } /** * Resets default Rules if necessary when modules are enabled or disabled. * * @param $modules * An array of module names that have been enabled or disabled. */ function _commerce_shipping_default_rules_reset($modules) { $reset_default_rules = FALSE; // Look for any module defining a new shipping method or service. foreach ($modules as $module) { if (function_exists($module . '_commerce_shipping_method_info') || function_exists($module . '_commerce_shipping_service_info')) { $reset_default_rules = TRUE; } } // If we found a module defining a new shipping method or service, we need to // rebuild the default Rules especially for this module so the default rules // and components will appear properly for this module. if ($reset_default_rules) { entity_defaults_rebuild(); rules_clear_cache(TRUE); variable_set('menu_rebuild_needed', TRUE); } } /** * Implements hook_module_implements_alter(). */ function commerce_shipping_module_implements_alter(&$implementations, $hook) { // To allow the default rules defined by this module to be overridden by other // modules (such as Features produced modules), we need to ensure that this // module's hook_default_rules_configuration() is invoked before theirs. if ($hook == 'default_rules_configuration') { // Extract this module's entry from the hook implementations array. $group = $implementations['commerce_shipping']; unset($implementations['commerce_shipping']); // And re-add it by prepending it back onto the array. $implementations = array('commerce_shipping' => $group) + $implementations; } } /** * Returns an array of shipping methods defined by enabled modules. * * @return * An associative array of shipping method arrays keyed by the method_id. */ function commerce_shipping_methods() { $shipping_methods = &drupal_static(__FUNCTION__); // If the shipping methods haven't been defined yet, do so now. if (!isset($shipping_methods)) { $shipping_methods = array(); // Build the shipping methods array, including module names for the purpose // of including files if necessary. foreach (module_implements('commerce_shipping_method_info') as $module) { foreach (module_invoke($module, 'commerce_shipping_method_info') as $name => $shipping_method) { $shipping_method['name'] = $name; $shipping_method['module'] = $module; $shipping_methods[$name] = $shipping_method; } } drupal_alter('commerce_shipping_method_info', $shipping_methods); foreach ($shipping_methods as $name => &$shipping_method) { $defaults = array( 'name' => $name, 'display_title' => $shipping_method['title'], 'description' => '', 'active' => TRUE, ); $shipping_method += $defaults; } } return $shipping_methods; } /** * Resets the cached list of shipping methods. */ function commerce_shipping_methods_reset() { $shipping_methods = &drupal_static('commerce_shipping_methods'); $shipping_methods = NULL; } /** * Returns a shipping method array. * * @param $name * The machine-name of the shipping method to return. * * @return * The fully loaded shipping method array or FALSE if not found. */ function commerce_shipping_method_load($name) { $shipping_methods = commerce_shipping_methods(); return isset($shipping_methods[$name]) ? $shipping_methods[$name] : FALSE; } /** * Returns the human readable title of any or all shipping methods. * * @param $name * The machine-name of the shipping method whose title should be returned. If * left NULL, an array of all titles will be returned. * @param $title_type * The type of title to return: 'title' or 'display_title'. * * @return * Either an array of all shipping method titles keyed by the machine-name or * a string containing the human readable title for the specified method. If a * method is specified that does not exist, this function returns FALSE. */ function commerce_shipping_method_get_title($name = NULL, $title_type = 'title') { $shipping_methods = commerce_shipping_methods(); // Return a method title if specified and it exists. if (!empty($name)) { if (isset($shipping_methods[$name])) { return $shipping_methods[$name][$title_type]; } else { // Return FALSE if it does not exist. return FALSE; } } // Otherwise turn the array values into the method title only. $shipping_method_titles = array(); foreach ((array) $shipping_methods as $key => $value) { $shipping_method_titles[$key] = $value[$title_type]; } return $shipping_method_titles; } /** * Wraps commerce_shipping_method_get_title() for the Entity module and Field API. * * @return * An array of shipping method titles keyed by machine-name for use in options * lists and allowed values lists. */ function commerce_shipping_method_options_list() { return commerce_shipping_method_get_title(); } /** * Returns an array of shipping services keyed by name. * * @param $method * The machine-name of a shipping method to filter the return value by. */ function commerce_shipping_services($method = NULL) { // First check the static cache for a shipping services array. $shipping_services = &drupal_static(__FUNCTION__); // If it did not exist, fetch the services now. if (!isset($shipping_services)) { $shipping_services = array(); // Find shipping services defined by hook_commerce_shipping_service_info(). $weight = 0; foreach (module_implements('commerce_shipping_service_info') as $module) { foreach ((array) module_invoke($module, 'commerce_shipping_service_info') as $name => $shipping_service) { // Initialize shipping service properties if necessary. $defaults = array( 'name' => $name, 'base' => $name, 'display_title' => $shipping_service['title'], 'description' => '', 'shipping_method' => '', 'rules_component' => TRUE, 'price_component' => $name, 'weight' => $weight++, 'callbacks' => array(), 'module' => $module, ); $shipping_service = array_merge($defaults, $shipping_service); // Merge in default callbacks. foreach (array('rate', 'details_form', 'details_form_validate', 'details_form_submit') as $callback) { if (!isset($shipping_service['callbacks'][$callback])) { $shipping_service['callbacks'][$callback] = $shipping_service['base'] . '_' . $callback; } } $shipping_services[$name] = $shipping_service; } } // Last allow the info to be altered by other modules. drupal_alter('commerce_shipping_service_info', $shipping_services); } // Filter out services that don't match the specified shipping method filter. if (!empty($method)) { $filtered_services = $shipping_services; foreach ($filtered_services as $name => $shipping_service) { if ($shipping_service['shipping_method'] != $method) { unset($filtered_services[$name]); } } return $filtered_services; } return $shipping_services; } /** * Resets the cached list of shipping services. */ function commerce_shipping_services_reset() { $shipping_services = &drupal_static('commerce_shipping_services'); $shipping_services = NULL; } /** * Returns a single shipping service array. * * @param $name * The machine-name of the shipping service to return. * * @return * The specified shipping service array or FALSE if it did not exist. */ function commerce_shipping_service_load($name) { $shipping_services= commerce_shipping_services(); return empty($shipping_services[$name]) ? FALSE : $shipping_services[$name]; } /** * Returns the human readable title of any or all shipping services. * * @param $name * The machine-name of the shipping service whose title should be returned. If * left NULL, an array of all titles will be returned. * @param $title_type * The type of title to return: 'title' or 'display_title'. * * @return * Either an array of all shipping service titles keyed by the machine-name or * a string containing the human readable title for the specified service. If * a service is specified that does not exist, this function returns FALSE. */ function commerce_shipping_service_get_title($name = NULL, $title_type = 'title') { $shipping_services = commerce_shipping_services(); // Return a service title if specified and it exists. if (!empty($name)) { if (isset($shipping_services[$name])) { return $shipping_services[$name][$title_type]; } else { // Return FALSE if it does not exist. return FALSE; } } // Otherwise turn the array values into the service title only. $shipping_service_titles = array(); foreach ((array) $shipping_services as $key => $value) { $method_title = commerce_shipping_method_get_title($value['shipping_method']); // Since Views needs a flat array and services must be unique, return a // flat array. $shipping_service_titles[$key] = $value[$title_type] . ' (' . $method_title . ')'; } // Sort the title groups by method title. ksort($shipping_service_titles); return $shipping_service_titles; } /** * Wraps commerce_shipping_service_get_title() for the Entity module. * * @return * An array of shipping service titles keyed by machine-name as needed for * options lists. */ function commerce_shipping_service_options_list() { return commerce_shipping_service_get_title(); } /** * Returns the specified callback for the given shipping service if one exists. * * @param $shipping_service * The shipping service info array. * @param $callback * The callback function to return, one of: * - rate * - details_form * - details_form_validate * - details_form_submit * * @return * A string containing the name of the callback function or FALSE if it could * not be found. */ function commerce_shipping_service_callback($shipping_service, $callback) { // If the specified callback function exists, return it. if (!empty($shipping_service['callbacks'][$callback]) && function_exists($shipping_service['callbacks'][$callback])) { return $shipping_service['callbacks'][$callback]; } // Otherwise return FALSE. return FALSE; } /** * Collects available shipping rates for an order, adding them to the order * object via an unsaved shipping_rates property. * * @param $order * The order for which rates will be collected. */ function commerce_shipping_collect_rates($order) { $order->shipping_rates = array(); rules_invoke_all('commerce_shipping_collect_rates', $order); // Sort rates by the weight value of their line items. This value is derived // from the related shipping service's rate but may be overridden via // hook_commerce_shipping_method_collect_rates(). uasort($order->shipping_rates, 'commerce_shipping_sort_rates'); } /** * Sorts shipping rates based on the weight property added to shipping line * items in an order's data array. */ function commerce_shipping_sort_rates($a, $b) { $a_weight = isset($a->weight) ? $a->weight : 0; $b_weight = isset($b->weight) ? $b->weight : 0; if ($a_weight == $b_weight) { return 0; } return ($a_weight < $b_weight) ? -1 : 1; } /** * Collects available shipping services of the specified method for an order. * * This function is typically called via the Rules action "Collect rates for a * shipping method" attached to a default Rule. * * @param $method * The machine-name of the shipping method whose services should be collected. * @param $order * The order to which the services should be made available. */ function commerce_shipping_method_collect_rates($method, $order) { // Load all the rule components. $components = rules_get_components(FALSE, 'action'); // Loop over each shipping service in search of matching components. foreach (commerce_shipping_services() as $name => $shipping_service) { // If the current service matches the method and specifies a default component... if ($shipping_service['shipping_method'] == $method && $shipping_service['rules_component']) { $component_name = 'commerce_shipping_service_' . $name; // If we found the current service's component... if (!empty($components[$component_name])) { // Invoke it with the order. rules_invoke_component($component_name, $order); } } } // Allow modules handling shipping service calculation on their own to return // services for this method, too. module_invoke_all('commerce_shipping_method_collect_rates', $method, $order); } /** * Adds a shipping rate to the given order object for the specified service. * * @param $service * The machine-name of the shipping service to rate. * @param $order * The order for which the shipping service should be rated. */ function commerce_shipping_service_rate_order($service, $order) { // Load the full shipping service info array. $shipping_service = commerce_shipping_service_load($service); // If the service specifies a rate callback... if ($callback = commerce_shipping_service_callback($shipping_service, 'rate')) { // Get the base rate price for the shipping service. $price = $callback($shipping_service, $order); // If we got a base price... if ($price) { // Create a calculated shipping line item out of it. $line_item = commerce_shipping_service_rate_calculate($service, $price, $order->order_id); $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); // Add the rate to the order as long as it doesn't have a NULL price amount. if (!is_null($line_item_wrapper->commerce_unit_price->amount->value())) { // Include a weight property on the line item object from the shipping // service for sorting rates. $line_item->weight = empty($shipping_service['weight']) ? 0 : $shipping_service['weight']; $order->shipping_rates[$service] = $line_item; } } } } /** * Creates a shipping line item with the specified initial price and passes it * through Rules for additional calculation. * * @param $service * The machine-name of the shipping service the rate is for. * @param $price * A price array used to establish the base unit price for the shipping. * @param $order * If available, the order to which the shipping line item will belong. * * @return * The shipping line item with a calculated shipping rate. */ function commerce_shipping_service_rate_calculate($service, $price, $order_id = 0) { $shipping_service = commerce_shipping_service_load($service); // Create the new line item for the service rate. $line_item = commerce_shipping_line_item_new($service, $price, $order_id); // Set the price component of the unit price if it hasn't already been done. $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); $data = $line_item_wrapper->commerce_unit_price->data->value(); if (empty($data['components'])) { $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add( $line_item_wrapper->commerce_unit_price->value(), $shipping_service['price_component'], $line_item_wrapper->commerce_unit_price->value(), TRUE, FALSE ); } rules_invoke_all('commerce_shipping_calculate_rate', $line_item); return $line_item; } /** * Turns an array of shipping rates into a form element options array. * * @param $order * An order object with a shipping_rates property defined as an array of * shipping rate price arrays keyed by shipping service name. * * @return * An options array of calculated shipping rates labeled using the display * title of the shipping services. */ function commerce_shipping_service_rate_options($order) { $options = array(); foreach ($order->shipping_rates as $name => $line_item) { $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); $options[$name] = t('!shipping_service: !price', array( '!shipping_service' => commerce_shipping_line_item_title($line_item), '!price' => commerce_currency_format($line_item_wrapper->commerce_unit_price->amount->value(), $line_item_wrapper->commerce_unit_price->currency_code->value()), )); } // Allow modules to alter the options array generated for the rates. drupal_alter('commerce_shipping_service_rate_options', $options, $order); return $options; } /** * Caches shipping rates for an order. * * @param $method * The name of the shipping method the rates are being cached for. * @param $order * The order the rates were calculated for. * @param $rates * An array of base rate price arrays keyed by shipping service name. */ function commerce_shipping_rates_cache_set($method, $order, $rates) { cache_set($order->order_id . ':' . $method, $rates, 'cache_commerce_shipping_rates', CACHE_TEMPORARY); } /** * Retrieves cached shipping rates for an order. * * @param $method * The name of the shipping method the rates are being cached for. * @param $order * The order the rates were calculated for. * @param $timeout * Number of seconds after which cached rates should be considered invalid. * Defaults to 0, meaning cached rates are only good for the current page * request. * * @return * A cached array of base rate price arrays keyed by shipping service name or * FALSE if no cache existed or the cache is invalid based on the timeout * parameter if specified. */ function commerce_shipping_rates_cache_get($method, $order, $timeout = 0) { $cache = cache_get($order->order_id . ':' . $method, 'cache_commerce_shipping_rates'); // If no data was retrieved, return FALSE. if (empty($cache)) { return FALSE; } // If a timeout value was specified... if ($cache->created < REQUEST_TIME - $timeout) { return FALSE; } return $cache->data; } /** * Clears the shipping rates cache for the specified order. */ function commerce_shipping_rates_cache_clear($order) { cache_clear_all($order->order_id . ':', 'cache_commerce_shipping_rates', TRUE); } /** * Implements hook_flush_caches(). */ function commerce_shipping_flush_caches() { return array('cache_commerce_shipping_rates'); } /** * Implements hook_commerce_line_item_type_info(). */ function commerce_shipping_commerce_line_item_type_info() { $line_item_types = array(); $line_item_types['shipping'] = array( 'name' => t('Shipping'), 'description' => t('References a shipping method and displays the rate with the selected service title.'), 'add_form_submit_value' => t('Add shipping'), 'base' => 'commerce_shipping_line_item', ); return $line_item_types; } /** * Line item callback: configures the shipping line item type on module enable. */ function commerce_shipping_line_item_configuration($line_item_type) { $field_name = 'commerce_shipping_service'; $type = $line_item_type['type']; $field = field_info_field($field_name); $instance = field_info_instance('commerce_line_item', $field_name, $type); if (empty($field)) { $field = array( 'field_name' => $field_name, 'type' => 'list_text', 'cardinality' => 1, 'entity_types' => array('commerce_line_item'), 'translatable' => FALSE, 'locked' => TRUE, 'settings' => array( 'allowed_values_function' => 'commerce_shipping_service_options_list', ), ); $field = field_create_field($field); } if (empty($instance)) { $instance = array( 'field_name' => $field_name, 'entity_type' => 'commerce_line_item', 'bundle' => $type, 'label' => t('Shipping service'), 'required' => TRUE, 'settings' => array(), 'widget' => array( 'type' => 'options_select', 'weight' => 0, ), 'display' => array( 'display' => array( 'label' => 'hidden', 'weight' => 0, ), ), ); field_create_instance($instance); } } /** * Returns the title of a shipping line item's related shipping service. */ function commerce_shipping_line_item_title($line_item) { $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); // First try to get the title from the line item's data array. if (!empty($line_item->data['shipping_service']['display_title'])) { return $line_item->data['shipping_service']['display_title']; } // Then try to get the title from the shipping service. elseif ($line_item_wrapper->commerce_shipping_service->value() != NULL && $title = commerce_shipping_service_get_title($line_item_wrapper->commerce_shipping_service->value(), 'display_title')) { return $title; } // Fallback to the line item label. return $line_item->line_item_label; } /** * Returns the elements necessary to add a shipping line item through the line * item manager widget. */ function commerce_shipping_line_item_add_form($form, &$form_state) { // Collect the available shipping rates for this order. $order = $form_state['commerce_order']; commerce_shipping_collect_rates($order); // Store the available rates in the form. $form = array(); $form['#attached']['css'][] = drupal_get_path('module', 'commerce_shipping') . '/theme/commerce_shipping.admin.css'; $form['shipping_rates'] = array( '#type' => 'value', '#value' => $order->shipping_rates, ); // Create an options array based on the rated services. $options = commerce_shipping_service_rate_options($order); $options['manual'] = t('Manually specify a shipping service and rate.'); $form['shipping_service'] = array( '#type' => 'radios', '#title' => t('Shipping service'), '#options' => $options, '#default_value' => key($options), ); $form['custom_rate'] = array( '#type' => 'container', '#states' => array( 'visible' => array( ':input[name="commerce_line_items[und][actions][shipping_service]"]' => array('value' => 'manual'), ), ), ); $form['custom_rate']['shipping_service'] = array( '#type' => 'select', '#title' => t('Shipping service'), '#options' => commerce_shipping_service_options_list(), ); $form['custom_rate']['amount'] = array( '#type' => 'textfield', '#title' => t('Shipping rate'), '#default_value' => '', '#size' => 10, '#prefix' => '
', '#states' => array( 'visible' => array( ':input[name="commerce_line_items[und][actions][shipping_service]"]' => array('value' => 'manual'), ), ), ); $form['custom_rate']['currency_code'] = array( '#type' => 'select', '#options' => commerce_currency_code_options_list(), '#default_value' => commerce_default_currency(), '#suffix' => '
', ); return $form; } /** * Adds the selected shipping information to a new shipping line item. * * @param $line_item * The newly created line item object. * @param $element * The array representing the widget form element. * @param $form_state * The present state of the form upon the latest submission. * @param $form * The actual form array. * * @return * NULL if all is well or an error message if something goes wrong. */ function commerce_shipping_line_item_add_form_submit($line_item, $element, &$form_state, $form) { $order = $form_state['commerce_order']; // Ensure a quantity of 1. $line_item->quantity = 1; // Use a custom service and rate amount if specified. if ($element['actions']['shipping_service']['#value'] == 'manual') { $service = $element['actions']['custom_rate']['shipping_service']['#value']; $shipping_service = commerce_shipping_service_load($service); // Build the custom unit price array. $unit_price = array( 'amount' => commerce_currency_decimal_to_amount($element['actions']['custom_rate']['amount']['#value'], $element['actions']['custom_rate']['currency_code']['#value']), 'currency_code' => $element['actions']['custom_rate']['currency_code']['#value'], 'data' => array(), ); // Add a price component for the custom amount. $unit_price['data'] = commerce_price_component_add( $unit_price, $shipping_service['price_component'], $unit_price, TRUE, FALSE ); } else { // Otherwise use the values for the selected calculated service. $service = $element['actions']['shipping_service']['#value']; $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $element['actions']['shipping_rates']['#value'][$service]); $unit_price = $line_item_wrapper->commerce_unit_price->value(); } // Populate the line item with the appropriate data. commerce_shipping_line_item_populate($line_item, $service, $unit_price); } /** * Creates a new shipping line item populated with the proper shipping values. * * @param $service * The machine-name of the shipping service the line item represents. * @param $unit_price * A price array used to initialize the value of the line item's unit price. * @param $order_id * The ID of the order the line item belongs to. * @param $data * An array value to initialize the line item's data array with. * @param $type * The name of the line item type being created; defaults to 'shipping'. * * @return * The shipping line item for the specified service initialized to the given * unit price. */ function commerce_shipping_line_item_new($service, $unit_price, $order_id = 0, $data = array(), $type = 'shipping') { // Ensure a default product line item type. if (empty($type)) { $type = 'shipping'; } // Create the new line item. $line_item = entity_create('commerce_line_item', array( 'type' => $type, 'order_id' => $order_id, 'quantity' => 1, 'data' => $data, )); // Populate it with the shipping service and unit price data. commerce_shipping_line_item_populate($line_item, $service, $unit_price); // Allow other modules to add additional data to the line item if necessary. drupal_alter('commerce_shipping_line_item_new', $line_item); // Return the line item. return $line_item; } /** * Populates a shipping line item with the specified values. * * @param $service * The machine-name of the shipping service the line item represents. * @param $unit_price * A price array used to initialize the value of the line item's unit price. */ function commerce_shipping_line_item_populate($line_item, $service, $unit_price) { // Use the label to store the display title of the shipping service. $line_item->line_item_label = commerce_shipping_service_get_title($service, 'display_title'); $line_item->quantity = 1; // Store the full shipping status object in the data array. $line_item->data['shipping_service'] = commerce_shipping_service_load($service); // Set the service textfield and unit price. $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); $line_item_wrapper->commerce_shipping_service = $service; $line_item_wrapper->commerce_unit_price = $unit_price; } /** * Deletes all shipping line items on an order. * * @param $order * The order object to delete the shipping line items from. * @param $skip_save * Boolean indicating whether or not to skip saving the order in this function. */ function commerce_shipping_delete_shipping_line_items($order, $skip_save = FALSE) { $order_wrapper = entity_metadata_wrapper('commerce_order', $order); // When deleting more than one line item, metadata_wrapper will give problems // if deleting while looping through the line items. So first remove from order // and then delete the line items. $line_item_ids = array(); foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) { // If this line item is a shipping line item... if ($line_item_wrapper->type->value() == 'shipping') { // Store its ID for later deletion and remove the reference from the line // item reference field. $line_item_ids[] = $line_item_wrapper->line_item_id->value(); $order_wrapper->commerce_line_items->offsetUnset($delta); } } // If we found any shipping line items... if (!empty($line_item_ids)) { // First save the order to update the line item reference field value. if (!$skip_save) { commerce_order_save($order); } // Then delete the line items. commerce_line_item_delete_multiple($line_item_ids); } } /** * Implements hook_commerce_order_update(). * * When an order is updated, we have no way of knowing if new information on the * order demands a recalculation of shipping rates. To be safe, we simply clear * the cached rates for an order any time that order is updated. */ function commerce_shipping_commerce_order_update($order) { commerce_shipping_rates_cache_clear($order); } /** * Adds a shipping line item to an order. * * @param $line_item * An unsaved shipping line item that should be added to the order. * @param $order * The order to add the shipping line item to. * @param $skip_save * Boolean indicating whether or not to skip saving the order in this function. * * @return * The saved shipping line item object or FALSE on failure. */ function commerce_shipping_add_shipping_line_item($line_item, $order, $skip_save = FALSE) { // Do not proceed without a valid order. if (empty($order)) { return FALSE; } // Save the incoming line item now so we get its ID. commerce_line_item_save($line_item); // Add it to the order's line item reference value. $order_wrapper = entity_metadata_wrapper('commerce_order', $order); $order_wrapper->commerce_line_items[] = $line_item; // Save the updated order. if (!$skip_save) { commerce_order_save($order); } // Otherwise, we still need to update the order total to take for account // added shipping line item. else { commerce_order_calculate_total($order); } return $line_item; } /** * Implements hook_form_FORM_ID_alter(). */ function commerce_shipping_form_commerce_checkout_form_alter(&$form, &$form_state) { // Attach the JavaScript to the page to recalculate services if it is enabled // for this form. We do this in a form alter instead of the checkout pane's // form callback to ensure the whole initial form has been built. if (commerce_shipping_recalculate_services($form)) { // Build an array to limit validation errors to customer profile data and // the currently selected shipping service. $limit_validation = array(array('commerce_shipping', 'shipping_service')); foreach (commerce_customer_profile_types() as $type => $profile_type) { $limit_validation[] = array('customer_profile_' . $type); } // Add a hidden submit button to the page that we'll use to recalculate // shipping when the form is updated or a non-JS customer clicks the button. $form['commerce_shipping']['recalculate'] = array( '#type' => 'submit', '#value' => t('Recalculate shipping'), '#limit_validation_errors' => $limit_validation, '#validate' => array('commerce_shipping_recalculate_services_validate'), '#submit' => array('commerce_shipping_recalculate_services_submit'), '#weight' => -10, '#ajax' => array( 'callback' => 'commerce_shipping_recalculate_services_refresh', 'wrapper' => 'commerce-shipping-service-ajax-wrapper', 'event' => 'click', ), '#attached' => array( 'js' => array(drupal_get_path('module', 'commerce_shipping') . '/js/commerce_shipping.js'), ), ); // Hide the recalculation button via JavaScript if there are shipping rate // options or if there aren't any options and shipping service selection is // required for checkout. if (!empty($form['commerce_shipping']['shipping_rates']['#value']) || !variable_get('commerce_shipping_pane_require_service', FALSE)) { $form['commerce_shipping']['recalculate']['#states'] = array( 'invisible' => array( ':input[id^="edit-commerce-shipping-recalculate"]' => array('value' => t('Recalculate shipping')), ), ); } } } /** * Implements hook_commerce_customer_profile_copy_refresh_alter(). */ function commerce_shipping_commerce_customer_profile_copy_refresh_alter(&$commands, $form, $form_state) { // Add an AJAX command to click the shipping recalculation button after the // customer clicks a profile copy button. if (!empty($form['commerce_shipping']['recalculate'])) { $commands[] = ajax_command_invoke(NULL, 'commerceCheckShippingRecalculation', array()); } } /** * Determines whether or not automatic shipping service recalculation is valid * for the given form. */ function commerce_shipping_recalculate_services($form, $form_state = NULL, $ignore_shipping = FALSE) { // If the shipping services pane is on the form and it has been configured to // support automatic recalculation... if ((!empty($form['commerce_shipping']) || $ignore_shipping) && variable_get('commerce_shipping_recalculate_services', TRUE)) { // If no form state was passed in, we only need to determine whether or not // there is a customer profile pane on the current form that might trigger // shipping rate recalculation if its form elements are changed. if (empty($form_state)) { // Loop over all of the customer profile types. foreach (commerce_customer_profile_types() as $type => $profile_type) { // If its pane is on the current form... if (!empty($form['customer_profile_' . $type])) { // Enable shipping service recalculation on the form. return TRUE; } } return FALSE; } else { // If we did get a form state, extract the order from it. $order = $form_state['order']; // Loop over the customer profile types looking for any represented on the // current checkout form. foreach (commerce_customer_profile_types() as $type => $profile_type) { // If the current type is on the form... if (!empty($form['customer_profile_' . $type])) { // Load the checkout pane to find its checkout pane validate callback. $checkout_pane = commerce_checkout_pane_load('customer_profile_' . $type); // If it has a validate callback... if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_validate')) { // Disallow shipping service recalculation on the current form build // if the current form state values do not pass validation. $validate = !empty($form_state['values'][$checkout_pane['pane_id']]) && $callback($form, $form_state, $checkout_pane, $order); if (!$validate) { return FALSE; } } } } // Otherwise enable recalculation since we'll have the data we need to // update or insert the customer profiles represented on the form, giving // us the address data we need to recalculate shipping services. return TRUE; } } return FALSE; }