'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; } /** * Implements hook_theme(). */ function uc_usps_theme() { return array( 'uc_usps_option_label' => array( 'variables' => array('service' => NULL), ), ); } /** * Implements hook_form_alter(). * * Adds 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()) + array('usps' => FALSE, 'usps_intl' => FALSE); $weight = variable_get('uc_quote_method_weight', array()) + 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' => isset($node->usps['container']) ? $node->usps['container'] : 'RECTANGULAR', ); } } /** * Implements hook_node_insert(). */ function uc_usps_node_insert($node) { uc_usps_node_update($node); } /** * Implements hook_node_update(). */ function uc_usps_node_update($node) { if (uc_product_is_product($node->type)) { if (isset($node->usps)) { $usps_values = $node->usps; if (!$node->revision) { db_delete('uc_usps_products') ->condition('vid', $node->vid) ->execute(); } db_insert('uc_usps_products') ->fields(array( 'vid' => $node->vid, 'nid' => $node->nid, 'container' => $usps_values['container'], )) ->execute(); } } } /** * Implements hook_node_load(). */ function uc_usps_node_load($nodes, $types) { $product_types = array_intersect(uc_product_types(), $types); if (empty($product_types)) { return; } $vids = array(); $shipping_type = variable_get('uc_store_shipping_type', 'small_package'); $shipping_types = db_query("SELECT id, shipping_type FROM {uc_quote_shipping_types} WHERE id_type = :type AND id IN (:ids)", array(':type' => 'product', ':ids' => array_keys($nodes)))->fetchAllKeyed(); foreach ($nodes as $nid => $node) { if (!in_array($node->type, $product_types)) { continue; } if (isset($shipping_types[$nid])) { $node->shipping_type = $shipping_types[$nid]; } else { $node->shipping_type = $shipping_type; } if ($node->shipping_type == 'small_package') { $vids[$nid] = $node->vid; } } if ($vids) { $result = db_query("SELECT * FROM {uc_usps_products} WHERE vid IN (:vids)", array(':vids' => $vids), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $usps) { $nodes[$usps['nid']]->usps = $usps; } } } /** * Implements hook_node_delete(). */ function uc_usps_node_delete($node) { db_delete('uc_usps_products') ->condition('nid', $node->nid) ->execute(); } /** * Implements hook_node_revision_delete(). */ function uc_usps_node_revision_delete($node) { db_delete('uc_usps_products') ->condition('vid', $node->vid) ->execute(); } /****************************************************************************** * Ubercart Hooks * ******************************************************************************/ /** * Implements hook_uc_shipping_type(). */ function uc_usps_uc_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' => isset($weight['envelope']) ? $weight['envelope'] : -1, ), 'small_package' => array( 'id' => 'small_package', 'title' => t('Small package'), 'weight' => isset($weight['small_package']) ? $weight['small_package'] : 0, ), ); return $types; } /** * Implements hook_uc_shipping_method(). */ function uc_usps_uc_shipping_method() { $enabled = variable_get('uc_quote_enabled', array()) + array( 'usps_env' => FALSE, 'usps' => FALSE, 'usps_intl_env' => FALSE, 'usps_intl' => FALSE, ); $weight = variable_get('uc_quote_method_weight', array()) + 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) { $services = array(); $addresses = array(variable_get('uc_quote_store_default_address', new UcAddress())); $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 = $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( 'method' => 'POST', 'data' => $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]); } } foreach ($services as $key => $quote) { if (isset($quote['rate'])) { $services[$key]['rate'] = $quote['rate']; $services[$key]['option_label'] = theme('uc_usps_option_label', array('service' => $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($variables) { $service = $variables['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; } /** * Constructs 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_encode_path($request); } /** * Constructs 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_encode_path($request); return $request; } /** * Modifies 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; } } /** * Organizes 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 = 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 = 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 = 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'), ); } /** * Maps 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'), ); } /** * Maps 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'), ); } /** * Maps 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'), ); } /** * Maps 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)'), ); }