uc_ups.module 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. <?php
  2. /**
  3. * @file
  4. * UPS shipping quote module.
  5. */
  6. /******************************************************************************
  7. * Drupal Hooks *
  8. ******************************************************************************/
  9. /**
  10. * Implements hook_menu().
  11. */
  12. function uc_ups_menu() {
  13. $items = array();
  14. $items['admin/store/settings/quotes/settings/ups'] = array(
  15. 'title' => 'UPS',
  16. 'page callback' => 'drupal_get_form',
  17. 'page arguments' => array('uc_ups_admin_settings'),
  18. 'access arguments' => array('configure quotes'),
  19. 'type' => MENU_LOCAL_TASK,
  20. 'file' => 'uc_ups.admin.inc',
  21. );
  22. $items['admin/store/orders/%uc_order/shipments/ups'] = array(
  23. 'title' => 'UPS shipment',
  24. 'page callback' => 'drupal_get_form',
  25. 'page arguments' => array('uc_ups_confirm_shipment', 3),
  26. 'access arguments' => array('fulfill orders'),
  27. 'file' => 'uc_ups.ship.inc',
  28. );
  29. $items['admin/store/orders/%uc_order/shipments/labels/ups'] = array(
  30. 'page callback' => 'theme',
  31. 'page arguments' => array('uc_ups_label_image'),
  32. 'access arguments' => array('fulfill orders'),
  33. 'file' => 'uc_ups.ship.inc',
  34. );
  35. return $items;
  36. }
  37. /**
  38. * Implements hook_cron().
  39. *
  40. * Deletes UPS shipping labels from the file system automatically
  41. * on a periodic basis. Cron must be enabled for automatic deletion.
  42. * Default is never delete the labels, keep them forever.
  43. */
  44. function uc_ups_cron() {
  45. $cutoff = REQUEST_TIME - variable_get('uc_ups_label_lifetime', 0);
  46. if ($cutoff == REQUEST_TIME) {
  47. // Label lifetime is set to 0, meaning never delete.
  48. return;
  49. }
  50. // Loop over label files in public://ups_labels and test
  51. // creation date against 'uc_ups_label_lifetime'.
  52. $files = file_scan_directory('public://ups_labels', '/^label-/');
  53. foreach ($files as $file) {
  54. if ($cutoff > filectime($file->uri)) {
  55. drupal_unlink($file->uri);
  56. watchdog('uc_ups', 'Removed uc_ups label file @file.', array('@file' => $file->uri), WATCHDOG_NOTICE);
  57. }
  58. }
  59. }
  60. /**
  61. * Implements hook_theme().
  62. */
  63. function uc_ups_theme() {
  64. return array(
  65. 'uc_ups_option_label' => array(
  66. 'variables' => array(
  67. 'service' => NULL,
  68. 'packages' => NULL,
  69. ),
  70. 'file' => 'uc_ups.theme.inc',
  71. ),
  72. 'uc_ups_confirm_shipment' => array(
  73. 'render element' => 'form',
  74. 'file' => 'uc_ups.ship.inc',
  75. ),
  76. 'uc_ups_label_image' => array(
  77. 'variables' => array(),
  78. 'file' => 'uc_ups.ship.inc',
  79. ),
  80. );
  81. }
  82. /**
  83. * Implements hook_form_alter().
  84. *
  85. * Adds package type to products.
  86. *
  87. * @see uc_product_form()
  88. * @see uc_ups_product_alter_validate()
  89. */
  90. function uc_ups_form_alter(&$form, &$form_state, $form_id) {
  91. if (uc_product_is_product_form($form)) {
  92. $node = $form['#node'];
  93. $enabled = variable_get('uc_quote_enabled', array()) + array('ups' => FALSE);
  94. $weight = variable_get('uc_quote_method_weight', array()) + array('ups' => 0);
  95. $ups = array(
  96. '#type' => 'fieldset',
  97. '#title' => t('UPS product description'),
  98. '#collapsible' => TRUE,
  99. '#collapsed' => ($enabled['ups'] == FALSE || uc_product_get_shipping_type($node) != 'small_package'),
  100. '#weight' => $weight['ups'],
  101. '#tree' => TRUE,
  102. );
  103. $ups['pkg_type'] = array(
  104. '#type' => 'select',
  105. '#title' => t('Package type'),
  106. '#options' => _uc_ups_pkg_types(),
  107. '#default_value' => isset($node->ups['pkg_type']) ? $node->ups['pkg_type'] : variable_get('uc_ups_pkg_type', '02'),
  108. );
  109. $form['shipping']['ups'] = $ups;
  110. if ($enabled['ups']) {
  111. $form['#validate'][] = 'uc_ups_product_alter_validate';
  112. }
  113. }
  114. }
  115. /**
  116. * Validation handler for UPS product fields.
  117. *
  118. * @see uc_ups_form_alter()
  119. */
  120. function uc_ups_product_alter_validate($form, &$form_state) {
  121. if (isset($form_state['values']['shippable']) && ($form_state['values']['shipping_type'] == 'small_package' || (empty($form_state['values']['shipping_type']) && variable_get('uc_store_shipping_type', 'small_package') == 'small_package'))) {
  122. if ($form_state['values']['ups']['pkg_type'] == '02' && (empty($form_state['values']['dim_length']) || empty($form_state['values']['dim_width']) || empty($form_state['values']['dim_height']))) {
  123. form_set_error('base][dimensions', t('Dimensions are required for custom packaging.'));
  124. }
  125. }
  126. }
  127. /**
  128. * Implements hook_node_insert().
  129. */
  130. function uc_ups_node_insert($node) {
  131. uc_ups_node_update($node);
  132. }
  133. /**
  134. * Implements hook_node_update().
  135. */
  136. function uc_ups_node_update($node) {
  137. if (uc_product_is_product($node->type)) {
  138. if (isset($node->ups)) {
  139. $ups_values = $node->ups;
  140. if (empty($node->revision)) {
  141. db_delete('uc_ups_products')
  142. ->condition('vid', $node->vid)
  143. ->execute();
  144. }
  145. db_insert('uc_ups_products')
  146. ->fields(array(
  147. 'vid' => $node->vid,
  148. 'nid' => $node->nid,
  149. 'pkg_type' => $ups_values['pkg_type'],
  150. ))
  151. ->execute();
  152. }
  153. }
  154. }
  155. /**
  156. * Implements hook_node_load().
  157. */
  158. function uc_ups_node_load($nodes, $types) {
  159. $product_types = array_intersect(uc_product_types(), $types);
  160. if (empty($product_types)) {
  161. return;
  162. }
  163. $vids = array();
  164. $shipping_type = variable_get('uc_store_shipping_type', 'small_package');
  165. $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();
  166. foreach ($nodes as $nid => $node) {
  167. if (!in_array($node->type, $product_types)) {
  168. continue;
  169. }
  170. if (isset($shipping_types[$nid])) {
  171. $node->shipping_type = $shipping_types[$nid];
  172. }
  173. else {
  174. $node->shipping_type = $shipping_type;
  175. }
  176. if ($node->shipping_type == 'small_package') {
  177. $vids[$nid] = $node->vid;
  178. }
  179. }
  180. if ($vids) {
  181. $result = db_query("SELECT * FROM {uc_ups_products} WHERE vid IN (:vids)", array(':vids' => $vids), array('fetch' => PDO::FETCH_ASSOC));
  182. foreach ($result as $ups) {
  183. $nodes[$ups['nid']]->ups = $ups;
  184. }
  185. }
  186. }
  187. /**
  188. * Implements hook_node_delete().
  189. */
  190. function uc_ups_node_delete($node) {
  191. db_delete('uc_ups_products')
  192. ->condition('nid', $node->nid)
  193. ->execute();
  194. }
  195. /**
  196. * Implements hook_node_revision_delete().
  197. */
  198. function uc_ups_node_revision_delete($node) {
  199. db_delete('uc_ups_products')
  200. ->condition('vid', $node->vid)
  201. ->execute();
  202. }
  203. /******************************************************************************
  204. * Ubercart Hooks *
  205. ******************************************************************************/
  206. /**
  207. * Implements hook_uc_shipping_type().
  208. */
  209. function uc_ups_uc_shipping_type() {
  210. $weight = variable_get('uc_quote_type_weight', array('small_package' => 0));
  211. $types = array();
  212. $types['small_package'] = array(
  213. 'id' => 'small_package',
  214. 'title' => t('Small packages'),
  215. 'weight' => $weight['small_package'],
  216. );
  217. return $types;
  218. }
  219. /**
  220. * Implements hook_uc_shipping_method().
  221. */
  222. function uc_ups_uc_shipping_method() {
  223. $methods['ups'] = array(
  224. 'id' => 'ups',
  225. 'module' => 'uc_ups',
  226. 'title' => t('UPS'),
  227. 'operations' => array(
  228. 'configure' => array(
  229. 'title' => t('configure'),
  230. 'href' => 'admin/store/settings/quotes/settings/ups',
  231. ),
  232. ),
  233. 'quote' => array(
  234. 'type' => 'small_package',
  235. 'callback' => 'uc_ups_quote',
  236. 'accessorials' => _uc_ups_service_list(),
  237. ),
  238. 'ship' => array(
  239. 'type' => 'small_package',
  240. 'callback' => 'uc_ups_fulfill_order',
  241. 'file' => 'uc_ups.ship.inc',
  242. 'pkg_types' => _uc_ups_pkg_types(),
  243. ),
  244. 'cancel' => 'uc_ups_void_shipment',
  245. );
  246. return $methods;
  247. }
  248. /**
  249. * Implements hook_uc_store_status().
  250. *
  251. * Lets the administrator know that the UPS account information has not been
  252. * filled out.
  253. */
  254. function uc_ups_uc_store_status() {
  255. $messages = array();
  256. $access = variable_get('uc_ups_access_license', '') != '';
  257. $account = variable_get('uc_ups_shipper_number', '') != '';
  258. $user = variable_get('uc_ups_user_id', '') != '';
  259. $password = variable_get('uc_ups_password', '') != '';
  260. if ($access && $account && $user && $password) {
  261. $messages[] = array(
  262. 'status' => 'ok',
  263. 'title' => t('UPS Online Tools'),
  264. 'desc' => t('Information needed to access UPS Online Tools has been entered.'),
  265. );
  266. }
  267. else {
  268. $messages[] = array(
  269. 'status' => 'error',
  270. 'title' => t('UPS Online Tools'),
  271. 'desc' => t('More information is needed to access UPS Online Tools. Please enter it <a href="!url">here</a>.', array('!url' => url('admin/store/settings/quotes/settings/ups'))),
  272. );
  273. }
  274. return $messages;
  275. }
  276. /******************************************************************************
  277. * Module Functions *
  278. ******************************************************************************/
  279. /**
  280. * Returns XML access request to be prepended to all requests to the
  281. * UPS webservice.
  282. */
  283. function uc_ups_access_request() {
  284. $access = variable_get('uc_ups_access_license', '');
  285. $user = variable_get('uc_ups_user_id', '');
  286. $password = variable_get('uc_ups_password', '');
  287. return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  288. <AccessRequest xml:lang=\"en-US\">
  289. <AccessLicenseNumber>$access</AccessLicenseNumber>
  290. <UserId>$user</UserId>
  291. <Password>$password</Password>
  292. </AccessRequest>
  293. ";
  294. }
  295. /**
  296. * Constructs an XML quote request.
  297. *
  298. * @param $packages
  299. * Array of packages received from the cart.
  300. * @param $origin
  301. * Delivery origin address information.
  302. * @param $destination
  303. * Delivery destination address information.
  304. * @param $ups_service
  305. * UPS service code (refers to UPS Ground, Next-Day Air, etc.).
  306. *
  307. * @return
  308. * RatingServiceSelectionRequest XML document to send to UPS.
  309. */
  310. function uc_ups_shipping_quote($packages, $origin, $destination, $ups_service) {
  311. $store['name'] = uc_store_name();
  312. $store['owner'] = variable_get('uc_store_owner', NULL);
  313. $store['email'] = uc_store_email();
  314. $store['email_from'] = uc_store_email();
  315. $store['phone'] = variable_get('uc_store_phone', NULL);
  316. $store['fax'] = variable_get('uc_store_fax', NULL);
  317. $store['street1'] = variable_get('uc_store_street1', NULL);
  318. $store['street2'] = variable_get('uc_store_street2', NULL);
  319. $store['city'] = variable_get('uc_store_city', NULL);
  320. $store['zone'] = variable_get('uc_store_zone', NULL);
  321. $store['postal_code'] = variable_get('uc_store_postal_code', NULL);
  322. $store['country'] = variable_get('uc_store_country', 840);
  323. $account = variable_get('uc_ups_shipper_number', '');
  324. $ua = explode(' ', $_SERVER['HTTP_USER_AGENT']);
  325. $user_agent = $ua[0];
  326. $services = _uc_ups_service_list();
  327. $service = array('code' => $ups_service, 'description' => $services[$ups_service]);
  328. $pkg_types = _uc_ups_pkg_types();
  329. $shipper_zone = uc_get_zone_code($store['zone']);
  330. $shipper_country = uc_get_country_data(array('country_id' => $store['country']));
  331. $shipper_country = $shipper_country[0]['country_iso_code_2'];
  332. $shipper_zip = $store['postal_code'];
  333. $shipto_zone = uc_get_zone_code($destination->zone);
  334. $shipto_country = uc_get_country_data(array('country_id' => $destination->country));
  335. $shipto_country = $shipto_country[0]['country_iso_code_2'];
  336. $shipto_zip = $destination->postal_code;
  337. $shipfrom_zone = uc_get_zone_code($origin->zone);
  338. $shipfrom_country = uc_get_country_data(array('country_id' => $origin->country));
  339. $shipfrom_country = $shipfrom_country[0]['country_iso_code_2'];
  340. $shipfrom_zip = $origin->postal_code;
  341. $ups_units = variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'));
  342. switch ($ups_units) {
  343. case 'in':
  344. $units = 'LBS';
  345. $unit_name = 'Pounds';
  346. break;
  347. case 'cm':
  348. $units = 'KGS';
  349. $unit_name = 'Kilograms';
  350. break;
  351. }
  352. $shipment_weight = 0;
  353. $package_schema = '';
  354. foreach ($packages as $package) {
  355. // Determine length conversion factor and weight conversion factor
  356. // for this shipment.
  357. $length_factor = uc_length_conversion($package->length_units, variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in')));
  358. switch ($ups_units) {
  359. case 'in':
  360. $weight_factor = uc_weight_conversion($package->weight_units, 'lb');
  361. break;
  362. case 'cm':
  363. $weight_factor = uc_weight_conversion($package->weight_units, 'kg');
  364. break;
  365. }
  366. // Loop over quantity of packages in this shipment.
  367. $qty = $package->qty;
  368. for ($i = 0; $i < $qty; $i++) {
  369. // Build XML for this package.
  370. $package_type = array('code' => $package->pkg_type, 'description' => $pkg_types[$package->pkg_type]);
  371. $package_schema .= "<Package>";
  372. $package_schema .= "<PackagingType>";
  373. $package_schema .= "<Code>" . $package_type['code'] . "</Code>";
  374. $package_schema .= "</PackagingType>";
  375. if ($package->pkg_type == '02' && $package->length && $package->width && $package->height) {
  376. if ($package->length < $package->width) {
  377. list($package->length, $package->width) = array($package->width, $package->length);
  378. }
  379. $package_schema .= "<Dimensions>";
  380. $package_schema .= "<UnitOfMeasurement>";
  381. $package_schema .= "<Code>" . strtoupper(variable_get('uc_ups_unit_system', variable_get('uc_length_unit', 'in'))) . "</Code>";
  382. $package_schema .= "</UnitOfMeasurement>";
  383. $package_schema .= "<Length>" . number_format($package->length * $length_factor, 2, '.', '') . "</Length>";
  384. $package_schema .= "<Width>" . number_format($package->width * $length_factor, 2, '.', '') . "</Width>";
  385. $package_schema .= "<Height>" . number_format($package->height * $length_factor, 2, '.', '') . "</Height>";
  386. $package_schema .= "</Dimensions>";
  387. }
  388. $weight = max(1, $package->weight * $weight_factor);
  389. $shipment_weight += $weight;
  390. $package_schema .= "<PackageWeight>";
  391. $package_schema .= "<UnitOfMeasurement>";
  392. $package_schema .= "<Code>" . $units . "</Code>";
  393. $package_schema .= "<Description>" . $unit_name . "</Description>";
  394. $package_schema .= "</UnitOfMeasurement>";
  395. $package_schema .= "<Weight>" . number_format($weight, 1, '.', '') . "</Weight>";
  396. $package_schema .= "</PackageWeight>";
  397. $size = $package->length * $length_factor + 2 * $length_factor * ($package->width + $package->height);
  398. if ($size > 130 && $size <= 165) {
  399. $package_schema .= "<LargePackageIndicator/>";
  400. }
  401. if (variable_get('uc_ups_insurance', TRUE)) {
  402. $package_schema .= "<PackageServiceOptions>";
  403. $package_schema .= "<InsuredValue>";
  404. $package_schema .= "<CurrencyCode>" . variable_get('uc_currency_code', 'USD') . "</CurrencyCode>";
  405. $package_schema .= "<MonetaryValue>" . $package->price . "</MonetaryValue>";
  406. $package_schema .= "</InsuredValue>";
  407. $package_schema .= "</PackageServiceOptions>";
  408. }
  409. $package_schema .= "</Package>";
  410. }
  411. }
  412. $schema = uc_ups_access_request() . "
  413. <?xml version=\"1.0\" encoding=\"UTF-8\"?>
  414. <RatingServiceSelectionRequest xml:lang=\"en-US\">
  415. <Request>
  416. <TransactionReference>
  417. <CustomerContext>Complex Rate Request</CustomerContext>
  418. <XpciVersion>1.0001</XpciVersion>
  419. </TransactionReference>
  420. <RequestAction>Rate</RequestAction>
  421. <RequestOption>rate</RequestOption>
  422. </Request>
  423. <PickupType>
  424. <Code>" . variable_get('uc_ups_pickup_type', '01') . "</Code>
  425. </PickupType>
  426. <CustomerClassification>
  427. <Code>" . variable_get('uc_ups_classification', '04') . "</Code>
  428. </CustomerClassification>
  429. <Shipment>
  430. <Shipper>
  431. <ShipperNumber>" . variable_get('uc_ups_shipper_number', '') . "</ShipperNumber>
  432. <Address>
  433. <City>" . $store['city'] . "</City>
  434. <StateProvinceCode>$shipper_zone</StateProvinceCode>
  435. <PostalCode>$shipper_zip</PostalCode>
  436. <CountryCode>$shipper_country</CountryCode>
  437. </Address>
  438. </Shipper>
  439. <ShipTo>
  440. <Address>
  441. <StateProvinceCode>$shipto_zone</StateProvinceCode>
  442. <PostalCode>$shipto_zip</PostalCode>
  443. <CountryCode>$shipto_country</CountryCode>
  444. ";
  445. if (variable_get('uc_ups_residential_quotes', 0)) {
  446. $schema .= "<ResidentialAddressIndicator/>
  447. ";
  448. }
  449. $schema .= "</Address>
  450. </ShipTo>
  451. <ShipFrom>
  452. <Address>
  453. <StateProvinceCode>$shipfrom_zone</StateProvinceCode>
  454. <PostalCode>$shipfrom_zip</PostalCode>
  455. <CountryCode>$shipfrom_country</CountryCode>
  456. </Address>
  457. </ShipFrom>
  458. <ShipmentWeight>
  459. <UnitOfMeasurement>
  460. <Code>$units</Code>
  461. <Description>$unit_name</Description>
  462. </UnitOfMeasurement>
  463. <Weight>" . number_format($shipment_weight, 1, '.', '') . "</Weight>
  464. </ShipmentWeight>
  465. <Service>
  466. <Code>{$service['code']}</Code>
  467. <Description>{$service['description']}</Description>
  468. </Service>
  469. ";
  470. $schema .= $package_schema;
  471. if (variable_get('uc_ups_negotiated_rates', FALSE)) {
  472. $schema .= "<RateInformation>
  473. <NegotiatedRatesIndicator/>
  474. </RateInformation>";
  475. }
  476. $schema .= "</Shipment>
  477. </RatingServiceSelectionRequest>";
  478. return $schema;
  479. }
  480. /**
  481. * Callback for retrieving a UPS shipping quote.
  482. *
  483. * Requests a quote for each enabled UPS Service. Therefore, the quote will
  484. * take longer to display to the user for each option the customer has
  485. * available.
  486. *
  487. * @param $products
  488. * Array of cart contents.
  489. * @param $details
  490. * Order details other than product information.
  491. * @param $method
  492. * The shipping method to create the quote.
  493. *
  494. * @return
  495. * JSON object containing rate, error, and debugging information.
  496. */
  497. function uc_ups_quote($products, $details, $method) {
  498. // The uc_quote AJAX query can fire before the customer has completely
  499. // filled out the destination address, so check to see whether the address
  500. // has all needed fields. If not, abort.
  501. $destination = (object) $details;
  502. if (empty($destination->zone) ||
  503. empty($destination->postal_code) ||
  504. empty($destination->country) ) {
  505. // Skip this shipping method.
  506. return array();
  507. }
  508. $quotes = array();
  509. $addresses = array(variable_get('uc_quote_store_default_address', new UcAddress()));
  510. $key = 0;
  511. $last_key = 0;
  512. $packages = array();
  513. if (variable_get('uc_ups_all_in_one', TRUE) && count($products) > 1) {
  514. foreach ($products as $product) {
  515. if ($product->nid) {
  516. // Packages are grouped by the address from which they will be
  517. // shipped. We will keep track of the different addresses in an array
  518. // and use their keys for the array of packages.
  519. $key = NULL;
  520. $address = uc_quote_get_default_shipping_address($product->nid);
  521. foreach ($addresses as $index => $value) {
  522. if ($address->isSamePhysicalLocation($value)) {
  523. // This is an existing address.
  524. $key = $index;
  525. break;
  526. }
  527. }
  528. if (!isset($key)) {
  529. // This is a new address. Increment the address counter $last_key
  530. // instead of using [] so that it can be used in $packages and
  531. // $addresses.
  532. $addresses[++$last_key] = $address;
  533. $key = $last_key;
  534. }
  535. }
  536. // Add this product to the last package from the found address or start
  537. // a new package.
  538. if (isset($packages[$key]) && count($packages[$key])) {
  539. $package = array_pop($packages[$key]);
  540. }
  541. else {
  542. $package = _uc_ups_new_package();
  543. }
  544. // Grab some product properties directly from the (cached) product
  545. // data. They are not normally available here because the $product
  546. // object is being read out of the $order object rather than from
  547. // the database, and the $order object only carries around a limited
  548. // number of product properties.
  549. $temp = node_load($product->nid);
  550. $product->length = $temp->length;
  551. $product->width = $temp->width;
  552. $product->height = $temp->height;
  553. $product->length_units = $temp->length_units;
  554. $product->ups['pkg_type'] = isset($temp->ups) ? $temp->ups['pkg_type'] : '02';
  555. $weight = $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
  556. $package->weight += $weight;
  557. $package->price += $product->price * $product->qty;
  558. $length_factor = uc_length_conversion($product->length_units, 'in');
  559. $package->length = max($product->length * $length_factor, $package->length);
  560. $package->width = max($product->width * $length_factor, $package->width);
  561. $package->height = max($product->height * $length_factor, $package->height);
  562. $packages[$key][] = $package;
  563. }
  564. foreach ($packages as $addr_key => $shipment) {
  565. foreach ($shipment as $key => $package) {
  566. if (!$package->weight) {
  567. unset($packages[$addr_key][$key]);
  568. continue;
  569. }
  570. elseif ($package->weight > 150) {
  571. // UPS has a weight limit on packages of 150 lbs. Pretend the
  572. // products can be divided into enough packages.
  573. $qty = floor($package->weight / 150) + 1;
  574. $package->qty = $qty;
  575. $package->weight /= $qty;
  576. $package->price /= $qty;
  577. }
  578. }
  579. }
  580. }
  581. else {
  582. foreach ($products as $product) {
  583. $key = 0;
  584. if ($product->nid) {
  585. $address = uc_quote_get_default_shipping_address($product->nid);
  586. if (in_array($address, $addresses)) {
  587. // This is an existing address.
  588. foreach ($addresses as $index => $value) {
  589. if ($address == $value) {
  590. $key = $index;
  591. break;
  592. }
  593. }
  594. }
  595. else {
  596. // This is a new address.
  597. $addresses[++$last_key] = $address;
  598. $key = $last_key;
  599. }
  600. }
  601. if (!isset($product->pkg_qty) || !$product->pkg_qty) {
  602. $product->pkg_qty = 1;
  603. }
  604. $num_of_pkgs = (int) ($product->qty / $product->pkg_qty);
  605. // Grab some product properties directly from the (cached) product
  606. // data. They are not normally available here because the $product
  607. // object is being read out of the $order object rather than from
  608. // the database, and the $order object only carries around a limited
  609. // number of product properties.
  610. $temp = node_load($product->nid);
  611. $product->length = $temp->length;
  612. $product->width = $temp->width;
  613. $product->height = $temp->height;
  614. $product->length_units = $temp->length_units;
  615. $product->ups['pkg_type'] = isset($temp->ups) ? $temp->ups['pkg_type'] : '02';
  616. if ($num_of_pkgs) {
  617. $package = clone $product;
  618. $package->description = $product->model;
  619. $package->weight = $product->weight * $product->pkg_qty;
  620. $package->price = $product->price * $product->pkg_qty;
  621. $package->qty = $num_of_pkgs;
  622. $package->pkg_type = $product->ups['pkg_type'];
  623. if ($package->weight) {
  624. $packages[$key][] = $package;
  625. }
  626. }
  627. $remaining_qty = $product->qty % $product->pkg_qty;
  628. if ($remaining_qty) {
  629. $package = clone $product;
  630. $package->description = $product->model;
  631. $package->weight = $product->weight * $remaining_qty;
  632. $package->price = $product->price * $remaining_qty;
  633. $package->qty = 1;
  634. $package->pkg_type = $product->ups['pkg_type'];
  635. if ($package->weight) {
  636. $packages[$key][] = $package;
  637. }
  638. }
  639. }
  640. }
  641. if (!count($packages)) {
  642. return array();
  643. }
  644. $dest = (object) $details;
  645. foreach ($packages as $key => $ship_packages) {
  646. $orig = $addresses[$key];
  647. $orig->email = uc_store_email();
  648. foreach (array_keys(array_filter(variable_get('uc_ups_services', array()))) as $ups_service) {
  649. $request = uc_ups_shipping_quote($ship_packages, $orig, $dest, $ups_service);
  650. $resp = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'Rate', array(
  651. 'method' => 'POST',
  652. 'data' => $request,
  653. ));
  654. if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
  655. if (!isset($debug_data[$ups_service]['debug'])) {
  656. $debug_data[$ups_service]['debug'] = '';
  657. }
  658. $debug_data[$ups_service]['debug'] .= htmlentities($request) . ' <br /><br /> ' . htmlentities($resp->data);
  659. }
  660. $response = new SimpleXMLElement($resp->data);
  661. if (isset($response->Response->Error)) {
  662. foreach ($response->Response->Error as $error) {
  663. if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
  664. $debug_data[$ups_service]['error'][] = (string) $error->ErrorSeverity . ' ' . (string) $error->ErrorCode . ': ' . (string) $error->ErrorDescription;
  665. }
  666. if (strpos((string) $error->ErrorSeverity, 'Hard') !== FALSE) {
  667. // All or nothing quote. If some products can't be shipped by
  668. // a certain service, no quote is given for that service. If
  669. // that means no quotes are given at all, they'd better call in.
  670. unset($quotes[$ups_service]['rate']);
  671. }
  672. }
  673. }
  674. // If NegotiatedRates exist, quote based on those, otherwise, use
  675. // TotalCharges.
  676. if (isset($response->RatedShipment)) {
  677. $charge = $response->RatedShipment->TotalCharges;
  678. if (isset($response->RatedShipment->NegotiatedRates)) {
  679. $charge = $response->RatedShipment->NegotiatedRates->NetSummaryCharges->GrandTotal;
  680. }
  681. if (!isset($charge->CurrencyCode) || (string) $charge->CurrencyCode == variable_get('uc_currency_code', "USD")) {
  682. // Markup rate before customer sees it.
  683. if (!isset($quotes[$ups_service]['rate'])) {
  684. $quotes[$ups_service]['rate'] = 0;
  685. }
  686. $rate = uc_ups_rate_markup((string) $charge->MonetaryValue);
  687. $quotes[$ups_service]['rate'] += $rate;
  688. }
  689. }
  690. }
  691. }
  692. // Sort quotes by price, lowest to highest.
  693. uasort($quotes, 'uc_quote_price_sort');
  694. foreach ($quotes as $key => $quote) {
  695. if (isset($quote['rate'])) {
  696. $quotes[$key]['rate'] = $quote['rate'];
  697. $quotes[$key]['label'] = $method['quote']['accessorials'][$key];
  698. $quotes[$key]['option_label'] = theme('uc_ups_option_label', array('service' => $method['quote']['accessorials'][$key], 'packages' => $packages));
  699. }
  700. }
  701. // Merge debug data into $quotes. This is necessary because
  702. // $debug_data is not sortable by a 'rate' key, so it has to be
  703. // kept separate from the $quotes data until this point.
  704. if (isset($debug_data)) {
  705. foreach ($debug_data as $key => $data) {
  706. if (isset($quotes[$key])) {
  707. // This is debug data for successful quotes.
  708. $quotes[$key]['debug'] = $debug_data[$key]['debug'];
  709. if (isset($debug_data[$key]['error'])) {
  710. $quotes[$key]['error'] = $debug_data[$key]['error'];
  711. }
  712. }
  713. else {
  714. // This is debug data for quotes that returned error responses from UPS.
  715. $quotes[$key] = $debug_data[$key];
  716. }
  717. }
  718. }
  719. return $quotes;
  720. }
  721. /**
  722. * Constructs a void shipment request.
  723. *
  724. * @param $shipment_number
  725. * The UPS shipment tracking number.
  726. * @param $tracking_numbers
  727. * Array of tracking numbers for individual packages in the shipment.
  728. * Optional for shipments of only one package, as they have the same tracking
  729. * number.
  730. *
  731. * @return
  732. * XML VoidShipmentRequest message.
  733. */
  734. function uc_ups_void_shipment_request($shipment_number, $tracking_numbers = array()) {
  735. $schema = uc_ups_access_request();
  736. $schema .= '<?xml version="1.0"?>';
  737. $schema .= '<VoidShipmentRequest>';
  738. $schema .= '<Request>';
  739. $schema .= '<RequestAction>Void</RequestAction>';
  740. $schema .= '<TransactionReference>';
  741. $schema .= '<CustomerContext>';
  742. $schema .= t('Void shipment @ship_number and tracking numbers @track_list', array('@ship_number' => $shipment_number, '@track_list' => implode(', ', $tracking_numbers)));
  743. $schema .= '</CustomerContext>';
  744. $schema .= '<XpciVersion>1.0</XpciVersion>';
  745. $schema .= '</TransactionReference>';
  746. $schema .= '</Request>';
  747. $schema .= '<ExpandedVoidShipment>';
  748. $schema .= '<ShipmentIdentificationNumber>' . $shipment_number . '</ShipmentIdentificationNumber>';
  749. foreach ($tracking_numbers as $number) {
  750. $schema .= '<TrackingNumber>' . $number . '</TrackingNumber>';
  751. }
  752. $schema .= '</ExpandedVoidShipment>';
  753. $schema .= '</VoidShipmentRequest>';
  754. return $schema;
  755. }
  756. /**
  757. * Instructs UPS to cancel (in whole or in part) a shipment.
  758. *
  759. * @param $shipment_number
  760. * The UPS shipment tracking number.
  761. * @param $tracking_numbers
  762. * Array of tracking numbers for individual packages in the shipment.
  763. * Optional for shipments of only one package, as they have the same tracking
  764. * number.
  765. *
  766. * @return
  767. * TRUE if the shipment or packages were successfully voided.
  768. */
  769. function uc_ups_void_shipment($shipment_number, $tracking_numbers = array()) {
  770. $success = FALSE;
  771. $request = uc_ups_void_shipment_request($shipment_number, $tracking_numbers);
  772. $resp = drupal_http_request(variable_get('uc_ups_connection_address', 'https://wwwcie.ups.com/ups.app/xml/') . 'Void', array(
  773. 'method' => 'POST',
  774. 'data' => $request,
  775. ));
  776. $response = new SimpleXMLElement($resp->data);
  777. if (isset($response->Response)) {
  778. if (isset($response->Response->ResponseStatusCode)) {
  779. $success = (string) $response->Response->ResponseStatusCode;
  780. }
  781. if (isset($response->Response->Error)) {
  782. foreach ($response->Response->Error as $error) {
  783. drupal_set_message((string) $error->ErrorSeverity . ' ' . (string) $error->ErrorCode . ': ' . (string) $error->ErrorDescription, 'error');
  784. }
  785. }
  786. }
  787. if (isset($response->Status)) {
  788. if (isset($response->Status->StatusType)) {
  789. $success = (string) $response->Status->StatusType->Code;
  790. }
  791. }
  792. return (bool) $success;
  793. }
  794. /**
  795. * Modifies the rate received from UPS before displaying to the customer.
  796. *
  797. * @param $rate
  798. * Shipping rate without any rate markup.
  799. *
  800. * @return
  801. * Shipping rate after markup.
  802. */
  803. function uc_ups_rate_markup($rate) {
  804. $markup = trim(variable_get('uc_ups_rate_markup', '0'));
  805. $type = variable_get('uc_ups_rate_markup_type', 'percentage');
  806. if (is_numeric($markup)) {
  807. switch ($type) {
  808. case 'percentage':
  809. return $rate + $rate * floatval($markup) / 100;
  810. case 'multiplier':
  811. return $rate * floatval($markup);
  812. case 'currency':
  813. return $rate + floatval($markup);
  814. }
  815. }
  816. else {
  817. return $rate;
  818. }
  819. }
  820. /**
  821. * Modifies the weight of shipment before sending to UPS for a quote.
  822. *
  823. * @param $weight
  824. * Shipping weight without any weight markup.
  825. *
  826. * @return
  827. * Shipping weight after markup.
  828. */
  829. function uc_ups_weight_markup($weight) {
  830. $markup = trim(variable_get('uc_ups_weight_markup', '0'));
  831. $type = variable_get('uc_ups_weight_markup_type', 'percentage');
  832. if (is_numeric($markup)) {
  833. switch ($type) {
  834. case 'percentage':
  835. return $weight + $weight * floatval($markup) / 100;
  836. case 'multiplier':
  837. return $weight * floatval($markup);
  838. case 'mass':
  839. return $weight + floatval($markup);
  840. }
  841. }
  842. else {
  843. return $weight;
  844. }
  845. }
  846. /**
  847. * Convenience function to get UPS codes for their services.
  848. */
  849. function _uc_ups_service_list() {
  850. return array(
  851. // Domestic services.
  852. '03' => t('UPS Ground'),
  853. '01' => t('UPS Next Day Air'),
  854. '13' => t('UPS Next Day Air Saver'),
  855. '14' => t('UPS Next Day Early A.M.'),
  856. '02' => t('UPS 2nd Day Air'),
  857. '59' => t('UPS 2nd Day Air A.M.'),
  858. '12' => t('UPS 3 Day Select'),
  859. // International services.
  860. '11' => t('UPS Standard'),
  861. '07' => t('UPS Worldwide Express'),
  862. '08' => t('UPS Worldwide Expedited'),
  863. '54' => t('UPS Worldwide Express Plus'),
  864. '65' => t('UPS Worldwide Saver'),
  865. // Poland to Poland shipments only.
  866. //'82' => t('UPS Today Standard'),
  867. //'83' => t('UPS Today Dedicated Courrier'),
  868. //'84' => t('UPS Today Intercity'),
  869. //'85' => t('UPS Today Express'),
  870. //'86' => t('UPS Today Express Saver'),
  871. );
  872. }
  873. /**
  874. * Convenience function to get UPS codes for their package types.
  875. */
  876. function _uc_ups_pkg_types() {
  877. return array(
  878. // Customer Supplied Page is first so it will be the default.
  879. '02' => t('Customer Supplied Package'),
  880. '01' => t('UPS Letter'),
  881. '03' => t('Tube'),
  882. '04' => t('PAK'),
  883. '21' => t('UPS Express Box'),
  884. '24' => t('UPS 25KG Box'),
  885. '25' => t('UPS 10KG Box'),
  886. '30' => t('Pallet'),
  887. '2a' => t('Small Express Box'),
  888. '2b' => t('Medium Express Box'),
  889. '2c' => t('Large Express Box'),
  890. );
  891. }
  892. /**
  893. * Pseudo-constructor to set default values of a package.
  894. */
  895. function _uc_ups_new_package() {
  896. $package = new stdClass();
  897. $package->weight = 0;
  898. $package->price = 0;
  899. $package->length = 0;
  900. $package->width = 0;
  901. $package->height = 0;
  902. $package->length_units = 'in';
  903. $package->weight_units = 'lb';
  904. $package->qty = 1;
  905. $package->pkg_type = '02';
  906. return $package;
  907. }