Guest Posted June 19, 2003 Posted June 19, 2003 Well, after realizing that the CGI/HTML version of qcostcgi.cgi is breaking down rapidly, I decided to whip up some quick XML Rate Quote service for the community to use. The qcostcgi.cgi program at UPS is what ALL OSCOMMERCE STORES RUN ON FOR UPS, and it's a DEPRECATED PROGRAM. My stores are getting around a 75% success rate on quotes, which is ridiculous. The amount of money we lost due to this is very, very large :) I'm not a great programmer. I can hack out code very quickly though. The contribution below should be considered AN EMERGENCY REPLACEMENT ONLY. My hope is that Fritz (the author of UPS Choice) can take a look at this code and whip it into production quality code. Either way I consider this a MAJOR ISSUE with OSCommerce. UPS can turn of the qcostcgi.cgi tomorrow (as they've indicated they will soon) and all OSCommerce stores relying on this antiquated rate method will be SCREWED. All that being said, the below code is provided AS IS. It works on my Oct 02 2.2-CVS, and is based on UPS 1.47. It does NOT have the nice UPS Choice features. Hopefully someone will resolve that when they repackage this in a new module. I have no desire to maintain, improve, or issue new versions of this source code. I've written major modules for OSCommerce in the past, and I simply do not have the time to make this module what you all want it to be. Someone else will have to pickup the ball on this, but the good news is that I've already done most of the architecture, now it just needs to be brought up for production use on servers with more error testing, and better UPS Choice style administration function. Lastly, you'll need CURL support with your PHP installation for the XML parsing to work correctly. All that being said, here is the code... <?php /* $Id: ups.php,v 1.47 2002/12/09 19:07:17 dgw_ Exp $ osCommerce, Open Source E-Commerce Solutions http://www.oscommerce.com Copyright © 2002 osCommerce Released under the GNU General Public License MODIFIED: Andrew Edmond <[email protected]> June 18th, 2003 CREDITS: Modified from UPSTrack Class by Jon Whitcraft <[email protected]> Updated by Chris Lee (http://www.neox.net) This module is based on ups.php v1.47, NOT UPS Choice. The author of UPS Choice for OSCommerce should be contacted to migrate changes to XML rate information into that program. THERE IS NO SUPPORT PROVIDED FOR THIS UPGRADE / MODIFICATION AND IS PROVIDED "AS IS!". UPS Choice is currently only running CGI/HTML quotes and is a service UPS might stop ANYTIME. Write [email protected] to ask UPS Choice author to upgrade to XML rate quoting! NOTE: YOU HAVE TO EDIT the class "upsRate" below with your UPS Login/Password and Access Code. Get your access code at: http://www.ec.ups.com/ */ class ups { var $code, $title, $descrption, $icon, $enabled, $types; function ups() { $this->code = 'ups'; $this->title = MODULE_SHIPPING_UPS_TEXT_TITLE; $this->description = MODULE_SHIPPING_UPS_TEXT_DESCRIPTION; $this->icon = DIR_WS_ICONS . 'shipping_ups.gif'; $this->enabled = MODULE_SHIPPING_UPS_STATUS; /* * You can remove or comment out the type of packages that you do not wish to ship */ $this->types = array ( '01' => 'UPS Next Day Air', '02' => 'UPS 2nd Day Air', // '03' => 'UPS Ground', // '07' => 'UPS Worldwide Express', '08' => 'UPS Worldwide Expedited', // '11' => 'UPS Standard', // '12' => 'UPS 3 Day Select', // '13' => 'UPS Next Day Air Saver', // '14' => 'UPS Next Day Air Early A.M.', // '54' => 'UPS Worldwide Express Plus', '59' => 'UPS 2nd Day Air A.M.', // '64' => '', // '65' => 'UPS Express Saver', ); } function quote($method = '') { global $HTTP_POST_VARS, $order, $shipping_weight, $shipping_num_boxes, $cart; if ( (tep_not_null($method)) && (isset($this->types[$method])) ) { $prod = $method; } else { $prod = 'GND'; } if ($method) $this->_upsAction('3'); // return a single quote $this->_upsProduct($prod); $country_name = tep_get_countries(STORE_COUNTRY, true); $this->_upsOrigin(STORE_ORIGIN_ZIP, $country_name['countries_iso_code_2']); $this->_upsDest($order->delivery['postcode'], $order->delivery['country']['iso_code_2']); $this->_upsRate(MODULE_SHIPPING_UPS_PICKUP); $this->_upsContainer(MODULE_SHIPPING_UPS_PACKAGE); $this->_upsWeight($shipping_weight); $this->_upsRescom(MODULE_SHIPPING_UPS_RES); $upsQuote = $this->_upsGetQuote(); if (count($upsQuote)) { $this->quotes = array('id' => $this->code, 'module' => $this->title . ' (' . $shipping_num_boxes . ' x ' . $shipping_weight . 'lbs)'); $methods = array(); if (($order->info['total'] > 50) && (($method == '')||($method == 'QFREEGND'))) { $methods[] = array('id' => "QFREEGND", 'title' => "Free UPS Shipping", 'cost' => '0.00'); } $qsize = count($upsQuote); for ($i=0; $i<$qsize; $i++) { $type = $upsQuote[$i]['service']; $cost= number_format($upsQuote[$i]['basic'],2); if ($this->types[$type] == "") continue; $methods[] = array('id' => $type, 'title' => $this->types[$type], 'cost' => (SHIPPING_HANDLING + $cost) * $shipping_num_boxes); } $this->quotes['methods'] = $methods; } else { $this->quotes = array('module' => $this->title, 'error' => 'An error occured with the UPS shipping calculations. ' . '<br>This is a problem with the UPS server. Reload your browser ' . 'to try the UPS server again. If not, please call us ' . 'and we can manually process your order!'); } if (tep_not_null($this->icon)) $this->quotes['icon'] = tep_image($this->icon, $this->title); return $this->quotes; } function check() { if (!isset($this->_check)) { $check_query = tep_db_query("select configuration_value from " . TABLE_CONFIGURATION . " where configuration_key = 'MODULE_SHIPPING_UPS_STATUS'"); $this->_check = tep_db_num_rows($check_query); } return $this->_check; } function install() { tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('Enable UPS Shipping', 'MODULE_SHIPPING_UPS_STATUS', '1', 'Do you want to offer UPS shipping?', '6', '9', now())"); tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('UPS Pickup Method', 'MODULE_SHIPPING_UPS_PICKUP', 'CC', 'How do you give packages to UPS? CC - Customer Counter, RDP - Daily Pickup, OTP - One Time Pickup, LC - Letter Center, OCA - On Call Air', '6', '10', now())"); tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('UPS Packaging?', 'MODULE_SHIPPING_UPS_PACKAGE', 'CP', 'CP - Your Packaging, ULE - UPS Letter, UT - UPS Tube, UBE - UPS Express Box', '6', '11', now())"); tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('Residential Delivery?', 'MODULE_SHIPPING_UPS_RES', 'RES', 'Quote for Residential (RES) or Commercial Delivery (COM)', '6', '12', now())"); } function remove() { tep_db_query("delete from " . TABLE_CONFIGURATION . " where configuration_key in ('" . implode("', '", $this->keys()) . "')"); } function keys() { return array('MODULE_SHIPPING_UPS_STATUS', 'MODULE_SHIPPING_UPS_PICKUP', 'MODULE_SHIPPING_UPS_PACKAGE', 'MODULE_SHIPPING_UPS_RES'); } function _upsProduct($prod){ $this->_upsProductCode = $prod; } function _upsOrigin($postal, $country){ $this->_upsOriginPostalCode = $postal; $this->_upsOriginCountryCode = $country; } function _upsDest($postal, $country){ $postal = str_replace(' ', '', $postal); if ($country == 'US') { $this->_upsDestPostalCode = substr($postal, 0, 5); } else { $this->_upsDestPostalCode = $postal; } $this->_upsDestCountryCode = $country; } function _upsRate($foo) { switch ($foo) { case 'RDP': $this->_upsRateCode = 'Regular+Daily+Pickup'; break; case 'OCA': $this->_upsRateCode = 'On+Call+Air'; break; case 'OTP': $this->_upsRateCode = 'One+Time+Pickup'; break; case 'LC': $this->_upsRateCode = 'Letter+Center'; break; case 'CC': $this->_upsRateCode = 'Customer+Counter'; break; } } function _upsContainer($foo) { switch ($foo) { case 'CP': // Customer Packaging $this->_upsContainerCode = '02'; break; case 'ULE': // UPS Letter Envelope $this->_upsContainerCode = '01'; break; case 'UT': // UPS Tube $this->_upsContainerCode = '03'; break; case 'UEB': // UPS Express Box $this->_upsContainerCode = '21'; break; case 'UW25': // UPS Worldwide 25 kilo $this->_upsContainerCode = '24'; break; case 'UW10': // UPS Worldwide 10 kilo $this->_upsContainerCode = '25'; break; } } function _upsWeight($foo) { $this->_upsPackageWeight = $foo; } function _upsRescom($foo) { switch ($foo) { case 'RES': // Residential Address $this->_upsResComCode = '1'; break; case 'COM': // Commercial Address $this->_upsResComCode = '2'; break; } } function _upsAction($action) { /* 3 - Single Quote 4 - All Available Quotes */ $this->_upsActionCode = $action; } function _upsGetQuote() { if (!isset($this->_upsActionCode)) $this->_upsActionCode = '4'; $upsXMLRate = new upsRate($this->_upsOriginPostalCode, '', $this->_upsOriginCountryCode); $upsXMLRateQuote = $upsXMLRate->rate('', $this->_upsDestPostalCode, '',$this->_upsDestCountryCode, $this->_upsPackageWeight, 1, $this->_upsContainerCode); return $upsXMLRateQuote; } } class upsRate { // You need to create userid ... at http://www.ec.ups.com var $userid = "your user id"; var $passwd = "your password"; var $accesskey = "your access code"; var $upstool='https://www.ups.com/ups.app/xml/Rate'; var $request; //'rate' for single service or 'shop' for all possible services var $service; var $pickuptype='01'; // 01 daily pickup /* Pickup Type 01- Daily Pickup 03- Customer Counter 06- One Time Pickup 07- On Call Air 11- Suggested Retail Rates 19- Letter Center 20- Air Service Center */ var $residential; //ship from location or shipper var $s_zip; var $s_state; var $s_country; //ship to location var $t_zip; var $t_state; var $t_country; //package info var $package_type; // 02 customer supplied package var $weight; var $error=0; var $errormsg; var $xmlarray = array(); var $xmlreturndata = ""; function upsRate($szip,$sstate,$scountry) { // init function $this->s_zip = $szip; $this->s_state = $sstate; $this->s_country = $scountry; } function construct_request_xml(){ $xml="<?xml version="1.0"?> <AccessRequest xml:lang="en-US"> <AccessLicenseNumber>$this->accesskey</AccessLicenseNumber> <UserId>$this->userid</UserId> <Password>$this->passwd</Password> </AccessRequest> <?xml version="1.0"?> <RatingServiceSelectionRequest xml:lang="en-US"> <Request> <TransactionReference> <CustomerContext>Rating and Service</CustomerContext> <XpciVersion>1.0001</XpciVersion> </TransactionReference> <RequestAction>Rate</RequestAction> <RequestOption>$this->request</RequestOption> </Request> <PickupType> <Code>$this->pickuptype</Code> </PickupType> <Shipment> <Shipper> <Address> <StateProvinceCode>$this->s_state</StateProvinceCode> <PostalCode>$this->s_zip</PostalCode> <CountryCode>$this->s_country</CountryCode> </Address> </Shipper> <ShipTo> <Address> <StateProvinceCode>$this->t_state</StateProvinceCode> <PostalCode>$this->t_zip</PostalCode> <CountryCode>$this->t_country</CountryCode> <ResidentialAddressIndicator>$this->residential</ResidentialAddressIndicator> </Address> </ShipTo> <Service> <Code>$this->service</Code> </Service> <Package> <PackagingType> <Code>$this->package_type</Code> <Description>Package</Description> </PackagingType> <Description>Rate Shopping</Description> <PackageWeight> <Weight>$this->weight</Weight> </PackageWeight> </Package> <ShipmentServiceOptions/> </Shipment> </RatingServiceSelectionRequest>"; return $xml; } function rate($service='', $tzip, $tstate='', $tcountry='US', $weight, $residential=0, $packagetype='02') { if($service=='') $this->request = 'shop'; else $this->request = 'rate'; $this->service = $service; $this->t_zip = $tzip; $this->t_state= $tstate; $this->t_country = $tcountry; $this->weight = $weight; $this->residential=$residential; $this->package_type=$packagetype; $this->__runCurl(); $this->xmlarray = $this->_get_xml_array($this->xmlreturndata); //check if error occurred if($this->xmlarray=="") { $this->error=0; $this->errormsg="Unable to retrieve the Shipping info"; return NULL; } $values = $this->xmlarray[RatingServiceSelectionResponse][Response][0]; if($values[ResponseStatusCode] == 0) { $this->error=$values[Error][0][ErrorCode]; $this->errormsg=$values[Error][0][ErrorDescription]; return NULL; } return $this->get_rates(); } function __runCurl() { $y = $this->construct_request_xml(); $ch = curl_init(); curl_setopt ($ch, CURLOPT_URL,"$this->upstool"); /// set the post-to url (do not includethe ?query+string here!) curl_setopt ($ch, CURLOPT_HEADER, 0); /// Header control curl_setopt ($ch, CURLOPT_POST, 1); /// tell it to make a POST, not a GET curl_setopt ($ch, CURLOPT_POSTFIELDS, "$y"); /// put the querystring here startingwith "?" curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); /// This allows the output to be setinto a variable $xyz curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); /// some php version cause error withcurl without this line $this->xmlreturndata = curl_exec ($ch); /// execute the curl session and return theoutput to a variable $xyz curl_close ($ch); /// close the curl session } function __get_xml_array($values, &$i) { $child = array(); if ($values[$i]['value']) array_push($child, $values[$i]['value']); while (++$i < count($values)) { switch ($values[$i]['type']) { case 'cdata': array_push($child, $values[$i]['value']); break; case 'complete': $name = $values[$i]['tag']; $child[$name]= $values[$i]['value']; if($values[$i]['attributes']) { $child[$name] = $values[$i]['attributes']; } break; case 'open': $name = $values[$i]['tag']; $size = sizeof($child[$name]); if($values[$i]['attributes']) { $child[$name][$size] = $values[$i]['attributes']; $child[$name][$size] = $this->__get_xml_array($values, $i); } else { $child[$name][$size] = $this->__get_xml_array($values, $i); } break; case 'close': return $child; break; } } return $child; } function _get_xml_array($data) { $values = array(); $index = array(); $array = array(); $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parse_into_struct($parser, $data, $values, $index); xml_parser_free($parser); $i = 0; $name = $values[$i]['tag']; $array[$name] = $values[$i]['attributes']; $array[$name] = $this->__get_xml_array($values, $i); return $array; } function get_rates() { // $retArray = array('service'=>'','basic'=>0,'option'=>0,'total'=>0,'days'=>'','time'=>''); $retArray=array(); $values = $this->xmlarray[RatingServiceSelectionResponse][RatedShipment]; $ct = count($values); for($i=0;$i<$ct;$i++) { $current=&$values[$i]; $retArray[$i]['service'] = $current[service][0][code]; $retArray[$i]['basic'] = $current[TransportationCharges][0][MonetaryValue]; $retArray[$i]['option'] = $current[serviceOptionsCharges][0][MonetaryValue]; $retArray[$i]['total'] = $current[TotalCharges][0][MonetaryValue]; $retArray[$i]['days'] = $current[GuaranteedDaysToDelivery]; $retArray[$i]['time'] = $current[scheduledDeliveryTime]; } unset($values); return $retArray; } } ?> Quote
Irata0062 Posted June 19, 2003 Posted June 19, 2003 Great... except, what is CURL exactly? And how do I get it installed? Quote
moyashi Posted June 20, 2003 Posted June 20, 2003 CURL is, if I'm correct, a system based program. You need to add it to the server. Another popular program that's pretty similar is wget. Both these programs are command-line unix/bsd ftp like programs. Quote NewsDesk(934) / FAQDesk(1106) / OrderCheck(1168) :::
lorax Posted June 20, 2003 Posted June 20, 2003 Thanks for the code - I'll dig through this in a few days! Quote Apathy is a dominant gene - mutate.
cgchris99 Posted June 23, 2003 Posted June 23, 2003 Is anyone working on combining the UPS choice module with this one? Thanks for any info Quote
Daibheid Posted June 23, 2003 Posted June 23, 2003 I haven't implimented this yet and will most likely do so this evening or tomorrow. Anyone using this and found issues? D. Quote
dreamscape Posted June 24, 2003 Posted June 24, 2003 Can't be any worse than the current UPS module... I was in the process of downloading the UPS Shipping XML Dev kit when I saw this post... Many thanks, it gives me a nice place to start :D Quote The only thing necessary for evil to flourish is for good men to do nothing - Edmund Burke
Daibheid Posted June 24, 2003 Posted June 24, 2003 Getting the following error: Fatal error: Call to undefined function: curl_init() in /home/eotrorg/public_html/shop/includes/modules/shipping/ups.php on line 386 D. Quote
lorax Posted June 25, 2003 Posted June 25, 2003 Daibheid, Your host may not support the cURL functions. Check with them to be sure. Quote Apathy is a dominant gene - mutate.
Daibheid Posted June 25, 2003 Posted June 25, 2003 Lorax, Thanks for the reply. They do support it. I asked that first :) Is there something specific that I need to ask them to do? D. Quote
lorax Posted June 25, 2003 Posted June 25, 2003 There are two versions of cURL support - command line and script (libcurl). They need to allow for script support. It's a pretty straight forward error - it's basically telling you that the webserver doesn't understand the curl_init() function which tells me the host doesn't support cURL. Quote Apathy is a dominant gene - mutate.
Millie Posted June 25, 2003 Posted June 25, 2003 Nothing happened when I installed the xml ups module. The UPS heading was listed but no rate quotes or errors. Is there something I can set to help identify if I communicated with UPS OK and view the response? Any other way to debug? I'm really excited to have this xml module to work with but I'm not sure what to do next. Millie Quote
Daibheid Posted June 26, 2003 Posted June 26, 2003 Lorax, My ISP recompiled their PHP and now I am no longer getting the error. I'm getting a whole new error :P I get quotes from USPS, but UPS says An error occured with the UPS shipping calculations. This is a problem with the UPS server. Reload your browser to try the UPS server again. If not, please call us and we can manually process your order! I do have an ID/PWD and those are configured. D. Quote
lorax Posted June 27, 2003 Posted June 27, 2003 It may be a timeout issue. Try building a loop around the code that queries the UPS server (loop for some number like 6-10). Use a false return to loop again and a true to break out of the loop. Quote Apathy is a dominant gene - mutate.
torinwalker Posted June 27, 2003 Posted June 27, 2003 I've tried to make a complete, language-supporting, worldwide origin supporting version of the UPS XML Rates and Services (quote) shipping module. Comments and bug reports are very much welcome and encouraged. Torin... Quote
cgchris99 Posted June 27, 2003 Posted June 27, 2003 Torin, "worldwide origin supporting version" Is this contribution available for download? If yes, please provide the link Thanks Quote
torinwalker Posted June 27, 2003 Posted June 27, 2003 Check out the contributions section for "UPS XML Rates and Services v1.0". I only posted it five minutes ago. I'm not sure if the moderators have to rubber stamp it before it becomes available. Please, please tell me if you encounter any errors, omissions, or if it isn't completely wonderful. I want to update it before it becomes really popular. Torin... Quote
Millie Posted June 28, 2003 Posted June 28, 2003 First of all, Thank you, Torin! I really appreciate the work you have done to get this working. I was able to install in a few minutes and obtain rates! The rate returned was several dollars too high and I could see that I needed to change the weight to LBS. I found 2 places in /includes/modules/shipping/upsxml.php that needed to be changed but the shipping quote did not go down to the expected amount (did not change at all). I wanted to suggest documenting the specific changes. I think it will help all who need to change from KG to LBS. Thanks! Millie Quote
torinwalker Posted June 29, 2003 Posted June 29, 2003 You're right, I had a bit of difficulty with the LB vs KG when using a Canadian Origin. It didn't accept KG, and figured it had something to do with the Origin. I'll work on it a bit more and publish a patch for both the code, if necessary, and the documentation. Give me a day or so. It's the weekend, and I'm installing new countertops in the kitchen. Torin... Quote
Millie Posted June 29, 2003 Posted June 29, 2003 Hi Torin, I've been playing with the settings and can not get the rates to return close to actual costs. Looking at the log, I think the weight is being processed differently than in the old ups.php (where is my packaging weight that should be added on?) and a handling fee does not add on. I process weights differently than most, I use $total_weight*.0625 in shipping.php to convert ounces to pounds. In upsxml.php, the shipping weight is shown accurately in pounds when viewing the rate chart from the web but in the upsxml log it shows ounces without the packaging weight included. When I added code to upsxml.php to convert ounces to pounds the log showed the correct weight (weight of packaging still missing) but the rates remained incorrect. The amount of error in the rate calculation is several dollars more than what a proper weight would correct so I think something else is going on as well... I'm not skilled at php or xml so my observations may not be professional but I hope sharing them will help identify bugs in the module. Millie Quote
torinwalker Posted July 2, 2003 Posted July 2, 2003 Guys and Gals, Upon closer inspection, I realized I copied some minor bugs from another module. Naturally, there were a few of my own. Incorrect rates and inflexible units of measurements are two of the symptoms. I will publish a corrected release very shortly. I have been trying very hard to produce a packaging feature whereby one can configure a number of boxes (length, width, height, max weight) for use by the shipping module. The shipping module will use a simple algorithm to pack products into as few boxes as possible, and return rates on the boxes, rather than the number of products. For example, if one sells CDs which have a nominal dimension of 5x5x0.5 inches, one would configure a few boxes: A single CD mailer of 5x5x0.5 inches, a five-CD mailer of 5x5x2.5 inches, and a ten-CD mailer with dimensions of 5x5x5 inches. The algoritm will attempt to ship the CDs in as few packages as possible and quote on the rates for those packages. The hope is that this algoritm will be smarter than just quoting on the total number of products and save you, the storeowner, some money. This featureset requires a few easy modifications and has no impact on other services. The only work I have left is to make dimension conversion work (we Canadians face the dillemma of working with and developing for both Metric and Imperial Systems) and clean up the pop-up text on buttons. It would be extremely helpful if you could tell me whether a feature like this is desirable. It will change the way I release this in the future. Right now, I'm practically splitting the module in half. One set of instructions for those who want to simply plug in the module, and another set for those who opt for the dimension support. If I learn that most people will want the dimension support, I will remove the dividers. Torin... Quote
cgchris99 Posted July 2, 2003 Posted July 2, 2003 Just my opinion. I think you should do it both ways. Dimension and no dimension. Some of my products are very big and require oversized boxes so the dimension would not help me at all. Now if you could have this option product specific that would be a plus. One product uses the dimension options and other products do not. Quote
delishus Posted July 4, 2003 Posted July 4, 2003 Does anyone have a script for dumping orders to a CSV file for Worldship to read in? Thanks Del Quote
moyashi Posted July 4, 2003 Posted July 4, 2003 What's the reliability of this contribution? Any comments would be appreciated! Quote NewsDesk(934) / FAQDesk(1106) / OrderCheck(1168) :::
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.