uc_usps.module 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. <?php
  2. /**
  3. * @file
  4. * United States Postal Service (USPS) shipping quote module.
  5. */
  6. /******************************************************************************
  7. * Drupal Hooks *
  8. ******************************************************************************/
  9. /**
  10. * Implements hook_menu().
  11. */
  12. function uc_usps_menu() {
  13. $items = array();
  14. $items['admin/store/settings/quotes/settings/usps'] = array(
  15. 'title' => 'USPS',
  16. 'page callback' => 'drupal_get_form',
  17. 'page arguments' => array('uc_usps_admin_settings'),
  18. 'access arguments' => array('configure quotes'),
  19. 'type' => MENU_LOCAL_TASK,
  20. 'file' => 'uc_usps.admin.inc',
  21. );
  22. return $items;
  23. }
  24. /**
  25. * Implements hook_theme().
  26. */
  27. function uc_usps_theme() {
  28. return array(
  29. 'uc_usps_option_label' => array(
  30. 'variables' => array(
  31. 'service' => NULL,
  32. 'packages' => NULL,
  33. ),
  34. 'file' => 'uc_usps.theme.inc',
  35. ),
  36. );
  37. }
  38. /**
  39. * Implements hook_form_alter().
  40. *
  41. * Adds package type to products.
  42. *
  43. * @see uc_product_form()
  44. */
  45. function uc_usps_form_alter(&$form, &$form_state, $form_id) {
  46. if (uc_product_is_product_form($form)) {
  47. $node = $form['#node'];
  48. $enabled = variable_get('uc_quote_enabled', array()) + array('usps' => FALSE, 'usps_intl' => FALSE);
  49. $weight = variable_get('uc_quote_method_weight', array()) + array('usps' => 0, 'usps_intl' => 1);
  50. $form['shipping']['usps'] = array(
  51. '#type' => 'fieldset',
  52. '#title' => t('USPS product description'),
  53. '#collapsible' => TRUE,
  54. '#collapsed' => ($enabled['usps'] == FALSE || uc_product_get_shipping_type($node) != 'small_package'),
  55. '#weight' => $weight['usps'],
  56. '#tree' => TRUE,
  57. );
  58. $form['shipping']['usps']['container'] = array(
  59. '#type' => 'select',
  60. '#title' => t('Package type'),
  61. '#options' => _uc_usps_pkg_types(),
  62. '#default_value' => isset($node->usps['container']) ? $node->usps['container'] : 'VARIABLE',
  63. );
  64. }
  65. }
  66. /**
  67. * Implements hook_node_insert().
  68. */
  69. function uc_usps_node_insert($node) {
  70. uc_usps_node_update($node);
  71. }
  72. /**
  73. * Implements hook_node_update().
  74. */
  75. function uc_usps_node_update($node) {
  76. if (uc_product_is_product($node->type)) {
  77. if (isset($node->usps)) {
  78. $usps_values = $node->usps;
  79. if (empty($node->revision)) {
  80. db_delete('uc_usps_products')
  81. ->condition('vid', $node->vid)
  82. ->execute();
  83. }
  84. db_insert('uc_usps_products')
  85. ->fields(array(
  86. 'vid' => $node->vid,
  87. 'nid' => $node->nid,
  88. 'container' => $usps_values['container'],
  89. ))
  90. ->execute();
  91. }
  92. }
  93. }
  94. /**
  95. * Implements hook_node_load().
  96. */
  97. function uc_usps_node_load($nodes, $types) {
  98. $product_types = array_intersect(uc_product_types(), $types);
  99. if (empty($product_types)) {
  100. return;
  101. }
  102. $vids = array();
  103. $shipping_type = variable_get('uc_store_shipping_type', 'small_package');
  104. $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();
  105. foreach ($nodes as $nid => $node) {
  106. if (!in_array($node->type, $product_types)) {
  107. continue;
  108. }
  109. if (isset($shipping_types[$nid])) {
  110. $node->shipping_type = $shipping_types[$nid];
  111. }
  112. else {
  113. $node->shipping_type = $shipping_type;
  114. }
  115. if ($node->shipping_type == 'small_package') {
  116. $vids[$nid] = $node->vid;
  117. }
  118. }
  119. if ($vids) {
  120. $result = db_query("SELECT * FROM {uc_usps_products} WHERE vid IN (:vids)", array(':vids' => $vids), array('fetch' => PDO::FETCH_ASSOC));
  121. foreach ($result as $usps) {
  122. $nodes[$usps['nid']]->usps = $usps;
  123. }
  124. }
  125. }
  126. /**
  127. * Implements hook_node_delete().
  128. */
  129. function uc_usps_node_delete($node) {
  130. db_delete('uc_usps_products')
  131. ->condition('nid', $node->nid)
  132. ->execute();
  133. }
  134. /**
  135. * Implements hook_node_revision_delete().
  136. */
  137. function uc_usps_node_revision_delete($node) {
  138. db_delete('uc_usps_products')
  139. ->condition('vid', $node->vid)
  140. ->execute();
  141. }
  142. /******************************************************************************
  143. * Ubercart Hooks *
  144. ******************************************************************************/
  145. /**
  146. * Implements hook_uc_shipping_type().
  147. */
  148. function uc_usps_uc_shipping_type() {
  149. $weight = variable_get('uc_quote_type_weight', array('envelope' => -1, 'small_package' => 0));
  150. $types = array(
  151. 'envelope' => array(
  152. 'id' => 'envelope',
  153. 'title' => t('Envelope'),
  154. 'weight' => isset($weight['envelope']) ? $weight['envelope'] : -1,
  155. ),
  156. 'small_package' => array(
  157. 'id' => 'small_package',
  158. 'title' => t('Small package'),
  159. 'weight' => isset($weight['small_package']) ? $weight['small_package'] : 0,
  160. ),
  161. );
  162. return $types;
  163. }
  164. /**
  165. * Implements hook_uc_shipping_method().
  166. */
  167. function uc_usps_uc_shipping_method() {
  168. $operations = array(
  169. 'configure' => array(
  170. 'title' => t('configure'),
  171. 'href' => 'admin/store/settings/quotes/settings/usps',
  172. ),
  173. );
  174. $methods = array(
  175. 'usps_env' => array(
  176. 'id' => 'usps_env',
  177. 'module' => 'uc_usps',
  178. 'title' => t('U.S. Postal Service (Envelope)'),
  179. 'operations' => $operations,
  180. 'quote' => array(
  181. 'type' => 'envelope',
  182. 'callback' => 'uc_usps_quote',
  183. 'accessorials' => _uc_usps_env_services(),
  184. ),
  185. ),
  186. 'usps' => array(
  187. 'id' => 'usps',
  188. 'module' => 'uc_usps',
  189. 'title' => t('U.S. Postal Service (Parcel)'),
  190. 'operations' => $operations,
  191. 'quote' => array(
  192. 'type' => 'small_package',
  193. 'callback' => 'uc_usps_quote',
  194. 'accessorials' => _uc_usps_services(),
  195. ),
  196. ),
  197. 'usps_intl_env' => array(
  198. 'id' => 'usps_intl_env',
  199. 'module' => 'uc_usps',
  200. 'title' => t('U.S. Postal Service (Intl., Envelope)'),
  201. 'operations' => $operations,
  202. 'quote' => array(
  203. 'type' => 'envelope',
  204. 'callback' => 'uc_usps_quote',
  205. 'accessorials' => _uc_usps_intl_env_services(),
  206. ),
  207. 'weight' => 1,
  208. ),
  209. 'usps_intl' => array(
  210. 'id' => 'usps_intl',
  211. 'module' => 'uc_usps',
  212. 'title' => t('U.S. Postal Service (Intl., Parcel)'),
  213. 'operations' => $operations,
  214. 'quote' => array(
  215. 'type' => 'small_package',
  216. 'callback' => 'uc_usps_quote',
  217. 'accessorials' => _uc_usps_intl_services(),
  218. ),
  219. 'weight' => 1,
  220. ),
  221. );
  222. return $methods;
  223. }
  224. /******************************************************************************
  225. * Module Functions *
  226. ******************************************************************************/
  227. /**
  228. * Callback for retrieving USPS shipping quote.
  229. *
  230. * @param $products
  231. * Array of cart contents.
  232. * @param $details
  233. * Order details other than product information.
  234. * @param $method
  235. * The shipping method to create the quote.
  236. *
  237. * @return
  238. * JSON object containing rate, error, and debugging information.
  239. */
  240. function uc_usps_quote($products, $details, $method) {
  241. // The uc_quote AJAX query can fire before the customer has completely
  242. // filled out the destination address, so check to see whether the address
  243. // has all needed fields. If not, abort.
  244. $destination = (object) $details;
  245. // Country code is always needed.
  246. if (empty($destination->country)) {
  247. // Skip this shipping method.
  248. return array();
  249. }
  250. // Shipments to the US also need zone and postal_code.
  251. if (($destination->country == 840) &&
  252. (empty($destination->zone) || empty($destination->postal_code))) {
  253. // Skip this shipping method.
  254. return array();
  255. }
  256. // USPS Production server.
  257. $connection_url = 'http://production.shippingapis.com/ShippingAPI.dll';
  258. // Initialize $debug_data to prevent PHP notices here and in uc_quote.
  259. $debug_data = array('debug' => NULL, 'error' => array());
  260. $services = array();
  261. $addresses = array(variable_get('uc_quote_store_default_address', new UcAddress()));
  262. $packages = _uc_usps_package_products($products, $addresses);
  263. if (!count($packages)) {
  264. return array();
  265. }
  266. foreach ($packages as $key => $ship_packages) {
  267. $orig = $addresses[$key];
  268. $orig->email = uc_store_email();
  269. if (strpos($method['id'], 'intl') && ($destination->country != 840)) {
  270. // Build XML for international rate request.
  271. $request = uc_usps_intl_rate_request($ship_packages, $orig, $destination);
  272. }
  273. elseif ($destination->country == 840) {
  274. // Build XML for domestic rate request.
  275. $request = uc_usps_rate_request($ship_packages, $orig, $destination);
  276. }
  277. if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
  278. $debug_data['debug'] .= htmlentities(urldecode($request)) . "<br />\n";
  279. }
  280. $result = drupal_http_request($connection_url, array(
  281. 'method' => 'POST',
  282. 'data' => $request,
  283. ));
  284. if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
  285. $debug_data['debug'] .= htmlentities($result->data) . "<br />\n";
  286. }
  287. $rate_type = variable_get('uc_usps_online_rates', FALSE);
  288. $response = new SimpleXMLElement($result->data);
  289. // Map double-encoded HTML markup in service names to Unicode characters.
  290. $service_markup = array(
  291. '&lt;sup&gt;&amp;reg;&lt;/sup&gt;' => '®',
  292. '&lt;sup&gt;&amp;trade;&lt;/sup&gt;' => '™',
  293. '&lt;sup&gt;&#174;&lt;/sup&gt;' => '®',
  294. '&lt;sup&gt;&#8482;&lt;/sup&gt;' => '™',
  295. '**' => '',
  296. );
  297. // Use this map to fix USPS service names.
  298. if (strpos($method['id'], 'intl')) {
  299. // Find and replace markup in International service names.
  300. foreach ($response->xpath('//Service') as $service) {
  301. $service->SvcDescription = str_replace(array_keys($service_markup), $service_markup, $service->SvcDescription);
  302. }
  303. }
  304. else {
  305. // Find and replace markup in Domestic service names.
  306. foreach ($response->xpath('//Postage') as $postage) {
  307. $postage->MailService = str_replace(array_keys($service_markup), $service_markup, $postage->MailService);
  308. }
  309. }
  310. if (isset($response->Package)) {
  311. foreach ($response->Package as $package) {
  312. if (isset($package->Error)) {
  313. $debug_data['error'][] = (string) $package->Error[0]->Description . '<br />';
  314. }
  315. else {
  316. if (strpos($method['id'], 'intl')) {
  317. foreach ($package->Service as $service) {
  318. $id = (string) $service['ID'];
  319. $services[$id]['label'] = t('U.S.P.S. @service', array('@service' => (string) $service->SvcDescription));
  320. // Markup rate before customer sees it.
  321. if (!isset($services[$id]['rate'])) {
  322. $services[$id]['rate'] = 0;
  323. }
  324. $services[$id]['rate'] += uc_usps_rate_markup((string) $service->Postage);
  325. }
  326. }
  327. else {
  328. foreach ($package->Postage as $postage) {
  329. $classid = (string) $postage['CLASSID'];
  330. if ($classid === '0') {
  331. if ((string) $postage->MailService == "First-Class Mail® Parcel") {
  332. $classid = 'zeroParcel';
  333. }
  334. elseif ((string) $postage->MailService == "First-Class Mail® Letter") {
  335. $classid = 'zeroFlat';
  336. }
  337. else {
  338. $classid = 'zero';
  339. }
  340. }
  341. if (!isset($services[$classid]['rate'])) {
  342. $services[$classid]['rate'] = 0;
  343. }
  344. $services[$classid]['label'] = t('U.S.P.S. @service', array('@service' => (string) $postage->MailService));
  345. // Markup rate before customer sees it.
  346. // Rates are stored differently if ONLINE $rate_type is requested.
  347. // First Class doesn't have online rates, so if CommercialRate
  348. // is missing use Rate instead.
  349. if ($rate_type && !empty($postage->CommercialRate)) {
  350. $services[$classid]['rate'] += uc_usps_rate_markup((string) $postage->CommercialRate);
  351. }
  352. else {
  353. $services[$classid]['rate'] += uc_usps_rate_markup((string) $postage->Rate);
  354. }
  355. }
  356. }
  357. }
  358. }
  359. }
  360. }
  361. $method_services = 'uc_' . $method['id'] . '_services';
  362. $usps_services = array_filter(variable_get($method_services, array_keys(call_user_func('_' . $method_services))));
  363. foreach ($services as $service => $quote) {
  364. if (!in_array($service, $usps_services)) {
  365. unset($services[$service]);
  366. }
  367. }
  368. foreach ($services as $key => $quote) {
  369. if (isset($quote['rate'])) {
  370. $services[$key]['rate'] = $quote['rate'];
  371. $services[$key]['option_label'] = theme('uc_usps_option_label', array('service' => $quote['label'], 'packages' => $packages));
  372. }
  373. }
  374. uasort($services, 'uc_quote_price_sort');
  375. // Merge debug data into $services. This is necessary because
  376. // $debug_data is not sortable by a 'rate' key, so it has to be
  377. // kept separate from the $services data until this point.
  378. if (isset($debug_data['debug']) ||
  379. (isset($debug_data['error']) && count($debug_data['error']))) {
  380. $services['data'] = $debug_data;
  381. }
  382. return $services;
  383. }
  384. /**
  385. * Constructs a quote request for domestic shipments.
  386. *
  387. * @param $packages
  388. * Array of packages received from the cart.
  389. * @param $origin
  390. * Delivery origin address information.
  391. * @param $destination
  392. * Delivery destination address information.
  393. *
  394. * @return
  395. * RateV4Request XML document to send to USPS.
  396. */
  397. function uc_usps_rate_request($packages, $origin, $destination) {
  398. $request = '<RateV4Request USERID="' . variable_get('uc_usps_user_id', '') . '">';
  399. $request .= '<Revision>2</Revision>';
  400. $rate_type = variable_get('uc_usps_online_rates', FALSE);
  401. $package_id = 0;
  402. foreach ($packages as $package) {
  403. $qty = $package->qty;
  404. for ($i = 0; $i < $qty; $i++) {
  405. $request .= '<Package ID="' . $package_id . '">' .
  406. '<Service>' . ($rate_type ? 'ONLINE' : 'ALL') . '</Service>' .
  407. '<ZipOrigination>' . substr(trim($origin->postal_code), 0, 5) . '</ZipOrigination>' .
  408. '<ZipDestination>' . substr(trim($destination->postal_code), 0, 5) . '</ZipDestination>' .
  409. '<Pounds>' . intval($package->pounds) . '</Pounds>' .
  410. '<Ounces>' . number_format($package->ounces, 1, '.', '') . '</Ounces>' .
  411. '<Container>' . $package->container . '</Container>' .
  412. '<Size>' . $package->size . '</Size>' .
  413. '<Width>' . $package->width . '</Width>' .
  414. '<Length>' . $package->length . '</Length>' .
  415. '<Height>' . $package->height . '</Height>' .
  416. '<Girth>' . $package->girth . '</Girth>' .
  417. '<Value>' . $package->price . '</Value>' .
  418. '<Machinable>' . ($package->machinable ? 'TRUE' : 'FALSE') . '</Machinable>' .
  419. '<ReturnLocations>TRUE</ReturnLocations>' .
  420. '<ShipDate Option="EMSH">' . format_date(time(), 'custom', 'd-M-Y', 'America/New_York', 'en') . '</ShipDate>';
  421. // Check if we need to add any special services to this package.
  422. if (variable_get('uc_usps_insurance', FALSE) ||
  423. variable_get('uc_usps_delivery_confirmation', FALSE) ||
  424. variable_get('uc_usps_signature_confirmation', FALSE) ) {
  425. $request .= '<SpecialServices>';
  426. if (variable_get('uc_usps_insurance', FALSE)) {
  427. $request .= '<SpecialService>1</SpecialService>';
  428. }
  429. if (variable_get('uc_usps_delivery_confirmation', FALSE)) {
  430. $request .= '<SpecialService>13</SpecialService>';
  431. }
  432. if (variable_get('uc_usps_signature_confirmation', FALSE)) {
  433. $request .= '<SpecialService>15</SpecialService>';
  434. }
  435. $request .= '</SpecialServices>';
  436. }
  437. // Close off Package tag.
  438. $request .= '</Package>';
  439. $package_id++;
  440. }
  441. }
  442. $request .= '</RateV4Request>';
  443. return 'API=RateV4&XML=' . drupal_encode_path($request);
  444. }
  445. /**
  446. * Constructs a quote request for international shipments.
  447. *
  448. * @param $packages
  449. * Array of packages received from the cart.
  450. * @param $origin
  451. * Delivery origin address information.
  452. * @param $destination
  453. * Delivery destination address information.
  454. *
  455. * @return
  456. * IntlRateRequest XML document to send to USPS.
  457. */
  458. function uc_usps_intl_rate_request($packages, $origin, $destination) {
  459. module_load_include('inc', 'uc_usps', 'uc_usps.countries');
  460. $request = '<IntlRateV2Request USERID="' . variable_get('uc_usps_user_id', '') . '">';
  461. $request .= '<Revision>2</Revision>';
  462. // USPS does not use ISO 3166 country name in some cases so we
  463. // need to transform ISO country name into one USPS understands.
  464. $shipto_country = uc_usps_country_map($destination->country);
  465. $package_id = 0;
  466. foreach ($packages as $package) {
  467. $qty = $package->qty;
  468. for ($i = 0; $i < $qty; $i++) {
  469. $request .= '<Package ID="' . $package_id . '">' .
  470. '<Pounds>' . intval($package->pounds) . '</Pounds>' .
  471. '<Ounces>' . ceil($package->ounces) . '</Ounces>' .
  472. '<MailType>All</MailType>' .
  473. '<ValueOfContents>' . $package->price . '</ValueOfContents>' .
  474. '<Country>' . $shipto_country . '</Country>' .
  475. '<Container>' . 'RECTANGULAR' . '</Container>' .
  476. '<Size>' . 'REGULAR' . '</Size>' .
  477. '<Width>' . '</Width>' .
  478. '<Length>' . '</Length>' .
  479. '<Height>' . '</Height>' .
  480. '<Girth>' . '</Girth>' .
  481. '<OriginZip>' . substr(trim($origin->postal_code), 0, 5) . '</OriginZip>';
  482. // Check if we need to add any special services to this package.
  483. if (variable_get('uc_usps_insurance', FALSE)) {
  484. $request .= '<ExtraServices><ExtraService>1</ExtraService></ExtraServices>';
  485. }
  486. // Close off Package tag.
  487. $request .= '</Package>';
  488. $package_id++;
  489. }
  490. }
  491. $request .= '</IntlRateV2Request>';
  492. $request = 'API=IntlRateV2&XML=' . drupal_encode_path($request);
  493. return $request;
  494. }
  495. /**
  496. * Modifies the rate received from USPS before displaying to the customer.
  497. *
  498. * @param $rate
  499. * Shipping rate without any rate markup.
  500. *
  501. * @return
  502. * Shipping rate after markup.
  503. */
  504. function uc_usps_rate_markup($rate) {
  505. $markup = trim(variable_get('uc_usps_rate_markup', '0'));
  506. $type = variable_get('uc_usps_rate_markup_type', 'percentage');
  507. if (is_numeric($markup)) {
  508. switch ($type) {
  509. case 'percentage':
  510. return $rate + $rate * floatval($markup) / 100;
  511. case 'multiplier':
  512. return $rate * floatval($markup);
  513. case 'currency':
  514. return $rate + floatval($markup);
  515. }
  516. }
  517. else {
  518. return $rate;
  519. }
  520. }
  521. /**
  522. * Modifies the weight of shipment before sending to USPS for a quote.
  523. *
  524. * @param $weight
  525. * Shipping weight without any weight markup.
  526. *
  527. * @return
  528. * Shipping weight after markup.
  529. */
  530. function uc_usps_weight_markup($weight) {
  531. $markup = trim(variable_get('uc_usps_weight_markup', '0'));
  532. $type = variable_get('uc_usps_weight_markup_type', 'percentage');
  533. if (is_numeric($markup)) {
  534. switch ($type) {
  535. case 'percentage':
  536. return $weight + $weight * floatval($markup) / 100;
  537. case 'multiplier':
  538. return $weight * floatval($markup);
  539. case 'mass':
  540. return $weight + floatval($markup);
  541. }
  542. }
  543. else {
  544. return $weight;
  545. }
  546. }
  547. /**
  548. * Organizes products into packages for shipment.
  549. *
  550. * @param $products
  551. * An array of product objects as they are represented in the cart or order.
  552. * @param &$addresses
  553. * Reference to an array of addresses which are the pickup locations of each
  554. * package. They are determined from the shipping addresses of their
  555. * component products.
  556. *
  557. * @return
  558. * Array of packaged products. Packages are separated by shipping address and
  559. * weight or quantity limits imposed by the shipping method or the products.
  560. */
  561. function _uc_usps_package_products($products, &$addresses) {
  562. $last_key = 0;
  563. $packages = array();
  564. if (variable_get('uc_usps_all_in_one', TRUE) && count($products) > 1) {
  565. // "All in one" packaging strategy.
  566. // Only need to do this if more than one product line item in order.
  567. $packages[$last_key] = array(0 => _uc_usps_new_package());
  568. foreach ($products as $product) {
  569. if ($product->nid) {
  570. // Packages are grouped by the address from which they will be
  571. // shipped. We will keep track of the different addresses in an array
  572. // and use their keys for the array of packages.
  573. $key = NULL;
  574. $address = uc_quote_get_default_shipping_address($product->nid);
  575. foreach ($addresses as $index => $value) {
  576. if ($address->isSamePhysicalLocation($value)) {
  577. // This is an existing address.
  578. $key = $index;
  579. break;
  580. }
  581. }
  582. if (!isset($key)) {
  583. // This is a new address. Increment the address counter $last_key
  584. // instead of using [] so that it can be used in $packages and
  585. // $addresses.
  586. $addresses[++$last_key] = $address;
  587. $key = $last_key;
  588. $packages[$key] = array(0 => _uc_usps_new_package());
  589. }
  590. }
  591. // Grab some product properties directly from the (cached) product
  592. // data. They are not normally available here because the $product
  593. // object is being read out of the $order object rather than from
  594. // the database, and the $order object only carries around a limited
  595. // number of product properties.
  596. $temp = node_load($product->nid);
  597. $product->length = $temp->length;
  598. $product->width = $temp->width;
  599. $product->height = $temp->height;
  600. $product->length_units = $temp->length_units;
  601. $product->usps['container'] = isset($temp->usps['container']) ? $temp->usps['container'] : 'VARIABLE';
  602. $packages[$key][0]->price += $product->price * $product->qty;
  603. $packages[$key][0]->weight += $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
  604. }
  605. foreach ($packages as $key => $package) {
  606. $packages[$key][0]->pounds = floor($package[0]->weight);
  607. $packages[$key][0]->ounces = LB_TO_OZ * ($package[0]->weight - $packages[$key][0]->pounds);
  608. $packages[$key][0]->container = 'VARIABLE';
  609. $packages[$key][0]->size = 'REGULAR';
  610. // Packages are "machinable" if heavier than 6oz. and less than 35lbs.
  611. $packages[$key][0]->machinable = (
  612. ($packages[$key][0]->pounds == 0 ? $packages[$key][0]->ounces >= 6 : TRUE) &&
  613. $packages[$key][0]->pounds <= 35 &&
  614. ($packages[$key][0]->pounds == 35 ? $packages[$key][0]->ounces == 0 : TRUE)
  615. );
  616. $packages[$key][0]->qty = 1;
  617. }
  618. }
  619. else {
  620. // !variable_get('uc_usps_all_in_one', TRUE) || count($products) = 1
  621. // "Each in own" packaging strategy, or only one product line item in order.
  622. foreach ($products as $product) {
  623. if ($product->nid) {
  624. $address = uc_quote_get_default_shipping_address($product->nid);
  625. if (in_array($address, $addresses)) {
  626. // This is an existing address.
  627. foreach ($addresses as $index => $value) {
  628. if ($address == $value) {
  629. $key = $index;
  630. break;
  631. }
  632. }
  633. }
  634. else {
  635. // This is a new address.
  636. $addresses[++$last_key] = $address;
  637. $key = $last_key;
  638. }
  639. }
  640. if (!isset($product->pkg_qty) || !$product->pkg_qty) {
  641. $product->pkg_qty = 1;
  642. }
  643. $num_of_pkgs = (int) ($product->qty / $product->pkg_qty);
  644. if ($num_of_pkgs) {
  645. $package = clone $product;
  646. $package->description = $product->model;
  647. $weight = $product->weight * $product->pkg_qty;
  648. switch ($product->weight_units) {
  649. case 'g':
  650. // Convert to kg and fall through.
  651. $weight = $weight * G_TO_KG;
  652. case 'kg':
  653. // Convert to lb and fall through.
  654. $weight = $weight * KG_TO_LB;
  655. case 'lb':
  656. $package->pounds = floor($weight);
  657. $package->ounces = LB_TO_OZ * ($weight - $package->pounds);
  658. break;
  659. case 'oz':
  660. $package->pounds = floor($weight * OZ_TO_LB);
  661. $package->ounces = $weight - $package->pounds * LB_TO_OZ;
  662. break;
  663. }
  664. // Grab some product properties directly from the (cached) product
  665. // data. They are not normally available here because the $product
  666. // object is being read out of the $order object rather than from
  667. // the database, and the $order object only carries around a limited
  668. // number of product properties.
  669. $temp = node_load($product->nid);
  670. $product->length = $temp->length;
  671. $product->width = $temp->width;
  672. $product->height = $temp->height;
  673. $product->length_units = $temp->length_units;
  674. $product->usps['container'] = isset($temp->usps['container']) ? $temp->usps['container'] : 'VARIABLE';
  675. $package->container = $product->usps['container'];
  676. $length_conversion = uc_length_conversion($product->length_units, 'in');
  677. $package->length = max($product->length, $product->width) * $length_conversion;
  678. $package->width = min($product->length, $product->width) * $length_conversion;
  679. $package->height = $product->height * $length_conversion;
  680. if ($package->length < $package->height) {
  681. list($package->length, $package->height) = array($package->height, $package->length);
  682. }
  683. $package->girth = 2 * $package->width + 2 * $package->height;
  684. $package->size = $package->length <= 12 ? 'REGULAR' : 'LARGE';
  685. $package->machinable = (
  686. $package->length >= 6 && $package->length <= 34 &&
  687. $package->width >= 0.25 && $package->width <= 17 &&
  688. $package->height >= 3.5 && $package->height <= 17 &&
  689. ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) &&
  690. $package->pounds <= 35 &&
  691. ($package->pounds == 35 ? $package->ounces == 0 : TRUE)
  692. );
  693. $package->price = $product->price * $product->pkg_qty;
  694. $package->qty = $num_of_pkgs;
  695. $packages[$key][] = $package;
  696. }
  697. $remaining_qty = $product->qty % $product->pkg_qty;
  698. if ($remaining_qty) {
  699. $package = clone $product;
  700. $package->description = $product->model;
  701. $weight = $product->weight * $remaining_qty;
  702. switch ($product->weight_units) {
  703. case 'g':
  704. // Convert to kg and fall through.
  705. $weight = $weight * G_TO_KG;
  706. case 'kg':
  707. // Convert to lb and fall through.
  708. $weight = $weight * KG_TO_LB;
  709. case 'lb':
  710. $package->pounds = floor($weight);
  711. $package->ounces = LB_TO_OZ * ($weight - $package->pounds);
  712. break;
  713. case 'oz':
  714. $package->pounds = floor($weight * OZ_TO_LB);
  715. $package->ounces = $weight - $package->pounds * LB_TO_OZ;
  716. break;
  717. }
  718. $package->container = $product->usps['container'];
  719. $length_conversion = uc_length_conversion($product->length_units, 'in');
  720. $package->length = max($product->length, $product->width) * $length_conversion;
  721. $package->width = min($product->length, $product->width) * $length_conversion;
  722. $package->height = $product->height * $length_conversion;
  723. if ($package->length < $package->height) {
  724. list($package->length, $package->height) = array($package->height, $package->length);
  725. }
  726. $package->girth = 2 * $package->width + 2 * $package->height;
  727. $package->size = $package->length <= 12 ? 'REGULAR' : 'LARGE';
  728. $package->machinable = (
  729. $package->length >= 6 && $package->length <= 34 &&
  730. $package->width >= 0.25 && $package->width <= 17 &&
  731. $package->height >= 3.5 && $package->height <= 17 &&
  732. ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) &&
  733. $package->pounds <= 35 &&
  734. ($package->pounds == 35 ? $package->ounces == 0 : TRUE)
  735. );
  736. $package->price = $product->price * $remaining_qty;
  737. $package->qty = 1;
  738. $packages[$key][] = $package;
  739. }
  740. }
  741. }
  742. return $packages;
  743. }
  744. /**
  745. * Convenience function for select form elements.
  746. */
  747. function _uc_usps_pkg_types() {
  748. return array(
  749. 'VARIABLE' => t('Variable'),
  750. 'FLAT RATE ENVELOPE' => t('Flat rate envelope'),
  751. 'PADDED FLAT RATE ENVELOPE' => t('Padded flat rate envelope'),
  752. 'LEGAL FLAT RATE ENVELOPE' => t('Legal flat rate envelope'),
  753. 'SMALL FLAT RATE ENVELOPE' => t('Small flat rate envelope'),
  754. 'WINDOW FLAT RATE ENVELOPE' => t('Window flat rate envelope'),
  755. 'GIFT CARD FLAT RATE BOX' => t('Gift card flat rate box'),
  756. 'FLAT RATE BOX' => t('Flat rate box'),
  757. 'SM FLAT RATE BOX' => t('Small flat rate box'),
  758. 'MD FLAT RATE BOX' => t('Medium flat rate box'),
  759. 'LG FLAT RATE BOX' => t('Large flat rate box'),
  760. 'REGIONALRATEBOXA' => t('Regional rate box A'),
  761. 'REGIONALRATEBOXB' => t('Regional rate box B'),
  762. 'RECTANGULAR' => t('Rectangular'),
  763. 'NONRECTANGULAR' => t('Non-rectangular'),
  764. );
  765. }
  766. /**
  767. * Maps envelope shipment services to their IDs.
  768. */
  769. function _uc_usps_env_services() {
  770. return array(
  771. 'zero' => t('U.S.P.S. First-Class Mail Postcard'),
  772. 'zeroFlat' => t('U.S.P.S. First-Class Mail Letter'),
  773. 12 => t('U.S.P.S. First-Class Postcard Stamped'),
  774. 1 => t('U.S.P.S. Priority Mail'),
  775. 16 => t('U.S.P.S. Priority Mail Flat-Rate Envelope'),
  776. 3 => t('U.S.P.S. Express Mail'),
  777. 13 => t('U.S.P.S. Express Mail Flat-Rate Envelope'),
  778. 23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'),
  779. 25 => t('U.S.P.S. Express Mail Flat-Rate Envelope Sunday/Holiday Guarantee'),
  780. );
  781. }
  782. /**
  783. * Maps parcel shipment services to their IDs.
  784. */
  785. function _uc_usps_services() {
  786. return array(
  787. 'zeroFlat' => t('U.S.P.S. First-Class Mail Letter'),
  788. 'zeroParcel' => t('U.S.P.S. First-Class Mail Parcel'),
  789. 1 => t('U.S.P.S. Priority Mail'),
  790. 28 => t('U.S.P.S. Priority Mail Small Flat-Rate Box'),
  791. 17 => t('U.S.P.S. Priority Mail Regular/Medium Flat-Rate Box'),
  792. 22 => t('U.S.P.S. Priority Mail Large Flat-Rate Box'),
  793. 3 => t('U.S.P.S. Express Mail'),
  794. 23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'),
  795. 4 => t('U.S.P.S. Parcel Post'),
  796. 5 => t('U.S.P.S. Bound Printed Matter'),
  797. 6 => t('U.S.P.S. Media Mail'),
  798. 7 => t('U.S.P.S. Library'),
  799. );
  800. }
  801. /**
  802. * Maps international envelope services to their IDs.
  803. */
  804. function _uc_usps_intl_env_services() {
  805. return array(
  806. 13 => t('First Class Mail International Letter'),
  807. 14 => t('First Class Mail International Large Envelope'),
  808. 2 => t('Priority Mail International'),
  809. 8 => t('Priority Mail International Flat Rate Envelope'),
  810. 4 => t('Global Express Guaranteed'),
  811. 12 => t('GXG Envelopes'),
  812. 1 => t('Express Mail International (EMS)'),
  813. 10 => t('Express Mail International (EMS) Flat Rate Envelope'),
  814. );
  815. }
  816. /**
  817. * Maps international parcel services to their IDs.
  818. */
  819. function _uc_usps_intl_services() {
  820. return array(
  821. 15 => t('First Class Mail International Package'),
  822. 2 => t('Priority Mail International'),
  823. 16 => t('Priority Mail International Small Flat-Rate Box'),
  824. 9 => t('Priority Mail International Regular/Medium Flat-Rate Box'),
  825. 11 => t('Priority Mail International Large Flat-Rate Box'),
  826. 4 => t('Global Express Guaranteed'),
  827. 6 => t('Global Express Guaranteed Non-Document Rectangular'),
  828. 7 => t('Global Express Guaranteed Non-Document Non-Rectangular'),
  829. 1 => t('Express Mail International (EMS)'),
  830. );
  831. }
  832. /**
  833. * Pseudo-constructor to set default values of a package.
  834. */
  835. function _uc_usps_new_package() {
  836. $package = new stdClass();
  837. $package->price = 0;
  838. $package->qty = 1;
  839. $package->pounds = 0;
  840. $package->ounces = 0;
  841. $package->container = 0;
  842. $package->size = 0;
  843. $package->machinable = TRUE;
  844. $package->length = 0;
  845. $package->width = 0;
  846. $package->height = 0;
  847. $package->girth = 0;
  848. // $package->length_units = 'in';
  849. // $package->weight_units = 'lb';
  850. return $package;
  851. }