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