'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)'),
);
}