<?php
// $Id: uc_ups.module,v 1.5.2.24 2010/02/08 15:29:30 islandusurper Exp $

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

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

/**
 * Implementation of hook_menu().
 */
function uc_ups_menu() {
  $items = array();

  $items['admin/store/settings/quotes/methods/ups'] = array(
    'title' => 'UPS',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_ups_admin_settings'),
    'access arguments' => array('configure quotes'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_ups.admin.inc',
  );
  $items['admin/store/orders/%uc_order/shipments/ups'] = array(
    'title' => 'UPS shipment',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_ups_confirm_shipment', 3),
    'access arguments' => array('fulfill orders'),
    'type' => MENU_CALLBACK,
    'file' => 'uc_ups.admin.inc',
  );
  $items['admin/store/orders/%uc_order/shipments/labels/ups'] = array(
    'page callback' => 'theme',
    'page arguments' => array('uc_ups_label_image'),
    'access arguments' => array('fulfill orders'),
    'type' => MENU_CALLBACK,
    'file' => 'uc_ups.admin.inc',
  );

  return $items;
}

/**
 * Implementation of hook_init().
 */
function uc_ups_init() {
  drupal_add_css(drupal_get_path('module', 'uc_ups') .'/uc_ups.css');
}

/**
 * Implementation of hook_theme().
 */
function uc_ups_theme() {
  return array(
    'uc_ups_confirm_shipment' => array(
      'arguments' => array('form' => NULL),
      'file' => 'uc_ups.admin.inc',
    ),
    'uc_ups_label_image' => array(
      'arguments' => array(),
      'file' => 'uc_ups.admin.inc',
    ),
    'uc_ups_option_label' => array(
      'arguments' => array('service' => NULL),
    ),
  );
}

/**
 * Implementation of hook_form_alter().
 *
 * Add package type to products.
 *
 * @see uc_product_form()
 * @see uc_ups_product_alter_validate()
 */
function uc_ups_form_alter(&$form, $form_state, $form_id) {
  if (uc_product_is_product_form($form)) {
    $node = $form['#node'];
    $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';
    }
  }
}

/**
 * Validation handler for UPS product fields.
 *
 * @see uc_ups_form_alter()
 */
