You are here

uc_ups.module in Ubercart 5

Shipping quote module that interfaces with www.ups.com to get rates for small package shipments.

Coded by Lyle Mantooth

File

shipping/uc_ups/uc_ups.module
View source
<?php

/**
 * @file
 * Shipping quote module that interfaces with www.ups.com to get rates for small
 * package shipments.
 *
 * Coded by Lyle Mantooth
 */

/******************************************************************************
 * Drupal Hooks                                                               *
 ******************************************************************************/

/**
 * Implementation of hook_menu().
 */
function uc_ups_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/settings/quotes/methods/ups',
      'access' => user_access('configure quotes'),
      'title' => t('UPS'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'uc_ups_admin_settings',
      'type' => MENU_LOCAL_TASK,
    );
  }
  else {
    $items[] = array(
      'path' => 'admin/store/orders/' . arg(3) . '/shipments/ups',
      'access' => user_access('fulfill orders'),
      'title' => t('UPS shipment'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_ups_confirm_shipment',
        arg(3),
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/orders/' . arg(3) . '/shipments/labels/ups',
      'access' => user_access('fulfill orders'),
      'callback' => 'theme',
      'callback arguments' => array(
        'uc_ups_label_image',
      ),
      'type' => MENU_CALLBACK,
    );
    drupal_add_css(drupal_get_path('module', 'uc_ups') . '/uc_ups.css');
  }
  return $items;
}

/**
 * Implementation of hook_form_alter().
 *
 * Add package type to products.
 *
 * @see uc_product_form
 * @see uc_ups_product_alte_validate
 */
function uc_ups_form_alter($form_id, &$form) {
  $node = $form['#node'];
  if (is_object($node) && $form_id == $node->type . '_node_form' && isset($form['base']['dimensions']) && in_array($node->type, module_invoke_all('product_types'))) {
    $enabled = variable_get('uc_quote_enabled', array());
    $weight = variable_get('uc_quote_method_weight', array(
      'ups' => 0,
    ));
    $ups = array(
      '#type' => 'fieldset',
      '#title' => t('UPS product description'),
      '#collapsible' => true,
      '#collapsed' => $enabled['ups'] == false || uc_product_get_shipping_type($node) != 'small_package',
      '#weight' => $weight['ups'],
      '#tree' => true,
    );
    $ups['pkg_type'] = array(
      '#type' => 'select',
      '#title' => t('Package type'),
      '#options' => _uc_ups_pkg_types(),
      '#default_value' => $node->ups['pkg_type'] ? $node->ups['pkg_type'] : '02',
    );
    $form['shipping']['ups'] = $ups;
    if ($enabled['ups']) {
      $form['#validate']['uc_ups_product_alter_validate'] = array();
    }
  }
}

/**
 * Validation handler for UPS product fields.
 */
