'admin/store/settings/quotes/methods/usps', 'title' => t('USPS'), 'access' => user_access('configure quotes'), 'callback' => 'drupal_get_form', 'callback arguments' => array('uc_usps_admin_settings'), 'type' => MENU_LOCAL_TASK, ); } return $items; } /** * Implementation of hook_form_alter(). * * Add package type to products. * * @see uc_product_form */ function uc_usps_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('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 (in_array($node->type, module_invoke_all('product_types'))) { 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; } } } /****************************************************************************** * Workflow-ng Hooks * ******************************************************************************/ /** * Implementation of hook_configuration(). * * Connect USPS quote action and event. */ function uc_usps_configuration() { $enabled = variable_get('uc_quote_enabled', array()); $configurations = array( 'uc_usps_get_quote' => array( '#label' => t('Shipping quote from USPS'), '#event' => 'get_quote_from_usps', '#module' => 'uc_usps', '#active' => $enabled['usps'], ), 'uc_usps_get_intl_quote' => array( '#label' => t('Shipping quote from USPS Intl.'), '#event' => 'get_quote_from_usps_intl', '#module' => 'uc_usps', '#active' => $enabled['usps_intl'], ), ); // Domestic areas include U.S., American Samoa, Guam, Puerto Rico, and the Virgin Islands $countries = array(16, 316, 630, 840, 850); $us_area_condition = workflow_ng_use_condition('uc_order_condition_delivery_country', array( '#label' => t('Is in domestic US areas (US, AS, GU, PR, VI)'), '#settings' => array( 'countries' => $countries, ), )); $not_us_area_condition = workflow_ng_use_condition('uc_order_condition_delivery_country', array( '#label' => t('Is not in domestic US areas (US, AS, GU, PR, VI)'), '#negate' => true, '#settings' => array( 'countries' => $countries, ), )); $action = workflow_ng_use_action('uc_quote_action_get_quote', array( '#label' => t('Fetch a shipping quote'), )); $configurations['uc_usps_get_quote'] = workflow_ng_configure($configurations['uc_usps_get_quote'], $action, $us_area_condition); $configurations['uc_usps_get_intl_quote'] = workflow_ng_configure($configurations['uc_usps_get_intl_quote'], $action, $not_us_area_condition); return $configurations; } /****************************************************************************** * Übercart Hooks * ******************************************************************************/ /** * Implementation of Übercart's hook_shipping_type(). */ function uc_usps_shipping_type() { $weight = variable_get('uc_quote_type_weight', array('small_package' => 0)); $types = array('small_package' => array( 'id' => 'small_package', 'title' => t('Small package'), 'weight' => $weight['small_package'], )); return $types; } /** * Implementation of Übercart's hook_shipping_method(). */ function uc_usps_shipping_method() { $enabled = variable_get('uc_quote_enabled', array()); $weight = variable_get('uc_quote_method_weight', array('usps' => 0, 'usps_intl' => 1)); $methods = array( 'usps' => array( 'id' => 'usps', 'title' => t('U.S. Postal Service'), 'quote' => array( 'type' => 'small_package', 'callback' => 'uc_usps_quote', 'accessorials' => _uc_usps_services(), ), 'enabled' => $enabled['usps'], 'weight' => $weight['usps'], ), 'usps_intl' => array( 'id' => 'usps_intl', 'title' => t('U.S. Postal Service (Intl.)'), 'quote' => array( 'type' => 'small_package', 'callback' => 'uc_usps_intl_quote', 'accessorials' => _uc_usps_intl_services(), ), 'enabled' => $enabled['usps_intl'], 'weight' => $weight['usps_intl'], ), ); return $methods; } /****************************************************************************** * Menu Callbacks * ******************************************************************************/ /** * Configure USPS settings. * * @ingroup forms */ function uc_usps_admin_settings() { $form = array(); $form['uc_usps_user_id'] = array('#type' => 'textfield', '#title' => t('USPS user ID'), '#description' => t('To acquire or locate your user ID, refer to the USPS documentation.', array('!url' => 'http://www.ubercart.org/docs/user/312/usps_web_tool_shipping_quote_settings')), '#default_value' => variable_get('uc_usps_user_id', ''), ); $form['uc_usps_services'] = array('#type' => 'checkboxes', '#title' => t('USPS services'), '#default_value' => variable_get('uc_usps_services', _uc_usps_services()), '#options' => _uc_usps_services(), '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'), ); $form['uc_usps_intl_services'] = array('#type' => 'checkboxes', '#title' => t('USPS international services'), '#default_value' => variable_get('uc_usps_intl_services', _uc_usps_intl_services()), '#options' => _uc_usps_intl_services(), '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'), ); $form['uc_usps_markup_type'] = array('#type' => 'select', '#title' => t('Markup type'), '#default_value' => variable_get('uc_usps_markup_type', 'percentage'), '#options' => array( 'percentage' => t('Percentage (%)'), 'multiplier' => t('Multiplier (×)'), 'currency' => t('Addition (!currency)', array('!currency' => variable_get('uc_currency_sign', '$'))), ), ); $form['uc_usps_markup'] = array('#type' => 'textfield', '#title' => t('Shipping rate markup'), '#default_value' => variable_get('uc_usps_markup', '0'), '#description' => t('Markup shipping rate quote by dollar amount, percentage, or multiplier.'), ); $form['uc_usps_all_in_one'] = array('#type' => 'radios', '#title' => t('Product packages'), '#default_value' => variable_get('uc_usps_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.'), ); return system_settings_form($form); } /****************************************************************************** * Module Functions * ******************************************************************************/ /** * Callback for retrieving USPS shipping quote. * * @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_usps_quote($products, $details) { include_once(drupal_get_path('module', 'uc_store') .'/includes/simplexml.php'); //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', ''); $request = uc_usps_rate_request($ship_packages, $orig, $dest); $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"; } $response = new JSimpleXML(); $response->loadString($result->data); if (is_array($response->document->package)) { foreach ($response->document->package as $package) { if (isset($package->error)) { $services['data']['error'] .= $package->error[0]->description[0]->data() .'
'; } else { foreach ($package->postage as $postage) { $attr = $postage->attributes(); if ($attr['classid'] === 0 || $attr['classid'] === '0') { if ($postage->mailservice[0]->data() == "First-Class Mail Parcel") { $attr['classid'] = 'zeroParcel'; } else if ($postage->mailservice[0]->data() == "First-Class Mail Flat") { $attr['classid'] = 'zeroFlat'; } else { $attr['classid'] = 'zero'; } } $services[$attr['classid']]['label'] = t('U.S.P.S. @service', array('@service' => $postage->mailservice[0]->data())); $services[$attr['classid']]['rate'] += uc_usps_markup($postage->rate[0]->data()); } } } } } $usps_services = array_filter(variable_get('uc_usps_services', _uc_usps_services())); foreach ($services as $service => $quote) { if ($service != 'data' && !in_array($service, $usps_services)) { unset($services[$service]); } } /* $transitreq = 'USERID="' . variable_get('uc_usps_user_id', '') .'">' . '' . $orig->postal_code . '' . '' . $dest->postal_code . ''; */ foreach ($services as $key => $quote) { if (isset($quote['rate'])) { $services[$key]['format'] = uc_currency_format($quote['rate']); $services[$key]['option_label'] = $quote['label']; /* if (strpos($quote['label'], 'Express') !== false) { $transreq = 'API=ExpressMailCommitment&XML='. urlencode(str_replace('Zip', 'ZIP', '')); } elseif (strpos($quote['label'], 'Priority') !== false) { $transreq = 'API=PriorityMail&XML='. urlencode( ''); } else { $transreq = 'API=StandardB&XML='. urlencode( ''); } $result = drupal_http_request($connection_url, array(), 'POST', $transreq); if (user_access('configure quotes') && variable_get('uc_quote_display_debug', false)) { $services['data']['debug'] .= htmlentities($result->data) ."
\n"; } $transresp = new JSimpleXML(); $transresp->loadString($result->data); if (isset($transresp->document)) { if ($transresp->document->name() == 'Error') { $services[$key]['error'][] = $transresp->document->description[0]->data(); } else if (isset($transresp->document->days)) { $services[$key]['transit'] = $transresp->document->days[0]->data(); $services[$key]['option_label'] .= format_plural($services[$key]['transit'], ', @count day in transit', ', @count days in transit'); } else if (isset($transresp->document->commitment)) { $services[$key]['commitment'] = $transresp->document->commitment[0]->commitmentname[0]->data() .' -- '. $transresp->document->commitment[0]->commitmenttime[0]->data(); $services[$key]['option_label'] .= ', '. $services[$key]['commitment']; } } */ } } uasort($services, 'uc_quote_price_sort'); return $services; } /** * Callback for retrieving USPS shipping quote to other countries. * * @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_usps_intl_quote($products, $details) { include_once(drupal_get_path('module', 'uc_store') .'/includes/simplexml.php'); $services = array(); $addresses = 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', ''); $request = uc_usps_intl_rate_request($ship_packages, $orig, $dest); $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"; } $response = new JSimpleXML(); $response->loadString($result->data); // drupal_set_message('
'. htmlentities($result->data) .'
'); if (is_array($response->document->package)) { foreach ($response->document->package as $package) { if (isset($package->error)) { $services['data']['error'] .= $package->error[0]->description[0]->data() .'
'; } else { foreach ($package->service as $service) { $attr = $service->attributes(); if ($attr['id'] === 0 || $attr['id'] === '0') { $attr['id'] = 'zero'; } $services[$attr['id']]['label'] = t('USPS @service', array('@service' => $service->svcdescription[0]->data())); $services[$attr['id']]['rate'] += uc_usps_markup($service->postage[0]->data()); } } } } } $usps_services = variable_get('uc_usps_intl_services', _uc_usps_intl_services()); foreach ($services as $service => $quote) { if (!in_array($service, $usps_services)) { unset($services[$service]); } } foreach ($services as $key => $quote) { if (isset($quote['rate'])) { $services[$key]['format'] = uc_currency_format($quote['rate']); $services[$key]['option_label'] = $quote['label']; } } uasort($services, 'uc_quote_price_sort'); return $services; } /** * 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. * @return * RateV3Request XML document to send to USPS */ function uc_usps_rate_request($packages, $origin, $destination) { $request = ''; $services_count = 0; foreach ($packages as $package) { $qty = $package->qty; for ($i = 0; $i < $qty; $i++) { $request .= '' . '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 .= ''; $request = 'API=RateV3&XML=' . urlencode($request); return $request; } /* * uc_usps_intl_rate_request function (added by Erik - feel free to improve) * */ function uc_usps_intl_rate_request($packages, $origin, $destination) { $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_get_country_data(array('country_id' => $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[0]['country_name'].'' . ''; $services_count++; } } $request .= ''; $request = 'API=IntlRate&XML=' . 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; } } 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'; } else if ($package->size <= 108) { $package->size = 'LARGE'; } else if ($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'; } else if ($package->size <= 108) { $package->size = 'LARGE'; } else if ($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'), 'FLAT RATE ENVELOPE' => t('Flat rate envelope'), 'RECTANGULAR' => t('Rectangular'), 'NONRECTANGULAR' => t('Non-rectangular'), ); } /** * Convenience function for select form elements. */ function _uc_usps_services() { return array( 'zero' => t('U.S.P.S. First-Class Mail'), 'zeroFlat' => t('U.S.P.S. First-Class Flat'), 'zeroParcel' => t('U.S.P.S. First-Class Parcel'), 1 => t('U.S.P.S. Priority Mail'), 2 => t('U.S.P.S. Express Mail PO to PO'), 3 => t('U.S.P.S. Express Mail PO to Addressee'), 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'), 12 => t('U.S.P.S. First-Class Postcard Stamped'), 13 => t('U.S.P.S. Express Mail Flat-Rate Envelope'), 16 => t('U.S.P.S. Priority Mail Flat-Rate Envelope'), 17 => t('U.S.P.S. Priority Mail Flat-Rate Box'), ); } function _uc_usps_intl_services() { return array( 1 => t('Express Mail International (EMS)'), 2 => t('Priority Mail International'), //3 => t('First Class Mail International'), // Deprecated May 12, 2008 4 => t('Global Express Guaranteed'), 6 => t('Global Express Guaranteed Non-Document Rectangular'), 7 => t('Global Express Guaranteed Non-Document Non-Rectangular'), 8 => t('Priority Mail International Flat Rate Envelope'), 9 => t('Priority Mail International Flat Rate Box'), 10 => t('Express Mail International (EMS) Flat Rate Envelope'), 13 => t('First Class Mail International Letter'), 14 => t('First Class Mail International Flat'), 15 => t('First Class Mail International Parcel'), ); }