function uc_ups_product_alter_validate($form, &$form_state) {
  $enabled = variable_get('uc_quote_enabled', array());
  if ($form_state['values']['shippable'] && ($form_state['values']['shipping_type'] == 'small_package' || (empty($form_state['values']['shipping_type']) && variable_get('uc_store_shipping_type', 'small_package') == 'small_package'))) {
    if ($form_state['values']['ups']['pkg_type'] == '02' && (empty($form_state['values']['dim_length']) || empty($form_state['values']['dim_width']) || empty($form_state['values']['dim_height']))) {
      form_set_error('base][dimensions', t('Dimensions are required for custom packaging.'));
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function uc_ups_nodeapi(&$node, $op) {
  if (uc_product_is_product($node->type)) {
    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;
    }
  }
}

/******************************************************************************
 * Conditional Actions Hooks                                                  *
 ******************************************************************************/

/**
 * Implementation of hook_ca_predicate().
 *
 * Connect the UPS quote action and event.
 */
function uc_ups_ca_predicate() {
  $enabled = variable_get('uc_quote_enabled', array());

  $predicates = array(
    'uc_ups_get_quote' => array(
      '#title' => t('Shipping quote from UPS'),
      '#trigger' => 'get_quote_from_ups',
      '#class' => 'uc_ups',
      '#status' => $enabled['ups'],
      '#actions' => array(
        array(
          '#name' => 'uc_quote_action_get_quote',
          '#title' => t('Fetch a shipping quote'),
          '#argument_map' => array(
            'order' => 'order',
            'method' => 'method',
          ),
        ),
      ),
    ),
  );

  return $predicates;
}

/******************************************************************************
 * Ubercart Hooks                                                             *
 ******************************************************************************/

/**
 * Implementation of 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 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 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;
}

/******************************************************************************
 * 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\"?>
<AccessRequest xml:lang=\"en-US\">
  <AccessLicenseNumber>$access</AccessLicenseNumber>
  <UserId>$user</UserId>
  <Password>$password</Password>
</AccessRequest>
";
}

/**
 * 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_length_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() ."
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<RatingServiceSelectionRequest xml:lang=\"en-US\">
  <Request>
    <TransactionReference>
      <CustomerContext>Complex Rate Request</CustomerContext>
      <XpciVersion>1.0001</XpciVersion>
    </TransactionReference>
    <RequestAction>Rate</RequestAction>
    <RequestOption>rate</RequestOption>
  </Request>
  <PickupType>
    <Code>". variable_get('uc_ups_pickup_type', '01') ."</Code>
  </PickupType>
  <CustomerClassification>
    <Code>". variable_get('uc_ups_classification', '04') ."</Code>
  </CustomerClassification>
  <Shipment>
    <Shipper>
      <ShipperNumber>". variable_get('uc_ups_shipper_number', '') ."</ShipperNumber>
      <Address>
        <City>". $store['city'] ."</City>
        <StateProvinceCode>$shipper_zone</StateProvinceCode>
        <PostalCode>$shipper_zip</PostalCode>
        <CountryCode>$shipper_country</CountryCode>
      </Address>
    </Shipper>
    <ShipTo>
      <Address>
        <StateProvinceCode>$shipto_zone</StateProvinceCode>
        <PostalCode>$shipto_zip</PostalCode>
        <CountryCode>$shipto_country</CountryCode>
      ";
      if (variable_get('uc_ups_residential_quotes', 0)) {
        $schema .= "<ResidentialAddressIndicator/>
      ";
      }
      $schema .= "</Address>
    </ShipTo>
    <ShipFrom>
      <Address>
        <StateProvinceCode>$shipfrom_zone</StateProvinceCode>
        <PostalCode>$shipfrom_zip</PostalCode>
        <CountryCode>$shipfrom_country</CountryCode>
      </Address>
    </ShipFrom>
    <ShipmentWeight>
      <UnitOfMeasurement>
        <Code>$units</Code>
        <Description>$unit_name</Description>
      </UnitOfMeasurement>
      <Weight>". number_format($shipment_weight, 1, '.', '') ."</Weight>
    </ShipmentWeight>
    <Service>
      <Code>{$service[code]}</Code>
      <Description>{$service[description]}</Description>
    </Service>
    ";
    $schema .= $package_schema;
    if (variable_get('uc_ups_negotiated_rates', FALSE)) {
      $schema .= "<RateInformation>
          <NegotiatedRatesIndicator/>
        </RateInformation>";
    }
  $schema .= "</Shipment>
</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 = uc_length_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>". (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 = max(1, $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->value, 2, '.', '') ."</MonetaryValue>";
          $package_schema .= "</InsuredValue>";
        $package_schema .= "</PackageServiceOptions>";
      $package_schema .= "</Package>";
    }
  }

  $schema = uc_ups_access_request() ."
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<ShipmentConfirmRequest xml:lang=\"en-US\">
  <Request>
    <TransactionReference>
      <CustomerContext>Complex Rate Request</CustomerContext>
      <XpciVersion>1.0001</XpciVersion>
    </TransactionReference>
    <RequestAction>ShipConfirm</RequestAction>
    <RequestOption>validate</RequestOption>
  </Request>
  <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>
          <NegotiatedRatesIndicator/>
        </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) {
  $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_length_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'] .= /* '<pre>'. print_r($ship_packages, TRUE) .'</pre>' . */ htmlentities($request) .' <br /><br /> '. htmlentities($resp->data);
      }
      $response = new SimpleXMLElement($resp->data);
      if (isset($response->Response->Error)) {
        foreach ($response->Response->Error as $error) {
          if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
            $quotes[$ups_service]['error'][] = (string)$error->ErrorSeverity .' '. (string)$error->ErrorCode .': '. (string)$error->ErrorDescription;
          }
          if (strpos((string)$error->ErrorSeverity, 'Hard') !== FALSE) {
            // 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->RatedShipment)) {
        $charge = $response->RatedShipment->TotalCharges;
        if (isset($response->RatedShipment->NegotiatedRates)) {
          $charge = $response->RatedShipment->NegotiatedRates->NetSummaryCharges->GrandTotal;
        }
        if (!isset($charge->CurrencyCode) || (string)$charge->CurrencyCode == variable_get('uc_currency_code', "USD")) {
          $rate = uc_ups_markup((string)$charge->MonetaryValue);
          $quotes[$ups_service]['rate'] += $rate;
        }
      }
    }
  }
  uasort($quotes, 'uc_quote_price_sort');
  $context = array(
    'revision' => 'themed',
    'type' => 'amount',
  );
  foreach ($quotes as $key => $quote) {
    if (isset($quote['rate'])) {
      $context['subject']['quote'] = $quote;
      $context['revision'] = 'altered';
      $quotes[$key]['rate'] = uc_price($quote['rate'], $context);
      $context['revision'] = 'formatted';
      $quotes[$key]['format'] = uc_price($quote['rate'], $context);
      $quotes[$key]['option_label'] = theme('uc_ups_option_label', $method['ups']['quote']['accessorials'][$key]);
    }
  }

  /**
   * 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 variables LIKE '%%SSL: fatal protocol error%%'");
  // End of ugly hack.

  return $quotes;
}

/**
 * Theme function to format the UPS service name and rate amount line-item shown
 *   to the customer.
 *
 * @param $service
 *   The UPS service name.
 * @ingroup themeable
 */