function uc_ups_product_alter_validate($form_id, $form_values) {
  $enabled = variable_get('uc_quote_enabled', array());
  if ($form_values['shippable'] && ($form_values['shipping_type'] == 'small_package' || empty($form_values['shipping_type']) && variable_get('uc_store_shipping_type', 'small_package') == 'small_package')) {
    if ($form_values['ups']['pkg_type'] == '02' && (empty($form_values['length']) || empty($form_values['width']) || empty($form_values['height']))) {
      form_set_error('base][dimensions', t('Dimensions are required for custom packaging.'));
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function uc_ups_nodeapi(&$node, $op) {
  if (in_array($node->type, module_invoke_all('product_types'))) {
    switch ($op) {
      case 'insert':
      case 'update':
        if (isset($node->ups)) {
          $ups_values = $node->ups;
          if (!$node->revision) {
            db_query("DELETE FROM {uc_ups_products} WHERE vid = %d", $node->vid);
          }
          db_query("INSERT INTO {uc_ups_products} (vid, nid, pkg_type) VALUES (%d, %d, '%s')", $node->vid, $node->nid, $ups_values['pkg_type']);
        }
        break;
      case 'load':
        if (uc_product_get_shipping_type($node) == 'small_package') {
          return array(
            'ups' => db_fetch_array(db_query("SELECT * FROM {uc_ups_products} WHERE vid = %d", $node->vid)),
          );
        }
        break;
      case 'delete':
        db_query("DELETE FROM {uc_ups_products} WHERE nid = %d", $node->nid);
        break;
      case 'delete revision':
        db_query("DELETE FROM {uc_ups_products} WHERE vid = %d", $node->vid);
        break;
    }
  }
}

/******************************************************************************
 * Workflow-ng Hooks                                                          *
 ******************************************************************************/

/**
 * Implementation of hook_configuration().
 *
 * Connect the UPS quote action and event.
 */
function uc_ups_configuration() {
  $enabled = variable_get('uc_quote_enabled', array());
  $configurations = array(
    'uc_ups_get_quote' => array(
      '#label' => t('Shipping quote from UPS'),
      '#event' => 'get_quote_from_ups',
      '#module' => 'uc_ups',
      '#active' => $enabled['ups'],
    ),
  );
  $action = workflow_ng_use_action('uc_quote_action_get_quote', array(
    '#label' => t('Fetch a shipping quote'),
  ));
  $configurations['uc_ups_get_quote'] = workflow_ng_configure($configurations['uc_ups_get_quote'], $action);
  return $configurations;
}

/******************************************************************************
 * Übercart Hooks                                                             *
 ******************************************************************************/

/**
 * Implementation of Übercart's hook_shipping_type().
 */
function uc_ups_shipping_type() {
  $weight = variable_get('uc_quote_type_weight', array(
    'small_package' => 0,
  ));
  $types = array();
  $types['small_package'] = array(
    'id' => 'small_package',
    'title' => t('Small packages'),
    'weight' => $weight['small_package'],
  );
  return $types;
}

/**
 * Implementation of Übercart's hook_shipping_method().
 */
function uc_ups_shipping_method() {
  $methods = array();
  $enabled = variable_get('uc_quote_enabled', array());
  $weight = variable_get('uc_quote_method_weight', array(
    'ups' => 0,
  ));
  $methods['ups'] = array(
    'id' => 'ups',
    'module' => 'uc_ups',
    'title' => t('UPS'),
    'enabled' => $enabled['ups'],
    'quote' => array(
      'type' => 'small_package',
      'callback' => 'uc_ups_quote',
      'accessorials' => _uc_ups_service_list(),
    ),
    'ship' => array(
      'type' => 'small_package',
      'callback' => 'uc_ups_fulfill_order',
      'pkg_types' => _uc_ups_pkg_types(),
    ),
    'cancel' => 'uc_ups_void_shipment',
    'weight' => $weight['ups'],
  );
  return $methods;
}

/**
 * Implementation of Übercart's hook_store_status().
 *
 * Let the administrator know that the UPS account information has not been
 * filled out.
 */
function uc_ups_store_status() {
  $messages = array();
  $access = variable_get('uc_ups_access_license', '') != '';
  $account = variable_get('uc_ups_shipper_number', '') != '';
  $user = variable_get('uc_ups_user_id', '') != '';
  $password = variable_get('uc_ups_password', '') != '';
  if ($access && $account && $user && $password) {
    $messages[] = array(
      'status' => 'ok',
      'title' => t('UPS Online Tools'),
      'desc' => t('Information needed to access UPS Online Tools has been entered.'),
    );
  }
  else {
    $messages[] = array(
      'status' => 'error',
      'title' => t('UPS Online Tools'),
      'desc' => t('More information is needed to access UPS Online Tools. Please enter it <a href="!url">here</a>.', array(
        '!url' => url('admin/store/settings/quotes/methods/ups'),
      )),
    );
  }
  return $messages;
}

/******************************************************************************
 * Menu Callbacks                                                             *
 ******************************************************************************/

/**
 * UPS Online Tool settings.
 *
 * Record UPS account information neccessary to use the service. Allow testing
 * or production mode. Configure which UPS services are quoted to customers.
 *
 * @ingroup forms
 * @see uc_admin_settings_validate
 */
function uc_ups_admin_settings() {
  $form = array();
  $form['uc_ups_access_license'] = array(
    '#type' => 'textfield',
    '#title' => t('UPS OnLine Tools XML Access Key'),
    '#default_value' => variable_get('uc_ups_access_license', ''),
    '#required' => true,
  );
  $form['uc_ups_shipper_number'] = array(
    '#type' => 'textfield',
    '#title' => t('UPS Shipper #'),
    '#description' => t('The 6-character string identifying your UPS account as a shipper.'),
    '#default_value' => variable_get('uc_ups_shipper_number', ''),
    '#required' => true,
  );
  $form['uc_ups_user_id'] = array(
    '#type' => 'textfield',
    '#title' => t('UPS.com user ID'),
    '#default_value' => variable_get('uc_ups_user_id', ''),
    '#required' => true,
  );
  $form['uc_ups_password'] = array(
    '#type' => 'password',
    '#title' => t('Password'),
    '#default_value' => variable_get('uc_ups_password', ''),
  );
  $form['uc_ups_connection_address'] = array(
    '#type' => 'select',
    '#title' => t('Mode'),
    '#description' => t('Quotes and shipments requested in Testing mode will not be picked up or charged to your account.'),
    '#options' => array(
      'https://wwwcie.ups.com/ups.app/xml/' => t('Testing'),
      'https://www.ups.com/ups.app/xml/' => t('Production'),
    ),
    '#default_value' => variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/'),
  );
  $form['uc_ups_services'] = array(
    '#type' => 'checkboxes',
    '#title' => t('UPS services'),
    '#default_value' => variable_get('uc_ups_services', _uc_ups_service_list()),
    '#options' => _uc_ups_service_list(),
    '#description' => t('Select the UPS services that are available to customers.'),
  );
  $form['uc_ups_pickup_type'] = array(
    '#type' => 'select',
    '#title' => t('Pickup type'),
    '#options' => array(
      '01' => 'Daily Pickup',
      '03' => 'Customer Counter',
      '06' => 'One Time Pickup',
      '07' => 'On Call Air',
      '11' => 'Suggested Retail Rates',
      '19' => 'Letter Center',
      '20' => 'Air Service Center',
    ),
    '#default_value' => variable_get('uc_ups_pickup_type', '01'),
  );
  $form['uc_ups_classification'] = array(
    '#type' => 'select',
    '#title' => t('UPS Customer classification'),
    '#options' => array(
      '01' => t('Wholesale'),
      '03' => t('Occasional'),
      '04' => t('Retail'),
    ),
    '#default_value' => variable_get('uc_ups_classification', '04'),
    '#description' => t('The kind of customer you are to UPS. For daily pickups the default is wholesale; for customer counter pickups the default is retail; for other pickups the default is occasional.'),
  );
  $form['uc_ups_negotiated_rates'] = array(
    '#type' => 'radios',
    '#title' => t('Negotiated rates'),
    '#default_value' => variable_get('uc_ups_negotiated_rates', 0),
    '#options' => array(
      1 => t('Yes'),
      0 => t('No'),
    ),
    '#description' => t('Is your UPS account receiving negotiated rates on shipments?'),
  );
  $form['uc_ups_residential_quotes'] = array(
    '#type' => 'radios',
    '#title' => t('Assume UPS shipping quotes will be delivered to'),
    '#default_value' => variable_get('uc_ups_residential_quotes', 0),
    '#options' => array(
      0 => t('Business locations'),
      1 => t('Residential locations (extra fees)'),
    ),
  );
  $form['uc_ups_markup_type'] = array(
    '#type' => 'select',
    '#title' => t('Markup type'),
    '#default_value' => variable_get('uc_ups_markup_type', 'percentage'),
    '#options' => array(
      'percentage' => t('Percentage (%)'),
      'multiplier' => t('Multiplier (×)'),
      'currency' => t('Addition (!currency)', array(
        '!currency' => variable_get('uc_currency_sign', '$'),
      )),
    ),
  );
  $form['uc_ups_markup'] = array(
    '#type' => 'textfield',
    '#title' => t('Shipping rate markup'),
    '#default_value' => variable_get('uc_ups_markup', '0'),
    '#description' => t('Markup shipping rate quote by currency amount, percentage, or multiplier.'),
  );
  $form['uc_ups_all_in_one'] = array(
    '#type' => 'radios',
    '#title' => t('Product packages'),
    '#default_value' => variable_get('uc_ups_all_in_one', 1),
    '#options' => array(
      0 => t('Each in its own package'),
      1 => t('All in one'),
    ),
    '#description' => t('Indicate whether each product is quoted as shipping separately or all in one package.'),
  );
  $form['uc_ups_unit_system'] = array(
    '#type' => 'select',
    '#title' => t('System of measurement'),
    '#default_value' => variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in')),
    '#options' => array(
      'in' => t('British'),
      'cm' => t('Metric'),
    ),
    '#description' => t('Choose the standard system of measurement for your country.'),
  );
  $form['uc_ups_insurance'] = array(
    '#type' => 'checkbox',
    '#title' => t('Package insurance'),
    '#default_value' => variable_get('uc_ups_insurance', TRUE),
    '#description' => t('When enabled, products are insured for their full value.'),
  );
  return system_settings_form($form);
}

/**
 * Validation handler for uc_ups_admin_settings.
 *
 * Require password only if it hasn't been set.
 */
function uc_ups_admin_settings_validate($form_id, $form_values, $form) {
  $old_password = variable_get('uc_ups_password', '');
  if (!$form_values['uc_ups_password']) {
    if ($old_password) {
      form_set_value($form['uc_ups_password'], $old_password);
    }
    else {
      form_set_error('uc_ups_password', t('Password field is required.'));
    }
  }
}

/******************************************************************************
 * Module Functions                                                           *
 ******************************************************************************/

/**
 * Return XML access request to be prepended to all requests to the UPS webservice.
 */
function uc_ups_access_request() {
  $access = variable_get('uc_ups_access_license', '');
  $user = variable_get('uc_ups_user_id', '');
  $password = variable_get('uc_ups_password', '');
  return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<AccessRequest xml:lang=\"en-US\">\n  <AccessLicenseNumber>{$access}</AccessLicenseNumber>\n  <UserId>{$user}</UserId>\n  <Password>{$password}</Password>\n</AccessRequest>\n";
}

/**
 * Construct an XML quote request.
 *
 * @param $packages
 *   Array of packages received from the cart.
 * @param $origin
 *   Delivery origin address information.
 * @param $destination
 *   Delivery destination address information.
 * @param $ups_service
 *   UPS service code (refers to UPS Ground, Next-Day Air, etc.).
 * @return
 *   RatingServiceSelectionRequest XML document to send to UPS
 */
function uc_ups_shipping_quote($packages, $origin, $destination, $ups_service) {
  $store['name'] = variable_get('uc_store_name', NULL);
  $store['owner'] = variable_get('uc_store_owner', NULL);
  $store['email'] = variable_get('uc_store_email', NULL);
  $store['email_from'] = variable_get('uc_store_email', NULL);
  $store['phone'] = variable_get('uc_store_phone', NULL);
  $store['fax'] = variable_get('uc_store_fax', NULL);
  $store['street1'] = variable_get('uc_store_street1', NULL);
  $store['street2'] = variable_get('uc_store_street2', NULL);
  $store['city'] = variable_get('uc_store_city', NULL);
  $store['zone'] = variable_get('uc_store_zone', NULL);
  $store['postal_code'] = variable_get('uc_store_postal_code', NULL);
  $store['country'] = variable_get('uc_store_country', 840);
  $account = variable_get('uc_ups_shipper_number', '');
  $ua = explode(' ', $_SERVER['HTTP_USER_AGENT']);
  $user_agent = $ua[0];
  $services = _uc_ups_service_list();
  $service = array(
    'code' => $ups_service,
    'description' => $services[$ups_service],
  );
  $pkg_types = _uc_ups_pkg_types();
  $shipper_zone = uc_get_zone_code($store['zone']);
  $shipper_country = uc_get_country_data(array(
    'country_id' => $store['country'],
  ));
  $shipper_country = $shipper_country[0]['country_iso_code_2'];
  $shipper_zip = $store['postal_code'];
  $shipto_zone = uc_get_zone_code($destination->zone);
  $shipto_country = uc_get_country_data(array(
    'country_id' => $destination->country,
  ));
  $shipto_country = $shipto_country[0]['country_iso_code_2'];
  $shipto_zip = $destination->postal_code;
  $shipfrom_zone = uc_get_zone_code($origin->zone);
  $shipfrom_country = uc_get_country_data(array(
    'country_id' => $origin->country,
  ));
  $shipfrom_country = $shipfrom_country[0]['country_iso_code_2'];
  $shipfrom_zip = $origin->postal_code;
  $ups_units = variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'));
  switch ($ups_units) {
    case 'in':
      $units = 'LBS';
      $unit_name = 'Pounds';
      break;
    case 'cm':
      $units = 'KGS';
      $unit_name = 'Kilograms';
      break;
  }
  $shipment_weight = 0;
  $package_schema = '';
  foreach ($packages as $package) {
    $qty = $package->qty;
    for ($i = 0; $i < $qty; $i++) {
      $package_type = array(
        'code' => $package->pkg_type,
        'description' => $pkg_types[$package->pkg_type],
      );
      $package_schema .= "<Package>";
      $package_schema .= "<PackagingType>";
      $package_schema .= "<Code>" . $package_type['code'] . "</Code>";

      //$package_schema .= "<Description>". $package_type['description'] ."</Description>";
      $package_schema .= "</PackagingType>";
      if ($package->pkg_type == '02' && $package->length && $package->width && $package->height) {
        if ($package->length < $package->width) {
          list($package->length, $package->width) = array(
            $package->width,
            $package->length,
          );
        }
        $package_schema .= "<Dimensions>";
        $package_schema .= "<UnitOfMeasurement>";
        $conversion = uc_weight_conversion($package->length_units, variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in')));
        $package_schema .= "<Code>" . strtoupper(variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'))) . "</Code>";
        $package_schema .= "</UnitOfMeasurement>";
        $package_schema .= "<Length>" . number_format($package->length * $conversion, 2, '.', '') . "</Length>";
        $package_schema .= "<Width>" . number_format($package->width * $conversion, 2, '.', '') . "</Width>";
        $package_schema .= "<Height>" . number_format($package->height * $conversion, 2, '.', '') . "</Height>";
        $package_schema .= "</Dimensions>";
      }
      $size = $package->length * $conversion + 2 * $conversion * ($package->width + $package->height);
      switch ($ups_units) {
        case 'in':
          $conversion = uc_weight_conversion($package->weight_units, 'lb');
          break;
        case 'cm':
          $conversion = uc_weight_conversion($package->weight_units, 'kg');
          break;
      }
      $weight = max(1, $package->weight * $conversion);
      $shipment_weight += $weight;
      $package_schema .= "<PackageWeight>";
      $package_schema .= "<UnitOfMeasurement>";
      $package_schema .= "<Code>{$units}</Code>";
      $package_schema .= "<Description>{$unit_name}</Description>";
      $package_schema .= "</UnitOfMeasurement>";
      $package_schema .= "<Weight>" . number_format($weight, 1, '.', '') . "</Weight>";
      $package_schema .= "</PackageWeight>";
      if ($size > 130 && $size <= 165) {
        $package_schema .= "<LargePackageIndicator/>";
      }
      if (variable_get('uc_ups_insurance', TRUE)) {
        $package_schema .= "<PackageServiceOptions>";
        $package_schema .= "<InsuredValue>";
        $package_schema .= "<CurrencyCode>" . variable_get('uc_currency_code', 'USD') . "</CurrencyCode>";
        $package_schema .= "<MonetaryValue>" . $package->price . "</MonetaryValue>";
        $package_schema .= "</InsuredValue>";
        $package_schema .= "</PackageServiceOptions>";
      }
      $package_schema .= "</Package>";
    }
  }
  $schema = uc_ups_access_request() . "\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RatingServiceSelectionRequest xml:lang=\"en-US\">\n  <Request>\n    <TransactionReference>\n      <CustomerContext>Complex Rate Request</CustomerContext>\n      <XpciVersion>1.0001</XpciVersion>\n    </TransactionReference>\n    <RequestAction>Rate</RequestAction>\n    <RequestOption>rate</RequestOption>\n  </Request>\n  <PickupType>\n    <Code>" . variable_get('uc_ups_pickup_type', '01') . "</Code>\n  </PickupType>\n  <CustomerClassification>\n    <Code>" . variable_get('uc_ups_classification', '04') . "</Code>\n  </CustomerClassification>\n  <Shipment>\n    <Shipper>\n      <ShipperNumber>" . variable_get('uc_ups_shipper_number', '') . "</ShipperNumber>\n      <Address>\n        <City>" . $store['city'] . "</City>\n        <StateProvinceCode>{$shipper_zone}</StateProvinceCode>\n        <PostalCode>{$shipper_zip}</PostalCode>\n        <CountryCode>{$shipper_country}</CountryCode>\n      </Address>\n    </Shipper>\n    <ShipTo>\n      <Address>\n        <StateProvinceCode>{$shipto_zone}</StateProvinceCode>\n        <PostalCode>{$shipto_zip}</PostalCode>\n        <CountryCode>{$shipto_country}</CountryCode>\n      ";
  if (variable_get('uc_ups_residential_quotes', 0)) {
    $schema .= "<ResidentialAddressIndicator/>\n      ";
  }
  $schema .= "</Address>\n    </ShipTo>\n    <ShipFrom>\n      <Address>\n        <StateProvinceCode>{$shipfrom_zone}</StateProvinceCode>\n        <PostalCode>{$shipfrom_zip}</PostalCode>\n        <CountryCode>{$shipfrom_country}</CountryCode>\n      </Address>\n    </ShipFrom>\n    <ShipmentWeight>\n      <UnitOfMeasurement>\n        <Code>{$units}</Code>\n        <Description>{$unit_name}</Description>\n      </UnitOfMeasurement>\n      <Weight>" . number_format($shipment_weight, 1, '.', '') . "</Weight>\n    </ShipmentWeight>\n    <Service>\n      <Code>{$service[code]}</Code>\n      <Description>{$service[description]}</Description>\n    </Service>\n    ";
  $schema .= $package_schema;
  if (variable_get('uc_ups_negotiated_rates', false)) {
    $schema .= "<RateInformation>\n          <NegotiatedRatesIndicator/>\n        </RateInformation>";
  }
  $schema .= "</Shipment>\n</RatingServiceSelectionRequest>";
  return $schema;
}

/**
 * Construct an XML shippment request.
 *
 * @param $packages
 *   Array of packages received from the cart.
 * @param $origin
 *   Delivery origin address information.
 * @param $destination
 *   Delivery destination address information.
 * @param $ups_service
 *   UPS service code (refers to UPS Ground, Next-Day Air, etc.).
 * @return
 *   ShipConfirm XML document to send to UPS
 */
function uc_ups_shipment_request($packages, $origin, $destination, $ups_service) {
  $store['name'] = variable_get('uc_store_name', NULL);
  $store['owner'] = variable_get('uc_store_owner', NULL);
  $store['email'] = variable_get('uc_store_email', NULL);
  $store['email_from'] = variable_get('uc_store_email', NULL);
  $store['phone'] = variable_get('uc_store_phone', NULL);
  $store['fax'] = variable_get('uc_store_fax', NULL);
  $store['street1'] = variable_get('uc_store_street1', NULL);
  $store['street2'] = variable_get('uc_store_street2', NULL);
  $store['city'] = variable_get('uc_store_city', NULL);
  $store['zone'] = variable_get('uc_store_zone', NULL);
  $store['postal_code'] = variable_get('uc_store_postal_code', NULL);
  $store['country'] = variable_get('uc_store_country', 840);
  $account = variable_get('uc_ups_shipper_number', '');
  $ua = explode(' ', $_SERVER['HTTP_USER_AGENT']);
  $user_agent = $ua[0];
  $services = _uc_ups_service_list();
  $service = array(
    'code' => $ups_service,
    'description' => $services[$ups_service],
  );
  $pkg_types = _uc_ups_pkg_types();
  $shipper_zone = uc_get_zone_code($store['zone']);
  $shipper_country = uc_get_country_data(array(
    'country_id' => $store['country'],
  ));
  $shipper_country = $shipper_country[0]['country_iso_code_2'];
  $shipper_zip = $store['postal_code'];
  $shipto_zone = uc_get_zone_code($destination->zone);
  $shipto_country = uc_get_country_data(array(
    'country_id' => $destination->country,
  ));
  $shipto_country = $shipto_country[0]['country_iso_code_2'];
  $shipto_zip = $destination->postal_code;
  $shipfrom_zone = uc_get_zone_code($origin->zone);
  $shipfrom_country = uc_get_country_data(array(
    'country_id' => $origin->country,
  ));
  $shipfrom_country = $shipfrom_country[0]['country_iso_code_2'];
  $shipfrom_zip = $origin->postal_code;
  $ups_units = variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'));
  $package_schema = '';
  foreach ($packages as $package) {
    $qty = $package->qty;
    for ($i = 0; $i < $qty; $i++) {
      $package_type = array(
        'code' => $package->pkg_type,
        'description' => $pkg_types[$package->pkg_type],
      );
      $package_schema .= "<Package>";
      $package_schema .= "<PackagingType>";
      $package_schema .= "<Code>" . $package_type['code'] . "</Code>";
      $package_schema .= "</PackagingType>";
      if ($package->pkg_type == '02' && $package->length && $package->width && $package->height) {
        if ($package->length < $package->width) {
          list($package->length, $package->width) = array(
            $package->width,
            $package->length,
          );
        }
        $package_schema .= "<Dimensions>";
        $package_schema .= "<UnitOfMeasurement>";
        $conversion = constant(strtoupper($package->length_units) . '_TO_' . strtoupper(variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'))));
        $package_schema .= "<Code>" . strtoupper(variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'))) . "</Code>";
        $package_schema .= "</UnitOfMeasurement>";
        $package_schema .= "<Length>" . (floor($package->length * $conversion) + 1) . "</Length>";
        $package_schema .= "<Width>" . (floor($package->width * $conversion) + 1) . "</Width>";
        $package_schema .= "<Height>" . (floor($package->height * $conversion) + 1) . "</Height>";
        $package_schema .= "</Dimensions>";
      }
      $size = $package->length * $conversion + 2 * $conversion * ($package->width + $package->height);
      switch ($ups_units) {
        case 'in':
          $conversion = uc_weight_conversion($package->weight_units, 'lb');
          break;
        case 'cm':
          $conversion = uc_weight_conversion($package->weight_units, 'kg');
          break;
      }
      $weight = $package->weight * $conversion;
      $package_schema .= "<PackageWeight>";
      $package_schema .= "<UnitOfMeasurement>";
      $package_schema .= "<Code>{$units}</Code>";
      $package_schema .= "<Description>{$unit_name}</Description>";
      $package_schema .= "</UnitOfMeasurement>";
      $package_schema .= "<Weight>" . number_format($weight, 1, '.', '') . "</Weight>";
      $package_schema .= "</PackageWeight>";
      if ($size > 130 && $size <= 165) {
        $package_schema .= "<LargePackageIndicator/>";
      }
      $package_schema .= "<PackageServiceOptions>";
      $package_schema .= "<InsuredValue>";
      $package_schema .= "<CurrencyCode>" . variable_get('uc_currency_code', 'USD') . "</CurrencyCode>";
      $package_schema .= "<MonetaryValue>" . number_format($package->price, 2, '.', '') . "</MonetaryValue>";
      $package_schema .= "</InsuredValue>";
      $package_schema .= "</PackageServiceOptions>";
      $package_schema .= "</Package>";
    }
  }
  $schema = uc_ups_access_request() . "\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ShipmentConfirmRequest xml:lang=\"en-US\">\n  <Request>\n    <TransactionReference>\n      <CustomerContext>Complex Rate Request</CustomerContext>\n      <XpciVersion>1.0001</XpciVersion>\n    </TransactionReference>\n    <RequestAction>ShipConfirm</RequestAction>\n    <RequestOption>validate</RequestOption>\n  </Request>\n  <Shipment>";
  $schema .= "<Shipper>";
  $schema .= "<Name>" . $store['name'] . "</Name>";
  $schema .= "<ShipperNumber>" . variable_get('uc_ups_shipper_number', '') . "</ShipperNumber>";
  if ($store['phone']) {
    $schema .= "<PhoneNumber>" . $store['phone'] . "</PhoneNumber>";
  }
  if ($store['fax']) {
    $schema .= "<FaxNumber>" . $store['fax'] . "</FaxNumber>";
  }
  if ($store['email']) {
    $schema .= "<EMailAddress>" . $store['email'] . "</EMailAddress>";
  }
  $schema .= "<Address>";
  $schema .= "<AddressLine1>" . $store['street1'] . "</AddressLine1>";
  if ($store['street2']) {
    $schema .= "<AddressLine2>" . $store['street2'] . "</AddressLine2>";
  }
  $schema .= "<City>" . $store['city'] . "</City>";
  $schema .= "<StateProvinceCode>{$shipper_zone}</StateProvinceCode>";
  $schema .= "<PostalCode>{$shipper_zip}</PostalCode>";
  $schema .= "<CountryCode>{$shipper_country}</CountryCode>";
  $schema .= "</Address>";
  $schema .= "</Shipper>";
  $schema .= "<ShipTo>";
  $schema .= "<CompanyName>" . $destination->company . "</CompanyName>";
  $schema .= "<AttentionName>" . $destination->first_name . ' ' . $destination->last_name . "</AttentionName>";
  $schema .= "<PhoneNumber>" . $destination->phone . "</PhoneNumber>";
  $schema .= "<EMailAddress>" . $destination->email . "</EMailAddress>";
  $schema .= "<Address>";
  $schema .= "<AddressLine1>" . $destination->street1 . "</AddressLine1>";
  if ($destination->street2) {
    $schema .= "<AddressLine2>" . $destination->street2 . "</AddressLine2>";
  }
  $schema .= "<City>" . $destination->city . "</City>";
  $schema .= "<StateProvinceCode>{$shipto_zone}</StateProvinceCode>";
  $schema .= "<PostalCode>{$shipto_zip}</PostalCode>";
  $schema .= "<CountryCode>{$shipto_country}</CountryCode>";
  if ($destination->residence) {
    $schema .= "<ResidentialAddressIndicator/>";
  }
  $schema .= "</Address>";
  $schema .= "</ShipTo>";
  $schema .= "<ShipFrom>";
  $schema .= "<CompanyName>" . $origin->company . "</CompanyName>";
  $schema .= "<AttentionName>" . $origin->first_name . ' ' . $origin->last_name . "</AttentionName>";
  $schema .= "<PhoneNumber>" . $origin->phone . "</PhoneNumber>";
  $schema .= "<EMailAddress>" . $origin->email . "</EMailAddress>";
  $schema .= "<Address>";
  $schema .= "<AddressLine1>" . $origin->street1 . "</AddressLine1>";
  if ($origin->street2) {
    $schema .= "<AddressLine2>" . $origin->street2 . "</AddressLine2>";
  }
  $schema .= "<City>" . $origin->city . "</City>";
  $schema .= "<StateProvinceCode>{$shipfrom_zone}</StateProvinceCode>";
  $schema .= "<PostalCode>{$shipfrom_zip}</PostalCode>";
  $schema .= "<CountryCode>{$shipfrom_country}</CountryCode>";
  $schema .= "</Address>";
  $schema .= "</ShipFrom>";
  $schema .= "<PaymentInformation>";
  $schema .= "<Prepaid>";
  $schema .= "<BillShipper>";
  $schema .= "<AccountNumber>{$account}</AccountNumber>";
  $schema .= "</BillShipper>";
  $schema .= "</Prepaid>";
  $schema .= "</PaymentInformation>";
  if (variable_get('uc_ups_negotiated_rates', false)) {
    $schema .= "<RateInformation>\n          <NegotiatedRatesIndicator/>\n        </RateInformation>";
  }
  $schema .= "<Service>";
  $schema .= "<Code>{$service[code]}</Code>";
  $schema .= "<Description>{$service[description]}</Description>";
  $schema .= "</Service>";
  $schema .= $package_schema;
  $schema .= "</Shipment>";
  $schema .= "<LabelSpecification>";
  $schema .= "<LabelPrintMethod>";
  $schema .= "<Code>GIF</Code>";
  $schema .= "</LabelPrintMethod>";
  $schema .= "<LabelImageFormat>";
  $schema .= "<Code>GIF</Code>";
  $schema .= "</LabelImageFormat>";
  $schema .= "</LabelSpecification>";
  $schema .= "</ShipmentConfirmRequest>";
  return $schema;
}

/**
 * Callback for retrieving a UPS shipping quote.
 *
 * Request a quote for each enabled UPS Service. Therefore, the quote will
 * take longer to display to the user for each option the customer has available.
 *
 * @param $products
 *   Array of cart contents.
 * @param $details
 *   Order details other than product information.
 * @return
 *   JSON object containing rate, error, and debugging information.
 */
function uc_ups_quote($products, $details) {
  include_once drupal_get_path('module', 'uc_store') . '/includes/simplexml.php';
  $quotes = array();
  $method = uc_ups_shipping_method();
  $addresses = array(
    (array) variable_get('uc_quote_store_default_address', new stdClass()),
  );
  $key = 0;
  $last_key = 0;
  $packages = array();
  if (variable_get('uc_ups_all_in_one', true) && count($products) > 1) {
    foreach ($products as $product) {
      if ($product->nid) {

        // Packages are grouped by the address from which they will be
        // shipped. We will keep track of the different addresses in an array
        // and use their keys for the array of packages.
        $address = (array) uc_quote_get_default_shipping_address($product->nid);
        $key = array_search($address, $addresses);
        if ($key === false) {

          // This is a new address. Increment the address counter $last_key
          // instead of using [] so that it can be used in $packages and
          // $addresses.
          $addresses[++$last_key] = $address;
          $key = $last_key;
        }
      }

      // Add this product to the last package from the found address or start
      // a new package.
      if (isset($packages[$key]) && count($packages[$key])) {
        $package = array_pop($packages[$key]);
      }
      else {
        $package = _uc_ups_new_package();
      }
      $weight = $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
      $package->weight += $weight;
      $package->price += $product->price * $product->qty;
      $conversion = uc_weight_conversion($product->length_units, 'in');
      $package->length = max($product->length * $conversion, $package->length);
      $package->width = max($product->width * $conversion, $package->width);
      $package->height = max($product->height * $conversion, $package->height);
      $packages[$key][] = $package;
    }
    foreach ($packages as $addr_key => $shipment) {
      foreach ($shipment as $key => $package) {
        if (!$package->weight) {
          unset($packages[$addr_key][$key]);
          continue;
        }
        elseif ($package->weight > 150) {

          // UPS has a weight limit on packages of 150 lbs. Pretend the
          // products can be divided into enough packages.
          $qty = floor($package->weight / 150) + 1;
          $package->qty = $qty;
          $package->weight /= $qty;
          $package->price /= $qty;
        }
      }
    }
  }
  else {
    foreach ($products as $product) {
      $key = 0;
      if ($product->nid) {
        $address = (array) uc_quote_get_default_shipping_address($product->nid);
        $key = array_search($address, $addresses);
        if ($key === false) {
          $addresses[++$last_key] = $address;
          $key = $last_key;
        }
      }
      if (!$product->pkg_qty) {
        $product->pkg_qty = 1;
      }
      $num_of_pkgs = (int) ($product->qty / $product->pkg_qty);
      if ($num_of_pkgs) {
        $package = drupal_clone($product);
        $package->description = $product->model;
        $package->weight = $product->weight * $product->pkg_qty;
        $package->price = $product->price * $product->pkg_qty;
        $package->qty = $num_of_pkgs;
        $package->pkg_type = $product->ups ? $product->ups['pkg_type'] : '02';
        if ($package->weight) {
          $packages[$key][] = $package;
        }
      }
      $remaining_qty = $product->qty % $product->pkg_qty;
      if ($remaining_qty) {
        $package = drupal_clone($product);
        $package->description = $product->model;
        $package->weight = $product->weight * $remaining_qty;
        $package->price = $product->price * $remaining_qty;
        $package->qty = 1;
        $package->pkg_type = $product->ups ? $product->ups['pkg_type'] : '02';
        if ($package->weight) {
          $packages[$key][] = $package;
        }
      }
    }
  }
  if (!count($packages)) {
    return array();
  }
  $dest = (object) $details;
  foreach ($packages as $key => $ship_packages) {
    $orig = (object) $addresses[$key];
    $orig->email = variable_get('uc_store_email', '');
    foreach (array_keys(array_filter(variable_get('uc_ups_services', array()))) as $ups_service) {
      $request = uc_ups_shipping_quote($ship_packages, $orig, $dest, $ups_service);
      $resp = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'Rate', array(), 'POST', $request);
      if (user_access('configure quotes') && variable_get('uc_quote_display_debug', false)) {
        $quotes[$ups_service]['debug'] .= htmlentities($request) . ' <br /><br /> ' . htmlentities($resp->data);
      }
      $response = new JSimpleXML();
      $response
        ->loadString($resp->data);
      if (isset($response->document->response[0]->error)) {
        foreach ($response->document->response[0]->error as $error) {
          if (user_access('configure quotes') && variable_get('uc_quote_display_debug', false)) {
            $quotes[$ups_service]['error'][] = $error->errorseverity[0]
              ->data() . ' ' . $error->errorcode[0]
              ->data() . ': ' . $error->errordescription[0]
              ->data();
          }
          if ($error->errorseverity[0]
            ->data() == 'HardError') {

            // All or nothing quote. If some products can't be shipped by
            // a certain service, no quote is given for that service. If
            // that means no quotes are given at all, they'd better call in.
            unset($quotes[$ups_service]['rate']);
          }
        }
      }

      // if NegotiatedRates exist, quote based on those, otherwise, use TotalCharges
      if (isset($response->document->ratedshipment)) {
        $charge = $response->document->ratedshipment[0]->totalcharges[0];
        if (isset($response->document->ratedshipment[0]->negotiatedrates)) {
          $charge = $response->document->ratedshipment[0]->negotiatedrates[0]->netsummarycharges[0]->grandtotal[0];
        }
        if (!isset($charge->currencycode) || $charge->currencycode[0]
          ->data() == variable_get('uc_currency_code', "USD")) {
          $rate = uc_ups_markup($charge->monetaryvalue[0]
            ->data());
          $quotes[$ups_service]['rate'] += $rate;
        }
      }
    }
  }
  uasort($quotes, 'uc_quote_price_sort');
  foreach ($quotes as $key => $quote) {
    if (isset($quote['rate'])) {
      $quotes[$key]['format'] = uc_currency_format($quote['rate']);
      $quotes[$key]['option_label'] = '<img class="ups_logo" src="' . base_path() . drupal_get_path('module', 'uc_ups') . '/uc_ups_logo.gif" /> ' . $method['ups']['quote']['accessorials'][$key] . t(' Rate');
    }
  }

  /**
   * Ugly hack to work around PHP bug, details here:
   *   http://bugs.php.net/bug.php?id=23220
   * We strip out errors that look something like:
   *  warning: fread() [function.fread]: SSL fatal protocol error in...
   * Copied from http://drupal.org/node/70915 and then improved by Lyle.
   */
  $messages = drupal_set_message();
  $errors = $messages['error'];
  $total = count($errors);
  for ($i = 0; $i <= $total; $i++) {
    if (strpos($errors[$i], 'SSL: fatal protocol error in')) {
      unset($_SESSION['messages']['error'][$i]);
    }
  }
  if (empty($_SESSION['messages']['error'])) {
    unset($_SESSION['messages']['error']);
  }
  db_query("DELETE FROM {watchdog} WHERE type = 'php' AND message LIKE '%%SSL: fatal protocol error%%'");

  // End of ugly hack.
  return $quotes;
}

/**
 * Shipment creation callback.
 *
 * Confirm shipment data before requesting a shipping label.
 *
 * @param $order_id
 *   The order id for the shipment.
 * @param $package_ids
 *   Array of package ids to shipped.
 * @ingroup forms
 * @see uc_ups_fulfill_order_submit
 */
function uc_ups_fulfill_order($order_id, $package_ids) {
  $form = array();
  $pkg_types = _uc_ups_pkg_types();
  if ($order = uc_order_load($order_id)) {
    $form['order_id'] = array(
      '#type' => 'value',
      '#value' => $order_id,
    );
    $packages = array();
    $addresses = array();
    $form['packages'] = array(
      '#tree' => true,
    );
    foreach ($package_ids as $id) {
      $package = uc_shipping_package_load($id);
      if ($package) {
        foreach ($package->addresses as $address) {
          if (!in_array($address, $addresses)) {
            $addresses[] = $address;
          }
        }

        // Create list of products and get a representative product for default values
        $product_list = array();
        $declared_value = 0;
        foreach ($package->products as $product) {
          $product_list[] = $product->qty . ' x ' . $product->model;
          $declared_value += $product->qty * $product->price;
        }
        $ups_data = db_fetch_array(db_query("SELECT pkg_type FROM {uc_ups_products} WHERE nid = %d", $product->nid));
        $product->ups = $ups_data;
        $pkg_form = array(
          '#type' => 'fieldset',
          '#title' => t('Package !id', array(
            '!id' => $id,
          )),
        );
        $pkg_form['products'] = array(
          '#value' => theme('item_list', $product_list),
        );
        $pkg_form['package_id'] = array(
          '#type' => 'hidden',
          '#value' => $id,
        );
        $pkg_form['pkg_type'] = array(
          '#type' => 'select',
          '#title' => t('Package type'),
          '#options' => $pkg_types,
          '#default_value' => $product->ups['pkg_type'],
          '#required' => true,
        );
        $pkg_form['declared_value'] = array(
          '#type' => 'textfield',
          '#title' => t('Declared value'),
          '#default_value' => $declared_value,
          '#required' => true,
        );
        $pkg_type['dimensions'] = array(
          '#type' => 'fieldset',
          '#title' => t('Dimensions'),
          '#description' => t('Physical dimensions of the package.'),
          '#theme' => 'uc_ups_dimensions',
        );
        $pkg_form['dimensions']['units'] = array(
          '#type' => 'select',
          '#title' => t('Units of measurement'),
          '#options' => array(
            'in' => t('Inches'),
            'ft' => t('Feet'),
            'cm' => t('Centimeters'),
            'mm' => t('Millimeters'),
          ),
          '#default_value' => $product->length_units ? $product->length_units : variable_get('uc_length_unit', 'in'),
        );
        $pkg_form['dimensions']['length'] = array(
          '#type' => 'textfield',
          '#title' => t('Length'),
          '#default_value' => $product->length,
        );
        $pkg_form['dimensions']['width'] = array(
          '#type' => 'textfield',
          '#title' => t('Width'),
          '#default_value' => $product->width,
        );
        $pkg_form['dimensions']['height'] = array(
          '#type' => 'textfield',
          '#title' => t('Height'),
          '#default_value' => $product->height,
        );
        $form['packages'][$id] = $pkg_form;
      }
    }
    $form = array_merge($form, uc_shipping_address_form($addresses, $order));
    foreach (array(
      'delivery_email',
      'delivery_last_name',
      'delivery_company',
      'delivery_street1',
      'delivery_city',
      'delivery_zone',
      'delivery_country',
      'delivery_postal_code',
    ) as $field) {
      $form['destination'][$field]['#required'] = true;
    }
    $ups_services = _uc_ups_service_list();
    $services = array_filter(variable_get('uc_ups_services', array()));
    foreach ($services as $ups_id => $service) {
      $services[$ups_id] = $ups_services[$ups_id];
    }
    if (count($services)) {
      $form['service'] = array(
        '#type' => 'select',
        '#title' => t('UPS service'),
        '#options' => $services,
      );
    }
    $today = getdate();
    $form['ship_date'] = array(
      '#type' => 'date',
      '#title' => t('Ship date'),
      '#default_value' => array(
        'year' => $today['year'],
        'month' => $today['mon'],
        'day' => $today['mday'],
      ),
    );
    $form['expected_delivery'] = array(
      '#type' => 'date',
      '#title' => t('Expected delivery'),
      '#default_value' => array(
        'year' => $today['year'],
        'month' => $today['mon'],
        'day' => $today['mday'],
      ),
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Review shipment'),
    );
  }
  else {
    drupal_set_message(t("What? That's not an order id. You can't create a shipment without an order."));
    drupal_goto('admin/store/orders');
  }
  return $form;
}

/**
 * Validation handler for uc_ups_fulfill_order().
 *
 * Pass final information into shipment object.
 *
 * @see uc_ups_confirm_shipment
 */
function uc_ups_fulfill_order_validate($form_id, $form_values) {
  include_once drupal_get_path('module', 'uc_store') . '/includes/simplexml.php';
  $origin = new stdClass();
  $destination = new stdClass();
  $packages = array();
  foreach ($form_values as $key => $value) {
    if (substr($key, 0, 7) == 'pickup_') {
      $field = substr($key, 7);
      $origin->{$field} = $value;
    }
    else {
      if (substr($key, 0, 9) == 'delivery_') {
        $field = substr($key, 9);
        $destination->{$field} = $value;
      }
    }
  }
  $_SESSION['ups'] = array();
  $_SESSION['ups']['origin'] = $origin;
  $_SESSION['ups']['destination'] = $destination;
  foreach ($form_values['packages'] as $id => $pkg_form) {
    $package = uc_shipping_package_load($id);
    $package->pkg_type = $pkg_form['pkg_type'];
    $package->value = $pkg_form['declared_value'];
    $package->length = $pkg_form['dimensions']['length'];
    $package->width = $pkg_form['dimensions']['width'];
    $package->height = $pkg_form['dimensions']['height'];
    $package->length_units = $pkg_form['dimensions']['units'];
    $package->qty = 1;
    $_SESSION['ups']['packages'][$id] = $package;
  }
  $_SESSION['ups']['service'] = $form_values['service'];
  $_SESSION['ups']['ship_date'] = $form_values['ship_date'];
  $_SESSION['ups']['expected_delivery'] = $form_values['expected_delivery'];
  $_SESSION['ups']['order_id'] = $form_values['order_id'];
  $request = uc_ups_shipment_request($_SESSION['ups']['packages'], $origin, $destination, $form_values['service']);

  //print htmlentities($request);
  $response_obj = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'ShipConfirm', array(), 'POST', $request);
  $response = new JSimpleXML();
  $response
    ->loadString($response_obj->data);

  //drupal_set_message('<pre>'. htmlentities($response->document->asXML()) .'</pre>');
  if (is_array($response->document->response[0]->error)) {
    $error = $response->document->response[0]->error[0];
    $error_msg = $error->errorseverity[0]
      ->data() . ' Error ' . $error->errorcode[0]
      ->data() . ': ' . $error->errordescription[0]
      ->data();
    drupal_set_message($error_msg, 'error');

    //drupal_set_message('<pre>'. print_r($_SESSION['ups']['packages'], true) .'</pre>' . htmlentities($request) .' <br /><br /> '. htmlentities($response->data));
    if ($error->errorseverity[0]
      ->data() == 'Hard') {
      return null;
    }
  }
  $charge = new stdClass();

  // if NegotiatedRates exist, quote based on those, otherwise, use TotalCharges
  if (is_array($response->document->shipmentcharges)) {
    $charge = $response->document->shipmentcharges[0]->totalcharges[0];
    $_SESSION['ups']['rate']['type'] = t('Total Charges');
    if (is_array($response->document->shipmentcharges[0]->negotiatedrates)) {
      $charge = $response->document->shipmentcharges[0]->negotiatedrates[0]->netsummarycharges[0]->grandtotal[0];
      $_SESSION['ups']['rate']['type'] = t('Negotiated Rates');
    }
  }
  $_SESSION['ups']['rate']['currency'] = $charge->currencycode[0]
    ->data();
  $_SESSION['ups']['rate']['amount'] = $charge->monetaryvalue[0]
    ->data();
  $_SESSION['ups']['digest'] = $response->document->shipmentdigest[0]
    ->data();
}

/**
 * Submit handler for uc_ups_fulfill_order().
 *
 * Pass final information into shipment object.
 *
 * @see uc_ups_confirm_shipment
 */
function uc_ups_fulfill_order_submit($form_id, $form_values) {
  return 'admin/store/orders/' . $form_values['order_id'] . '/shipments/ups';
}

/**
 * Last chance for user to review shipment.
 *
 * @ingroup forms
 * @see theme_uc_ups_confirm_shipment
 * @see uc_ups_confirm_shipment_submit
 */
function uc_ups_confirm_shipment($order_id) {
  $form = array();
  $form['digest'] = array(
    '#type' => 'hidden',
    '#value' => $_SESSION['ups']['digest'],
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Request Pickup'),
  );
  return $form;
}

/**
 * Display final shipment information for review.
 */
function theme_uc_ups_confirm_shipment($form) {
  $output = '';
  $output .= '<div class="shipping_address"><b>' . t('Ship from:') . '</b><br />';
  $output .= uc_address_format(check_plain($_SESSION['ups']['origin']->first_name), check_plain($_SESSION['ups']['origin']->last_name), check_plain($_SESSION['ups']['origin']->company), check_plain($_SESSION['ups']['origin']->street1), check_plain($_SESSION['ups']['origin']->street2), check_plain($_SESSION['ups']['origin']->city), check_plain($_SESSION['ups']['origin']->zone), check_plain($_SESSION['ups']['origin']->postal_code), check_plain($_SESSION['ups']['origin']->country));
  $output .= '<br />' . check_plain($_SESSION['ups']['origin']->email);
  $output .= '</div>';
  $output .= '<div class="shipping_address"><b>' . t('Ship to:') . '</b><br />';
  $output .= uc_address_format(check_plain($_SESSION['ups']['destination']->first_name), check_plain($_SESSION['ups']['destination']->last_name), check_plain($_SESSION['ups']['destination']->company), check_plain($_SESSION['ups']['destination']->street1), check_plain($_SESSION['ups']['destination']->street2), check_plain($_SESSION['ups']['destination']->city), check_plain($_SESSION['ups']['destination']->zone), check_plain($_SESSION['ups']['destination']->postal_code), check_plain($_SESSION['ups']['destination']->country));
  $output .= '<br />' . check_plain($_SESSION['ups']['destination']->email);
  $output .= '</div>';
  $output .= '<div class="shipment_data">';
  $method = uc_ups_shipping_method();
  $output .= '<b>' . $method['ups']['quote']['accessorials'][$_SESSION['ups']['service']] . '</b><br />';
  $output .= '<i>' . check_plain($_SESSION['ups']['rate']['type']) . '</i>: ' . uc_currency_format($_SESSION['ups']['rate']['amount']) . ' (' . check_plain($_SESSION['ups']['rate']['currency']) . ')<br />';
  $ship_date = $_SESSION['ups']['ship_date'];
  $output .= 'Ship date: ' . format_date(gmmktime(12, 0, 0, $ship_date['month'], $ship_date['day'], $ship_date['year']), 'custom', variable_get('uc_date_format_default', 'm/d/Y'));
  $exp_delivery = $_SESSION['ups']['expected_delivery'];
  $output .= '<br />Expected delivery: ' . format_date(gmmktime(12, 0, 0, $exp_delivery['month'], $exp_delivery['day'], $exp_delivery['year']), 'custom', variable_get('uc_date_format_default', 'm/d/Y'));
  $output .= "</div>\n<br style=\"clear: both;\" />";
  $output .= drupal_render($form);
  return $output;
}

/**
 * Submit handler for uc_ups_confirm_shipment().
 *
 * Generate label and schedule pickup of the shipment.
 */
function uc_ups_confirm_shipment_submit($form_id, $form_values) {
  include_once drupal_get_path('module', 'uc_store') . '/includes/simplexml.php';

  // Request pickup using parameters in form.
  $order_id = $_SESSION['ups']['order_id'];
  $packages = array_keys($_SESSION['ups']['packages']);
  $request = uc_ups_request_pickup($form_values['digest'], $order_id, $packages);
  $result = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'ShipAccept', array(), 'POST', $request);
  $response = new JSimpleXML();
  $response
    ->loadString($result->data);
  $code = $response->document->response[0]->responsestatuscode[0]
    ->data();
  if ($code == 0) {

    // failed request
    $error = $response->document->response[0]->error[0];
    $error_severity = $error->errorseverity[0]
      ->data();
    $error_code = $error->errorcode[0]
      ->data();
    $error_description = $error->errordescription[0]
      ->data();
    drupal_set_message(t('(@severity error @code) @description', array(
      '@severity' => $error_severity,
      '@code' => $error_code,
      '@description' => $error_description,
    )), 'error');
    if ($error_severity == 'HardError') {
      return 'admin/store/orders/' . $order_id . '/shipments/ups/' . implode('/', $packages);
    }
  }
  $shipment = new stdClass();
  $shipment->order_id = $order_id;
  $shipment->origin = drupal_clone($_SESSION['ups']['origin']);
  $shipment->destination = drupal_clone($_SESSION['ups']['destination']);
  $shipment->packages = $_SESSION['ups']['packages'];
  $shipment->shipping_method = 'ups';
  $shipment->accessorials = $_SESSION['ups']['service'];
  $shipment->carrier = t('UPS');

  // if NegotiatedRates exist, quote based on those, otherwise, use TotalCharges
  if (is_array($response->document->shipmentresults[0]->shipmentcharges)) {
    $charge = $response->document->shipmentresults[0]->shipmentcharges[0]->totalcharges[0];
    if (is_array($response->document->shipmentresults[0]->negotiatedrates)) {
      $charge = $response->document->shipmentresults[0]->negotiatedrates[0]->netsummarycharges[0]->grandtotal[0];
    }
  }
  $cost = $charge->monetaryvalue[0]
    ->data();
  $shipment->cost = $cost;
  $shipment->tracking_number = $response->document->shipmentresults[0]->shipmentidentificationnumber[0]
    ->data();
  $ship_date = $_SESSION['ups']['ship_date'];
  $shipment->ship_date = gmmktime(12, 0, 0, $ship_date['month'], $ship_date['day'], $ship_date['year']);
  $exp_delivery = $_SESSION['ups']['expected_delivery'];
  $shipment->expected_delivery = gmmktime(12, 0, 0, $exp_delivery['month'], $exp_delivery['day'], $exp_delivery['year']);
  foreach ($response->document->shipmentresults[0]->packageresults as $package_results) {
    $package =& current($shipment->packages);
    $package->tracking_number = $package_results->trackingnumber[0]
      ->data();
    $label_image = $package_results->labelimage[0]->graphicimage[0]
      ->data();
    if (file_check_directory(file_create_path('ups_labels'), FILE_CREATE_DIRECTORY)) {
      $label_path = file_create_path('ups_labels') . '/label' . $package->tracking_number . '.gif';
      if ($label_file = fopen($label_path, 'wb')) {
        fwrite($label_file, base64_decode($label_image));
        fclose($label_file);
        $package->label_image = $label_path;
      }
      else {
        drupal_set_message(t('Could not open a file to save the label image.'), 'error');
      }
    }
    else {
      drupal_set_message(t('Could not find or create the directory "ups_labels" in the file system path.'), 'error');
    }
    unset($package);
    next($shipment->packages);
  }
  uc_shipping_shipment_save($shipment);
  unset($_SESSION['ups']);
  return 'admin/store/orders/' . $order_id . '/shipments';
}

/**
 * Construct an XML label and pickup request.
 *
 * @param $digest
 *   Base-64 encoded shipment request.
 * @param $order_id
 *   The order id of the shipment.
 * @param $packages
 *   An array of package ids to be shipped.
 * @return
 *   ShipmentAcceptRequest XML document to send to UPS.
 */
function uc_ups_request_pickup($digest, $order_id = 0, $packages = array()) {
  $packages = (array) $packages;
  $schema = uc_ups_access_request();
  $schema .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ShipmentAcceptRequest>\n  <Request>\n    <RequestAction>ShipAccept</RequestAction>";
  if ($order_id || count($packages)) {
    $schema .= "\n<TransactionReference>\n      <CustomerContext>";
    if ($order_id) {
      $schema .= "<OrderId>" . $order_id . "</OrderId>\n";
    }
    foreach ($packages as $pkg_id) {
      $schema .= "<PackageId>" . $pkg_id . "</PackageId>\n";
    }
    $schema .= "</CustomerContext>\n</TransactionReference>\n";
  }
  $schema .= "  </Request>\n  <ShipmentDigest>" . $digest . "</ShipmentDigest>\n</ShipmentAcceptRequest>";

  //drupal_set_message('<pre>'. htmlentities($schema) .'</pre>');
  return $schema;
}

/**
 * Display the shipping label for printing.
 *
 * Each argument is a component of the file path to the image.
 *
 * @ingroup themeable
 */
function theme_uc_ups_label_image() {
  $args = func_get_args();
  $image_path = implode('/', $args);
  print '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 3.2//EN">
<html><head><title>
View/Print Label</title></head><style>
    .small_text {font-size: 80%;}
    .large_text {font-size: 115%;}
</style>
<body bgcolor="#FFFFFF">
<table border="0" cellpadding="0" cellspacing="0" width="600"><tr>
<td height="410" align="left" valign="top">
<b class="large_text">View/Print Label</b>
&nbsp;<br />
<ol class="small_text"> <li><b>Print the label:</b> &nbsp;
Select Print from the File menu in this browser window to print the label below.<br /><br /><li><b>
Fold the printed label at the dotted line.</b> &nbsp;
Place the label in a UPS Shipping Pouch. If you do not have a pouch, affix the folded label using clear plastic shipping tape over the entire label.<br /><br /><li><b>GETTING YOUR SHIPMENT TO UPS<br />
Customers without a Daily Pickup</b><ul><li>Ground, 3 Day Select, and Standard to Canada shipments must be dropped off at an authorized UPS location, or handed to a UPS driver. Pickup service is not available for these services. To find the nearest drop-off location, select the Drop-off icon from the UPS tool bar.<li>
Air shipments (including Worldwide Express and Expedited) can be picked up or dropped off. To schedule a pickup, or to find a drop-off location, select the Pickup or Drop-off icon from the UPS tool bar.  </ul> <br />
<b>Customers with a Daily Pickup</b><ul><li>
Your driver will pickup your shipment(s) as usual. </ul>
</ol></td></tr></table><table border="0" cellpadding="0" cellspacing="0" width="600">
<tr>
<td class="small_text" align="left" valign="top">
&nbsp;&nbsp;&nbsp;
FOLD HERE</td>
</tr>
<tr>
<td align="left" valign="top"><hr />
</td>
</tr>
</table>

<table>
<tr>
<td height="10">&nbsp;
</td>
</tr>
</table>

<table border="0" cellpadding="0" cellspacing="0" width="650" ><tr>
<td align="left" valign="top">
<img src="' . base_path() . $image_path . '" height="392" width="672">
</td>
</tr></table>
</body>
</html>';
  exit;
}
function uc_ups_void_shipment_request($shipment_number, $tracking_numbers = array()) {
  $schema = uc_ups_access_request();
  $schema .= '<?xml version="1.0"?>';
  $schema .= '<VoidShipmentRequest>';
  $schema .= '<Request>';
  $schema .= '<RequestAction>Void</RequestAction>';
  $schema .= '<TransactionReference>';
  $schema .= '<CustomerContext>';
  $schema .= t('Void shipment @ship_number and tracking numbers @track_list', array(
    '@ship_number' => $shipment_number,
    '@track_list' => implode(', ', $tracking_numbers),
  ));
  $schema .= '</CustomerContext>';
  $schema .= '<XpciVersion>1.0</XpciVersion>';
  $schema .= '</TransactionReference>';
  $schema .= '</Request>';
  $schema .= '<ExpandedVoidShipment>';
  $schema .= '<ShipmentIdentificationNumber>' . $shipment_number . '</ShipmentIdentificationNumber>';
  foreach ($tracking_numbers as $number) {
    $schema .= '<TrackingNumber>' . $number . '</TrackingNumber>';
  }
  $schema .= '</ExpandedVoidShipment>';
  $schema .= '</VoidShipmentRequest>';
  return $schema;
}
function uc_ups_void_shipment($shipment_number, $tracking_numbers = array()) {
  include_once drupal_get_path('module', 'uc_store') . '/includes/simplexml.php';
  $success = false;
  $request = uc_ups_void_shipment_request($shipment_number, $tracking_numbers);
  $resp = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'Void', array(), 'POST', $request);
  $response = new JSimpleXML();
  $response
    ->loadString($resp->data);
  if (is_array($response->document->response)) {
    if (is_array($response->document->response[0]->responsestatuscode)) {
      $success = $response->document->response[0]->responsestatuscode[0]
        ->data();
    }
    if (is_array($response->document->response[0]->error)) {
      foreach ($response->document->response[0]->error as $error) {
        drupal_set_message($error->errorseverity[0]
          ->data() . ' ' . $error->errorcode[0]
          ->data() . ': ' . $error->errordescription[0]
          ->data(), 'error');
      }
    }
  }
  if (is_array($response->document->status)) {
    if (is_array($response->document->status[0]->statustype)) {
      $success = $response->document->status[0]->statustype[0]->code[0]
        ->data();
    }
  }
  return (bool) $success;
}

/**
 * Modify the rate received from UPS before displaying to the customer.
 */
function uc_ups_markup($rate) {
  $markup = variable_get('uc_ups_markup', '0');
  $type = variable_get('uc_ups_markup_type', 'percentage');
  if (is_numeric(trim($markup))) {
    switch ($type) {
      case 'percentage':
        return $rate + $rate * floatval(trim($markup)) / 100;
      case 'multiplier':
        return $rate * floatval(trim($markup));
      case 'currency':
        return $rate + floatval(trim($markup));
    }
  }
  else {
    return $rate;
  }
}

/**
 * Convenience function to get UPS codes for their services.
 */
function _uc_ups_service_list() {
  return array(
    '03' => t('UPS Ground'),
    '11' => t('UPS Standard'),
    '01' => t('UPS Next Day Air'),
    '13' => t('UPS Next Day Air Saver'),
    '14' => t('UPS Next Day Early A.M.'),
    '02' => t('UPS 2nd Day Air'),
    '59' => t('UPS 2nd Day Air A.M.'),
    '12' => t('UPS 3-Day Select'),
  );
}

/**
 * Convenience function to get UPS codes for their package types.
 */
function _uc_ups_pkg_types() {
  return array(
    '01' => t('UPS Letter'),
    '02' => t('Customer Supplied Package'),
    '03' => t('Tube'),
    '04' => t('PAK'),
    '21' => t('UPS Express Box'),
    '24' => t('UPS 25KG Box'),
    '25' => t('UPS 10KG Box'),
    '30' => t('Pallet'),
  );
}
function _uc_ups_new_package() {
  $package = new stdClass();
  $package->weight = 0;
  $package->price = 0;
  $package->length = 0;
  $package->width = 0;
  $package->height = 0;
  $package->length_units = 'in';
  $package->weight_units = 'lb';
  $package->qty = 1;
  $package->pkg_type = '02';
  return $package;
}

Functions

Namesort descending Description
theme_uc_ups_confirm_shipment Display final shipment information for review.
theme_uc_ups_label_image Display the shipping label for printing.
uc_ups_access_request Return XML access request to be prepended to all requests to the UPS webservice.
uc_ups_admin_settings UPS Online Tool settings.
uc_ups_admin_settings_validate Validation handler for uc_ups_admin_settings.
uc_ups_configuration Implementation of hook_configuration().
uc_ups_confirm_shipment Last chance for user to review shipment.
uc_ups_confirm_shipment_submit Submit handler for uc_ups_confirm_shipment().
uc_ups_form_alter Implementation of hook_form_alter().
uc_ups_fulfill_order Shipment creation callback.
uc_ups_fulfill_order_submit Submit handler for uc_ups_fulfill_order().
uc_ups_fulfill_order_validate Validation handler for uc_ups_fulfill_order().
uc_ups_markup Modify the rate received from UPS before displaying to the customer.
uc_ups_menu Implementation of hook_menu().
uc_ups_nodeapi Implementation of hook_nodeapi().
uc_ups_product_alter_validate Validation handler for UPS product fields.
uc_ups_quote Callback for retrieving a UPS shipping quote.
uc_ups_request_pickup Construct an XML label and pickup request.
uc_ups_shipment_request Construct an XML shippment request.
uc_ups_shipping_method Implementation of Übercart's hook_shipping_method().
uc_ups_shipping_quote Construct an XML quote request.
uc_ups_shipping_type Implementation of Übercart's hook_shipping_type().
uc_ups_store_status Implementation of Übercart's hook_store_status().
uc_ups_void_shipment
uc_ups_void_shipment_request
_uc_ups_new_package
_uc_ups_pkg_types Convenience function to get UPS codes for their package types.
_uc_ups_service_list Convenience function to get UPS codes for their services.