'USPS', 'page callback' => 'drupal_get_form', 'page arguments' => array('uc_usps_admin_settings'), 'access arguments' => array('configure quotes'), 'type' => MENU_LOCAL_TASK, 'file' => 'uc_usps.admin.inc', ); return $items; } /** * Implementation of hook_theme(). */ function uc_usps_theme() { return array( 'uc_usps_option_label' => array( 'arguments' => array('service' => NULL), ), ); } /** * Implementation of hook_form_alter(). * * Add package type to products. * * @see uc_product_form() */ function uc_usps_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('usps' => 0, 'usps_intl' => 1)); $form['shipping']['usps'] = array( '#type' => 'fieldset', '#title' => t('USPS product description'), '#collapsible' => TRUE, '#collapsed' => ($enabled['usps'] == FALSE || uc_product_get_shipping_type($node) != 'small_package'), '#weight' => $weight['usps'], '#tree' => TRUE, ); $form['shipping']['usps']['container'] = array( '#type' => 'select', '#title' => t('Package type'), '#options' => _uc_usps_pkg_types(), '#default_value' => $node->usps['container'] ? $node->usps['container'] : 'RECTANGULAR', ); } } /** * Implementation of hook_nodeapi(). */ function uc_usps_nodeapi(&$node, $op) { if (uc_product_is_product($node->type)) { switch ($op) { case 'insert': case 'update': if (isset($node->usps)) { $usps_values = $node->usps; if (!$node->revision) { db_query("DELETE FROM {uc_usps_products} WHERE vid = %d", $node->vid); } db_query("INSERT INTO {uc_usps_products} (vid, nid, container) VALUES (%d, %d, '%s')", $node->vid, $node->nid, $usps_values['container']); } break; case 'load': if (uc_product_get_shipping_type($node) == 'small_package') { return array('usps' => db_fetch_array(db_query("SELECT * FROM {uc_usps_products} WHERE vid = %d", $node->vid))); } break; case 'delete': db_query("DELETE FROM {uc_usps_products} WHERE nid = %d", $node->nid); break; case 'delete revision': db_query("DELETE FROM {uc_usps_products} WHERE vid = %d", $node->vid); break; } } } /****************************************************************************** * Conditional Actions Hooks * ******************************************************************************/ /** * Implementation of hook_ca_predicate(). * * Connect USPS quote action and event. */ function uc_usps_ca_predicate() { $enabled = variable_get('uc_quote_enabled', array()); $quote_action = array( '#name' => 'uc_quote_action_get_quote', '#title' => t('Fetch a shipping quote'), '#argument_map' => array( 'order' => 'order', 'method' => 'method', ), ); // Domestic areas include U.S., American Samoa, Guam, Puerto Rico, and the Virgin Islands $countries = array(16, 316, 630, 840, 850); $predicates = array( 'uc_usps_get_quote' => array( '#title' => t('Shipping quote from USPS'), '#trigger' => 'get_quote_from_usps', '#class' => 'uc_usps', '#status' => $enabled['usps'], '#conditions' => array( '#operator' => 'AND', '#conditions' => array( array( '#name' => 'uc_order_condition_delivery_country', '#title' => t('Is in domestic US areas (US, AS, GU, PR, VI)'), '#argument_map' => array( 'order' => 'order', ), '#settings' => array( 'countries' => $countries, ), ), ), ), '#actions' => array( $quote_action, ), ), 'uc_usps_get_env_quote' => array( '#title' => t('Shipping quote from USPS'), '#trigger' => 'get_quote_from_usps_env', '#class' => 'uc_usps', '#status' => $enabled['usps_env'], '#conditions' => array( '#operator' => 'AND', '#conditions' => array( array( '#name' => 'uc_order_condition_delivery_country', '#title' => t('Is in domestic US areas (US, AS, GU, PR, VI)'), '#argument_map' => array( 'order' => 'order', ), '#settings' => array( 'countries' => $countries, ), ), ), ), '#actions' => array( $quote_action, ), ), 'uc_usps_get_intl_quote' => array( '#title' => t('Shipping quote from USPS Intl.'), '#trigger' => 'get_quote_from_usps_intl', '#class' => 'uc_usps', '#status' => $enabled['usps_intl'], '#conditions' => array( '#operator' => 'AND', '#conditions' => array( array( '#name' => 'uc_order_condition_delivery_country', '#title' => t('Is not in domestic US areas (US, AS, GU, PR, VI)'), '#argument_map' => array( 'order' => 'order', ), '#settings' => array( 'negate' => TRUE, 'countries' => $countries, ), ), ), ), '#actions' => array( $quote_action, ), ), 'uc_usps_get_intl_env_quote' => array( '#title' => t('Shipping quote from USPS Intl.'), '#trigger' => 'get_quote_from_usps_intl_env', '#class' => 'uc_usps', '#status' => $enabled['usps_intl_env'], '#conditions' => array( '#operator' => 'AND', '#conditions' => array( array( '#name' => 'uc_order_condition_delivery_country', '#title' => t('Is not in domestic US areas (US, AS, GU, PR, VI)'), '#argument_map' => array( 'order' => 'order', ), '#settings' => array( 'negate' => TRUE, 'countries' => $countries, ), ), ), ), '#actions' => array( $quote_action, ), ), ); return $predicates; } /****************************************************************************** * Ubercart Hooks * ******************************************************************************/ /** * Implementation of hook_shipping_type(). */ function uc_usps_shipping_type() { $weight = variable_get('uc_quote_type_weight', array('envelope' => -1, 'small_package' => 0)); $types = array( 'envelope' => array( 'id' => 'envelope', 'title' => t('Envelope'), 'weight' => $weight['envelope'], ), 'small_package' => array( 'id' => 'small_package', 'title' => t('Small package'), 'weight' => $weight['small_package'], ), ); return $types; } /** * Implementation of hook_shipping_method(). */ function uc_usps_shipping_method() { $enabled = variable_get('uc_quote_enabled', array()); $weight = variable_get('uc_quote_method_weight', array( 'usps_env' => 0, 'usps' => 0, 'usps_intl_env' => 1, 'usps_intl' => 1, )); $methods = array( 'usps_env' => array( 'id' => 'usps_env', 'module' => 'uc_usps', 'title' => t('U.S. Postal Service (Envelope)'), 'quote' => array( 'type' => 'envelope', 'callback' => 'uc_usps_quote', 'accessorials' => _uc_usps_env_services(), ), 'enabled' => $enabled['usps_env'], 'weight' => $weight['usps_env'], ), 'usps' => array( 'id' => 'usps', 'module' => 'uc_usps', 'title' => t('U.S. Postal Service (Parcel)'), 'quote' => array( 'type' => 'small_package', 'callback' => 'uc_usps_quote', 'accessorials' => _uc_usps_services(), ), 'enabled' => $enabled['usps'], 'weight' => $weight['usps'], ), 'usps_intl_env' => array( 'id' => 'usps_intl_env', 'module' => 'uc_usps', 'title' => t('U.S. Postal Service (Intl., Envelope)'), 'quote' => array( 'type' => 'envelope', 'callback' => 'uc_usps_quote', 'accessorials' => _uc_usps_intl_env_services(), ), 'enabled' => $enabled['usps_intl_env'], 'weight' => $weight['usps_intl_env'], ), 'usps_intl' => array( 'id' => 'usps_intl', 'module' => 'uc_usps', 'title' => t('U.S. Postal Service (Intl., Parcel)'), 'quote' => array( 'type' => 'small_package', 'callback' => 'uc_usps_quote', 'accessorials' => _uc_usps_intl_services(), ), 'enabled' => $enabled['usps_intl'], 'weight' => $weight['usps_intl'], ), ); return $methods; } /****************************************************************************** * Module Functions * ******************************************************************************/ /** * Callback for retrieving USPS shipping quote. * * @param $products * Array of cart contents. * @param $details * Order details other than product information. * @param $method * The shipping method to create the quote. * @return * JSON object containing rate, error, and debugging information. */ function uc_usps_quote($products, $details, $method) { //drupal_set_message('
'. print_r($products, TRUE) .'
'); //drupal_set_message('
'. print_r($details, TRUE) .'
'); $services = array(); $addresses = array((array)variable_get('uc_quote_store_default_address', new stdClass())); $packages = _uc_usps_package_products($products, $addresses); if (!count($packages)) { return array(); } $dest = (object)$details; $usps_server = 'production.shippingapis.com'; $api_dll = 'ShippingAPI.dll'; $connection_url = 'http://'. $usps_server .'/'. $api_dll; foreach ($packages as $key => $ship_packages) { $orig = (object)$addresses[$key]; $orig->email = variable_get('uc_store_email', ''); if (strpos($method['id'], 'intl')) { $request = uc_usps_intl_rate_request($ship_packages, $orig, $dest); } else { $request = uc_usps_rate_request($ship_packages, $orig, $dest); } if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) { $services['data']['debug'] .= htmlentities(urldecode($request)) ."
\n"; } $result = drupal_http_request($connection_url, array(), 'POST', $request); if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) { $services['data']['debug'] .= htmlentities($result->data) ."
\n"; } $rate_type = variable_get('uc_usps_online_rates', FALSE); $response = new SimpleXMLElement($result->data); if (isset($response->Package)) { foreach ($response->Package as $package) { if (isset($package->Error)) { $services['data']['error'] .= (string)$package->Error[0]->Description .'
'; } else { if (strpos($method['id'], 'intl')) { foreach ($package->Service as $service) { $id = (string)$service['ID']; $services[$id]['label'] = t('U.S.P.S. @service', array('@service' => (string)$service->SvcDescription)); $services[$id]['rate'] += uc_usps_markup((string)$service->Postage); } } else { foreach ($package->Postage as $postage) { $classid = (string)$postage['CLASSID']; if ($classid === '0') { if ((string)$postage->MailService == "First-Class Mail Parcel") { $classid = 'zeroParcel'; } elseif ((string)$postage->MailService == "First-Class Mail Flat") { $classid = 'zeroFlat'; } else { $classid = 'zero'; } } $services[$classid]['label'] = t('U.S.P.S. @service', array('@service' => (string)$postage->MailService)); // Markup rate before customer sees it // Rates are stored differently if ONLINE $rate_type is requested. // First Class doesn't have online rates, so if CommercialRate // is missing use Rate instead if ($rate_type && !empty($postage->CommercialRate)) { $services[$classid]['rate'] += uc_usps_markup((string)$postage->CommercialRate); } else { $services[$classid]['rate'] += uc_usps_markup((string)$postage->Rate); } } } } } } } $method_services = 'uc_'. $method['id'] .'_services'; $usps_services = array_filter(variable_get($method_services, array_keys(call_user_func('_'. $method_services)))); foreach ($services as $service => $quote) { if ($service != 'data' && !in_array($service, $usps_services)) { unset($services[$service]); } } $context = array( 'revision' => 'themed', 'type' => 'amount', ); foreach ($services as $key => $quote) { if (isset($quote['rate'])) { $context['subject']['quote'] = $quote; $context['revision'] = 'altered'; $services[$key]['rate'] = uc_price($quote['rate'], $context); $context['revision'] = 'formatted'; $services[$key]['format'] = uc_price($quote['rate'], $context); $services[$key]['option_label'] = theme('uc_usps_option_label', $quote['label']); } } uasort($services, 'uc_quote_price_sort'); return $services; } /** * Theme function to format the USPS service name and rate amount line-item * shown to the customer. * * @param $service * The USPS service name. * @ingroup themeable */ function theme_uc_usps_option_label($service) { // Start with logo $output = ' '; // Add USPS service name, removing the first nine characters // (== 'U.S.P.S. ') because these replicate the logo image $output .= substr($service, 9); return $output; } /** * Construct a quote request for domestic shipments. * * @param $packages * Array of packages received from the cart. * @param $origin * Delivery origin address information. * @param $destination * Delivery destination address information. * @return * RateV3Request XML document to send to USPS */ function uc_usps_rate_request($packages, $origin, $destination) { $request = ''; $services_count = 0; $rate_type = variable_get('uc_usps_online_rates', FALSE); foreach ($packages as $package) { $qty = $package->qty; for ($i = 0; $i < $qty; $i++) { $request .= ''. ''. ($rate_type ? 'ONLINE' : 'ALL') .''. ''. substr($origin->postal_code, 0, 5) .''. ''. substr($destination->postal_code, 0, 5) .''. ''. intval($package->pounds) .''. ''. number_format($package->ounces, 1, '.', '') .''. ''. $package->container .''. ''. $package->size .''. ''. ($package->machinable ? 'True' : 'False') .''. ''; $services_count++; } } $request .= ''; return 'API=RateV3&XML='. drupal_urlencode($request); } /** * Construct a quote request for international shipments. * * @param $packages * Array of packages received from the cart. * @param $origin * Delivery origin address information. * @param $destination * Delivery destination address information. * @return * IntlRateRequest XML document to send to USPS */ function uc_usps_intl_rate_request($packages, $origin, $destination) { module_load_include('inc', 'uc_usps', 'uc_usps.countries'); $request = ''; $services_count = 0; // This needs to be international name per USPS website. See http://pe.usps.com/text/Imm/immctry.htm $shipto_country = uc_usps_country_map($destination->country); foreach ($packages as $package) { $qty = $package->qty; for ($i = 0; $i < $qty; $i++) { $request .= ''. ''. intval($package->pounds) .''. ''. ceil($package->ounces) .''. 'Package'. ''. $shipto_country .''. ''; $services_count++; } } $request .= ''; $request = 'API=IntlRate&XML='. drupal_urlencode($request); return $request; } /** * Modify the rate received from USPS before displaying to the customer. */ function uc_usps_markup($rate) { $markup = variable_get('uc_usps_markup', '0'); $type = variable_get('uc_usps_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; } } /** * Organize products into packages for shipment. * * @param $products * An array of product objects as they are represented in the cart or order. * @param &$addresses * Reference to an array of addresses which are the pickup locations of each * package. They are determined from the shipping addresses of their * component products. * @return * Array of packaged products. Packages are separated by shipping address and * weight or quantity limits imposed by the shipping method or the products. */ function _uc_usps_package_products($products, &$addresses) { $last_key = 0; $packages = array(); if (variable_get('uc_usps_all_in_one', TRUE) && count($products) > 1) { foreach ($products as $product) { 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; $packages[$key][0] = new stdClass(); } } $packages[$key][0]->price += $product->price * $product->qty; $packages[$key][0]->weight += $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb'); } foreach ($packages as $key => $package) { $packages[$key][0]->pounds = floor($package[0]->weight); $packages[$key][0]->ounces = 16 * ($package[0]->weight - $packages[$key][0]->pounds); $packages[$key][0]->container = 'RECTANGULAR'; $packages[$key][0]->size = 'REGULAR'; // Packages are "machinable" if heavier than 6oz. and less than 35lbs. $packages[$key][0]->machinable = ( ($packages[$key][0]->pounds == 0 ? $packages[$key][0]->ounces >= 6 : TRUE) && $packages[$key][0]->pounds <= 35 && ($packages[$key][0]->pounds == 35 ? $packages[$key][0]->ounces == 0 : TRUE) ); $packages[$key][0]->qty = 1; } } else { foreach ($products as $product) { 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; $weight = $product->weight * $product->pkg_qty; switch ($product->weight_units) { case 'g': $weight = $weight / 1000; case 'kg': $weight = $weight * 2.2; case 'lb': $package->pounds = floor($weight); $package->ounces = 16 * ($weight - $package->pounds); break; case 'oz': $package->pounds = floor($weight / 16); $package->ounces = $weight - $package->pounds * 16; break; } $package->container = $product->usps['container']; $length_conversion = uc_length_conversion($product->length_units, 'in'); $package->length = max($product->length, $product->width) * $length_conversion; $package->width = min($product->length, $product->width) * $length_conversion; $package->height = $product->height * $length_conversion; if ($package->length < $package->width) { list($package->length, $package->width) = array($package->width, $package->length); } $package->girth = 2 * $package->width + 2 * $package->height; $package->size = $package->length + $package->girth; if ($package->size <= 84) { $package->size = 'REGULAR'; } elseif ($package->size <= 108) { $package->size = 'LARGE'; } elseif ($package->size <= 130) { $package->size = 'OVERSIZE'; } else { $package->size = 'GI-HUGE-IC'; // Too big for the U.S. Postal service. } $package->machinable = ( $package->length >= 6 && $package->length <= 34 && $package->width >= 0.25 && $package->width <= 17 && $package->height >= 3.5 && $package->height <= 17 && ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) && $package->pounds <= 35 && ($package->pounds == 35 ? $package->ounces == 0 : TRUE) ); $package->price = $product->price * $product->pkg_qty; $package->qty = $num_of_pkgs; $packages[$key][] = $package; } $remaining_qty = $product->qty % $product->pkg_qty; if ($remaining_qty) { $package = drupal_clone($product); $package->description = $product->model; $weight = $product->weight * $remaining_qty; switch ($product->weight_units) { case 'g': $weight = $weight / 1000; case 'kg': $weight = $weight * 2.2; case 'lb': $package->pounds = floor($weight); $package->ounces = 16 * ($weight - $package->pounds); break; case 'oz': $package->pounds = floor($weight / 16); $package->ounces = $weight - $package->pounds * 16; break; } $package->container = $product->usps['container']; $package->length = max($product->length, $product->width) * $length_conversion; $package->width = min($product->length, $product->width) * $length_conversion; $package->height = $product->height * $length_conversion; $package->girth = 2 * $package->width + 2 * $package->height; $package->size = $package->length + $package->girth; if ($package->size <= 84) { $package->size = 'REGULAR'; } elseif ($package->size <= 108) { $package->size = 'LARGE'; } elseif ($package->size <= 130) { $package->size = 'OVERSIZE'; } else { $package->size = 'GI-HUGE-IC'; // Too big for the U.S. Postal service. } $package->machinable = ( $package->length >= 6 && $package->length <= 34 && $package->width >= 0.25 && $package->width <= 17 && $package->height >= 3.5 && $package->height <= 17 && ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) && $package->pounds <= 35 && ($package->pounds == 35 ? $package->ounces == 0 : TRUE) ); $package->price = $product->price * $remaining_qty; $package->qty = 1; $packages[$key][] = $package; } } } return $packages; } /** * Convenience function for select form elements. */ function _uc_usps_pkg_types() { return array( 'VARIABLE' => t('Variable'), 'FLAT RATE BOX' => t('Flat rate box'), 'LG FLAT RATE BOX' => t('Large flat rate box'), 'FLAT RATE ENVELOPE' => t('Flat rate envelope'), 'RECTANGULAR' => t('Rectangular'), 'NONRECTANGULAR' => t('Non-rectangular'), ); } /** * Map envelope shipment services to their IDs. */ function _uc_usps_env_services() { return array( 'zero' => t('U.S.P.S. First-Class Mail'), 'zeroFlat' => t('U.S.P.S. First-Class Flat'), 12 => t('U.S.P.S. First-Class Postcard Stamped'), 1 => t('U.S.P.S. Priority Mail'), 16 => t('U.S.P.S. Priority Mail Flat-Rate Envelope'), 3 => t('U.S.P.S. Express Mail'), 13 => t('U.S.P.S. Express Mail Flat-Rate Envelope'), 23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'), 25 => t('U.S.P.S. Express Mail Flat-Rate Envelope Sunday/Holiday Guarantee'), ); } /** * Map parcel shipment services to their IDs. */ function _uc_usps_services() { return array( 'zero' => t('U.S.P.S. First-Class Mail'), 'zeroParcel' => t('U.S.P.S. First-Class Parcel'), 1 => t('U.S.P.S. Priority Mail'), 28 => t('U.S.P.S. Priority Mail Small Flat-Rate Box'), 17 => t('U.S.P.S. Priority Mail Regular/Medium Flat-Rate Box'), 22 => t('U.S.P.S. Priority Mail Large Flat-Rate Box'), 3 => t('U.S.P.S. Express Mail'), 23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'), 4 => t('U.S.P.S. Parcel Post'), 5 => t('U.S.P.S. Bound Printed Matter'), 6 => t('U.S.P.S. Media Mail'), 7 => t('U.S.P.S. Library'), ); } /** * Map international envelope services to their IDs. */ function _uc_usps_intl_env_services() { return array( 13 => t('First Class Mail International Letter'), 14 => t('First Class Mail International Large Envelope'), 2 => t('Priority Mail International'), 8 => t('Priority Mail International Flat Rate Envelope'), 4 => t('Global Express Guaranteed'), 12 => t('GXG Envelopes'), 1 => t('Express Mail International (EMS)'), 10 => t('Express Mail International (EMS) Flat Rate Envelope'), ); } /** * Map international parcel services to their IDs. */ function _uc_usps_intl_services() { return array( 15 => t('First Class Mail International Package'), 2 => t('Priority Mail International'), 16 => t('Priority Mail International Small Flat-Rate Box'), 9 => t('Priority Mail International Regular/Medium Flat-Rate Box'), 11 => t('Priority Mail International Large Flat-Rate Box'), 4 => t('Global Express Guaranteed'), 6 => t('Global Express Guaranteed Non-Document Rectangular'), 7 => t('Global Express Guaranteed Non-Document Non-Rectangular'), 1 => t('Express Mail International (EMS)'), ); }