function theme_uc_ups_option_label($service) {
  // Start with logo as required by the UPS terms of service.
  $output = '<img class="ups-logo" src="'. url(drupal_get_path('module', 'uc_ups')) .'/uc_ups_logo.gif" alt="'. t('UPS Logo') .'" /> ';

  // Add the UPS service name.
  $output .= t('@service Rate', array('@service' => $service));

  return $output;
}

/**
 * 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_validate()
 *   uc_ups_fulfill_order_submit()
 */
function uc_ups_fulfill_order($form_state, $order, $package_ids) {
  $form = array();
  $pkg_types = _uc_ups_pkg_types();
  $form['order_id'] = array('#type' => 'value', '#value' => $order->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($form_state, $addresses, $order));

  foreach (array('delivery_email', 'delivery_last_name', 'delivery_street1', 'delivery_city', 'delivery_zone', 'delivery_country', 'delivery_postal_code') as $field) {
    $form['destination'][$field]['#required'] = TRUE;
  }

  $services = _uc_ups_service_list();
  $default_service = '';
  $method = $order->quote['method'];
  if ($method == 'ups') {
    $default_service = $order->quote['accessorials'];
    $method = $services[$default_service];
  }

  // Inform user of customer's shipping choice
  $form['shipping_choice'] = array(
    '#type' => 'markup',
    '#value' => t('Customer selected @method as the shipping method and paid @rate', array('@method' => $method, '@rate' => uc_currency_format($order->quote['rate']))),
  );

  $form['service'] = array('#type' => 'select',
    '#title' => t('UPS service'),
    '#options' => $services,
    '#default_value' => $default_service,
  );
  $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'));

  return $form;
}

/**
 * Pass final information into shipment object.
 *
 * @see
 *   uc_ups_fulfill_order()
 *   uc_ups_confirm_shipment()
 */
function uc_ups_fulfill_order_validate($form, &$form_state) {
  $origin = new stdClass();
  $destination = new stdClass();
  $packages = array();
  foreach ($form_state['values'] as $key => $value) {
    if (substr($key, 0, 7) == 'pickup_') {
      $field = substr($key, 7);
      $origin->$field = $value;
    }
    elseif (substr($key, 0, 9) == 'delivery_') {
      $field = substr($key, 9);
      $destination->$field = $value;
    }
  }
  $_SESSION['ups'] = array();
  $_SESSION['ups']['origin'] = $origin;
  if (empty($destination->company)) {
    $destination->company = $destination->first_name .' '. $destination->last_name;
  }
  $_SESSION['ups']['destination'] = $destination;
  foreach ($form_state['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_state['values']['service'];
  $_SESSION['ups']['ship_date'] = $form_state['values']['ship_date'];
  $_SESSION['ups']['expected_delivery'] = $form_state['values']['expected_delivery'];
  $_SESSION['ups']['order_id'] = $form_state['values']['order_id'];

  $request = uc_ups_shipment_request($_SESSION['ups']['packages'], $origin, $destination, $form_state['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 SimpleXMLElement($response_obj->data);
  //drupal_set_message('<pre>'. htmlentities($response->asXML()) .'</pre>');
  if (isset($response->Response->Error)) {
    $error = $response->Response->Error;
    $error_msg = (string)$error->ErrorSeverity .' Error '. (string)$error->ErrorCode .': '. (string)$error->ErrorDescription;
    //drupal_set_message('<pre>'. print_r($_SESSION['ups']['packages'], TRUE) .'</pre>' . htmlentities($request) .' <br /><br /> '. htmlentities($response->data));
    if (strpos((string)$error->ErrorSeverity, 'Hard') !== FALSE) {
      form_set_error('', $error_msg);
      return FALSE;
    }
    else {
      drupal_set_message($error_msg, 'error');
    }
  }
  $charge = new stdClass();
  // if NegotiatedRates exist, quote based on those, otherwise, use TotalCharges
  if (isset($response->ShipmentCharges)) {
    $charge = $response->ShipmentCharges->TotalCharges;
    $_SESSION['ups']['rate']['type'] = t('Total Charges');
    if (isset($response->NegotiatedRates)) {
      $charge = $response->NegotiatedRates->NetSummaryCharges->GrandTotal;
      $_SESSION['ups']['rate']['type'] = t('Negotiated Rates');
    }
  }
  $_SESSION['ups']['rate']['currency'] = (string)$charge->CurrencyCode;
  $_SESSION['ups']['rate']['amount'] = (string)$charge->MonetaryValue;
  $_SESSION['ups']['digest'] = (string)$response->ShipmentDigest;
}

/**
 * Pass final information into shipment object.
 *
 * @see
 *   uc_ups_fulfill_order()
 *   uc_ups_confirm_shipment()
 */
function uc_ups_fulfill_order_submit($form, &$form_state) {
  $form_state['redirect'] = 'admin/store/orders/'. $form_state['values']['order_id'] .'/shipments/ups';
}

/**
 * Construct a void shipment request.
 *
 * @param $shipment_number
 *   The UPS shipment tracking number.
 * @param $tracking_numbers
 *   Array of tracking numbers for individual packages in the shipment.
 *   Optional for shipments of only one package, as they have the same tracking
 *   number.
 * @return
 *   XML VoidShipmentRequest message.
 */
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;
}

/**
 * Instruct UPS to cancel (in whole or in part) a shipment.
 *
 * @param $shipment_number
 *   The UPS shipment tracking number.
 * @param $tracking_numbers
 *   Array of tracking numbers for individual packages in the shipment.
 *   Optional for shipments of only one package, as they have the same tracking
 *   number.
 * @return
 *   TRUE if the shipment or packages were successfully voided.
 */
function uc_ups_void_shipment($shipment_number, $tracking_numbers = array()) {
  $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 SimpleXMLElement($resp->data);
  if (isset($response->Response)) {
    if (isset($response->Response->ResponseStatusCode)) {
      $success = (string)$response->Response->ResponseStatusCode;
    }
    if (isset($response->Response->Error)) {
      foreach ($response->Response->Error as $error) {
        drupal_set_message((string)$error->ErrorSeverity .' '. (string)$error->ErrorCode .': '. (string)$error->ErrorDescription, 'error');
      }
    }
  }
  if (isset($response->Status)) {
    if (isset($response->Status->StatusType)) {
      $success = (string)$response->Status->StatusType->Code;
    }
  }
  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'),
    '07' => t('UPS Worldwide Express'),
    '08' => t('UPS Worldwide Expedited'),
    '54' => t('UPS Worldwide Express Plus'),
    '65' => t('UPS Saver'),
    '82' => t('UPS Today Standard'),
    '83' => t('UPS Today Dedicated Courrier'),
    '84' => t('UPS Today Intercity'),
    '85' => t('UPS Today Express'),
    '86' => t('UPS Today Express Saver'),
  );
}

/**
 * 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'),
    '2a' => t('Small Express Box'),
    '2b' => t('Medium Express Box'),
    '2c' => t('Large Express Box'),
  );
}

/**
 * Pseudo-constructor to set default values of a package.
 */
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;
}
