svn commit: r800198 - in /ofbiz/trunk/applications/product: config/shipment.properties src/org/ofbiz/product/product/ProductWorker.java src/org/ofbiz/shipment/shipment/ShipmentWorker.java src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

svn commit: r800198 - in /ofbiz/trunk/applications/product: config/shipment.properties src/org/ofbiz/product/product/ProductWorker.java src/org/ofbiz/shipment/shipment/ShipmentWorker.java src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java

lektran
Author: lektran
Date: Mon Aug  3 04:57:57 2009
New Revision: 800198

URL: http://svn.apache.org/viewvc?rev=800198&view=rev
Log:
Some fixes to the usps priority mail international rate estimate service
First pass implementation of a usps priority mail international shipping label service

Added:
    ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java   (with props)
Modified:
    ofbiz/trunk/applications/product/config/shipment.properties
    ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductWorker.java
    ofbiz/trunk/applications/product/src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java

Modified: ofbiz/trunk/applications/product/config/shipment.properties
URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/config/shipment.properties?rev=800198&r1=800197&r2=800198&view=diff
==============================================================================
--- ofbiz/trunk/applications/product/config/shipment.properties (original)
+++ ofbiz/trunk/applications/product/config/shipment.properties Mon Aug  3 04:57:57 2009
@@ -130,8 +130,16 @@
 # USPS Webtools API Configuration
 ############################################
 
+# USPS configuration indicator
+shipment.usps.shipping=Y
+# Testing indicator, currently only used for printing labels since USPS doesn't
+# use a separate url for testing
+shipment.usps.test=Y
+
 # USPS Connection URL & timeout in seconds
 shipment.usps.connect.url=http://localhost:8080/facility/ShippingAPI.dll
+# Url for labels differs in that the url is the same whether testing or not
+shipment.usps.connect.url.labels=http://localhost:8080/facility/ShippingAPI.dll
 shipment.usps.connect.timeout=60
 
 # USPS Credentials

Modified: ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductWorker.java
URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductWorker.java?rev=800198&r1=800197&r2=800198&view=diff
==============================================================================
--- ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductWorker.java (original)
+++ ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductWorker.java Mon Aug  3 04:57:57 2009
@@ -31,6 +31,7 @@
 import javax.servlet.jsp.PageContext;
 
 import javolution.util.FastList;
+import javolution.util.FastMap;
 import javolution.util.FastSet;
 
 import org.ofbiz.base.util.Debug;
@@ -834,6 +835,49 @@
         return null;
     }
 
+    /*
+     * Returns the product's unit weight converted to the desired Uom.  If the weight is null,
+     * then a check is made for an associated virtual product to retrieve the weight from.  If the
+     * weight is still null then null is returned.  If a weight is found and a desiredUomId has
+     * been supplied and the product specifies a weightUomId then an attempt will be made to
+     * convert the value otherwise the weight is returned as is.
+     */
+    public static BigDecimal getProductWeight(GenericValue product, String desiredUomId, GenericDelegator delegator, LocalDispatcher dispatcher) {
+        BigDecimal weight = product.getBigDecimal("weight");
+        String weightUomId = product.getString("weightUomId");
+
+        if (weight == null) {
+            GenericValue parentProduct = getParentProduct(product.getString("productId"), delegator);
+            if (parentProduct != null) {
+                weight = parentProduct.getBigDecimal("weight");
+                weightUomId = parentProduct.getString("weightUomId");
+            }
+        }
+
+        if (weight == null) {
+            return null;
+        } else {
+            // attempt a conversion if necessary
+            if (desiredUomId != null && product.get("weightUomId") != null && !desiredUomId.equals(product.get("weightUomId"))) {
+                Map<String, Object> result = FastMap.newInstance();
+                try {
+                    result = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", weightUomId, "uomIdTo", "WT_lb", "originalValue", weight));
+                } catch (GenericServiceException e) {
+                    Debug.logError(e, module);
+                }
+
+                if (result.get(ModelService.RESPONSE_MESSAGE).equals(ModelService.RESPOND_SUCCESS) && result.get("convertedValue") != null) {
+                    weight = (BigDecimal) result.get("convertedValue");
+                } else {
+                    Debug.logError("Unsupported conversion from [" + weightUomId + "] to [" + desiredUomId + "]",module);
+                    return null;
+                }
+            }
+            return weight;
+        }
+    }
+
+
 
     /**
      * Generic service to find product by id.

Added: ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java
URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java?rev=800198&view=auto
==============================================================================
--- ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java (added)
+++ ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java Mon Aug  3 04:57:57 2009
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.ofbiz.shipment.shipment;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javolution.util.FastMap;
+
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.UtilHttp;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.entity.GenericDelegator;
+import org.ofbiz.entity.GenericEntityException;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.util.EntityUtil;
+import org.ofbiz.product.product.ProductWorker;
+import org.ofbiz.service.GenericServiceException;
+import org.ofbiz.service.LocalDispatcher;
+import org.ofbiz.service.ModelService;
+
+/**
+ * ShipmentWorker - Worker methods for Shipment and related entities
+ */
+public class ShipmentWorker {
+
+    public static final String module = ShipmentWorker.class.getName();
+
+    /*
+     * Returns the value of a given ShipmentPackageContent record.  Calculated by working out the total value (from the OrderItems) of all ItemIssuances
+     * for the ShipmentItem then dividing that by the total quantity issued for the same to get an average item value then multiplying that by the package
+     * content quantity.
+     * Note: No rounding of the calculation is performed so you will need to round it to the accuracy that you require
+     */
+    public static BigDecimal getShipmentPackageContentValue(GenericValue shipmentPackageContent) {
+        BigDecimal quantity = shipmentPackageContent.getBigDecimal("quantity");
+
+        BigDecimal value = new BigDecimal("0");
+
+        // lookup the issuance to find the order
+        List<GenericValue> issuances = null;
+        try {
+            GenericValue shipmentItem = shipmentPackageContent.getRelatedOne("ShipmentItem");
+            issuances = shipmentItem.getRelated("ItemIssuance");
+        } catch (GenericEntityException e) {
+            Debug.logError(e, module);
+        }
+
+        BigDecimal totalIssued = BigDecimal.ZERO;
+        BigDecimal totalValue = BigDecimal.ZERO;
+        if (UtilValidate.isNotEmpty(issuances)) {
+            for (GenericValue issuance : issuances) {
+                // we only need one
+                BigDecimal issuanceQuantity = issuance.getBigDecimal("quantity");
+                BigDecimal issuanceCancelQuantity = issuance.getBigDecimal("cancelQuantity");
+                if (issuanceCancelQuantity != null) {
+                    issuanceQuantity = issuanceQuantity.subtract(issuanceCancelQuantity);
+                }
+                // get the order item
+                GenericValue orderItem = null;
+                try {
+                    orderItem = issuance.getRelatedOne("OrderItem");
+                } catch (GenericEntityException e) {
+                    Debug.logError(e, module);
+                }
+
+                if (orderItem != null) {
+                    // get the value per unit - (base price * amount)
+                    BigDecimal selectedAmount = orderItem.getBigDecimal("selectedAmount");
+                    if (selectedAmount == null || selectedAmount.compareTo(BigDecimal.ZERO) <= 0) {
+                        selectedAmount = BigDecimal.ONE;
+                    }
+
+                    BigDecimal unitPrice = orderItem.getBigDecimal("unitPrice");
+                    BigDecimal itemValue = unitPrice.multiply(selectedAmount);
+
+                    // total value for package (per unit * quantity)
+                    totalIssued = totalIssued.add(issuanceQuantity);
+                    totalValue = totalValue.add(itemValue.multiply(issuanceQuantity));
+                }
+            }
+        }
+        // take the average value of the issuances and multiply it by the shipment package content quantity
+        value = totalValue.divide(totalIssued, 10, BigDecimal.ROUND_HALF_EVEN).multiply(quantity);
+        return value;
+    }
+}
+

Propchange: ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java
------------------------------------------------------------------------------
    svn:keywords = "Date Rev Author URL Id"

Propchange: ofbiz/trunk/applications/product/src/org/ofbiz/shipment/shipment/ShipmentWorker.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: ofbiz/trunk/applications/product/src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java
URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java?rev=800198&r1=800197&r2=800198&view=diff
==============================================================================
--- ofbiz/trunk/applications/product/src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java (original)
+++ ofbiz/trunk/applications/product/src/org/ofbiz/shipment/thirdparty/usps/UspsServices.java Mon Aug  3 04:57:57 2009
@@ -26,6 +26,7 @@
 import java.math.BigDecimal;
 import java.math.MathContext;
 import java.text.DecimalFormat;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
@@ -44,21 +45,26 @@
 import org.ofbiz.base.util.GeneralException;
 import org.ofbiz.base.util.HttpClient;
 import org.ofbiz.base.util.HttpClientException;
+import org.ofbiz.base.util.StringUtil;
 import org.ofbiz.base.util.UtilGenerics;
 import org.ofbiz.base.util.UtilMisc;
 import org.ofbiz.base.util.UtilProperties;
 import org.ofbiz.base.util.UtilValidate;
 import org.ofbiz.base.util.UtilXml;
+import org.ofbiz.common.uom.UomWorker;
 import org.ofbiz.entity.GenericDelegator;
 import org.ofbiz.entity.GenericEntityException;
 import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.util.EntityUtil;
 import org.ofbiz.party.contact.ContactMechWorker;
+import org.ofbiz.product.product.ProductWorker;
 import org.ofbiz.product.store.ProductStoreWorker;
 import org.ofbiz.service.DispatchContext;
 import org.ofbiz.service.GenericServiceException;
 import org.ofbiz.service.LocalDispatcher;
 import org.ofbiz.service.ModelService;
 import org.ofbiz.service.ServiceUtil;
+import org.ofbiz.shipment.shipment.ShipmentWorker;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xml.sax.SAXException;
@@ -73,6 +79,15 @@
 
     public static final MathContext generalRounding = new MathContext(10);
 
+    private static List<String> domesticCountries = FastList.newInstance();
+    // Countries treated as domestic for rate enquiries
+    static {
+        domesticCountries.add("USA");
+        domesticCountries.add("ASM");
+        domesticCountries.add("GU");
+        domesticCountries = Collections.unmodifiableList(domesticCountries);
+    }
+
     public static Map<String, Object> uspsRateInquire(DispatchContext dctx, Map<String, ? extends Object> context) {
 
         GenericDelegator delegator = dctx.getDelegator();
@@ -112,6 +127,9 @@
             try {
                 GenericValue shipToAddress = delegator.findByPrimaryKey("PostalAddress", UtilMisc.toMap("contactMechId", shippingContactMechId));
                 if (shipToAddress != null) {
+                    if (!domesticCountries.contains(shipToAddress.getString("countryGeoId"))) {
+                        return ServiceUtil.returnFailure("uspsRateInquire is only valid for US destinations.");
+                    }
                     destinationZip = shipToAddress.getString("postalCode");
                 }
             } catch (GenericEntityException e) {
@@ -231,6 +249,25 @@
         return result;
     }
 
+    /*
+     * USPS International Service Codes
+     * 1 - Express Mail International
+     * 2 - Priority Mail International
+     * 4 - Global Express Guaranteed (Document and Non-document)
+     * 5 - Global Express Guaranteed Document used
+     * 6 - Global Express Guaranteed Non-Document Rectangular shape
+     * 7 - Global Express Guaranteed Non-Document Non-Rectangular
+     * 8 - Priority Mail Flat Rate Envelope
+     * 9 - Priority Mail Flat Rate Box
+     * 10 - Express Mail International Flat Rate Envelope
+     * 11 - Priority Mail Large Flat Rate Box
+     * 12 - Global Express Guaranteed Envelope
+     * 13 - First Class Mail International Letters
+     * 14 - First Class Mail International Flats
+     * 15 - First Class Mail International Parcels
+     * 16 - Priority Mail Small Flat Rate Box
+     * 21 - PostCards
+     */
     public static Map<String, Object> uspsInternationalRateInquire(DispatchContext dctx, Map<String, ? extends Object> context) {
 
         GenericDelegator delegator = dctx.getDelegator();
@@ -247,8 +284,8 @@
         if (UtilValidate.isNotEmpty(shippingContactMechId)) {
             try {
                 GenericValue shipToAddress = delegator.findByPrimaryKey("PostalAddress", UtilMisc.toMap("contactMechId", shippingContactMechId));
-                if ("USA".equals(shipToAddress.get("countryGeoId"))) {
-                    return ServiceUtil.returnError("The USPS International Rate Calculation service is not applicable to US destinations, use uspsRateInquire");
+                if (domesticCountries.contains(shipToAddress.get("countryGeoId"))) {
+                    return ServiceUtil.returnError("The USPS International Rate Calculation service is not applicable to US destinations (including Possesions), use uspsRateInquire");
                 }
                 if (shipToAddress != null && UtilValidate.isNotEmpty(shipToAddress.getString("countryGeoId"))) {
                     GenericValue countryGeo = shipToAddress.getRelatedOne("CountryGeo");
@@ -295,36 +332,33 @@
 
         // create the request document
         Document requestDocument = createUspsRequestDocument("IntlRateRequest", false);
-        UtilXml.addChildElementValue(requestDocument.getDocumentElement(), "Country", destinationCountry, requestDocument);
 
         // TODO: Up to 25 packages can be included per request - handle more than 25
         for (ListIterator<Map<String, BigDecimal>> li = packages.listIterator(); li.hasNext();) {
             Map<String, BigDecimal> packageMap = li.next();
 
+            Element packageElement = UtilXml.addChildElement(requestDocument.getDocumentElement(), "Package", requestDocument);
+            packageElement.setAttribute("ID", String.valueOf(li.nextIndex() - 1)); // use zero-based index (see examples)
+
             BigDecimal packageWeight = isOnePackage ? shippableWeight : calcPackageWeight(dctx, packageMap, shippableItemInfo, BigDecimal.ZERO);
             if (packageWeight.compareTo(BigDecimal.ZERO) == 0) {
                 continue;
             }
-
-            Element packageElement = UtilXml.addChildElement(requestDocument.getDocumentElement(), "Package", requestDocument);
-            packageElement.setAttribute("ID", String.valueOf(li.nextIndex() - 1)); // use zero-based index (see examples)
-
-            UtilXml.addChildElementValue(packageElement, "MailType", "Package", requestDocument);
-
-            BigDecimal weightPounds = packageWeight.setScale(0, BigDecimal.ROUND_FLOOR);
+            Integer[] weightPoundsOunces = convertPoundsToPoundsOunces(packageWeight);
             // for Parcel post, the weight must be at least 1 lb
-            if ("PARCEL".equals(serviceCode.toUpperCase()) && (weightPounds.compareTo(BigDecimal.ONE) < 0)) {
-                weightPounds = BigDecimal.ONE;
-                packageWeight = BigDecimal.ZERO;
+            if ("PARCEL".equals(serviceCode.toUpperCase()) && (weightPoundsOunces[0] < 1)) {
+                weightPoundsOunces[0] = 1;
+                weightPoundsOunces[1] = 0;
             }
-            // (packageWeight % 1) * 16 (Rounded up to 1 dp)
-            BigDecimal weightOunces = packageWeight.remainder(BigDecimal.ONE).multiply(new BigDecimal("16")).setScale(1, BigDecimal.ROUND_CEILING);
-            UtilXml.addChildElementValue(packageElement, "Pounds", weightPounds.toPlainString(), requestDocument);
-            UtilXml.addChildElementValue(packageElement, "Ounces", weightOunces.toPlainString(), requestDocument);
+            UtilXml.addChildElementValue(packageElement, "Pounds", weightPoundsOunces[0].toString(), requestDocument);
+            UtilXml.addChildElementValue(packageElement, "Ounces", weightPoundsOunces[1].toString(), requestDocument);
 
             UtilXml.addChildElementValue(packageElement, "Machinable", "False", requestDocument);
+            UtilXml.addChildElementValue(packageElement, "MailType", "Package", requestDocument);
             
             // TODO: Add package value so that an insurance fee can be returned
+
+            UtilXml.addChildElementValue(packageElement, "Country", destinationCountry, requestDocument);
         }
 
         // send the request
@@ -1451,6 +1485,209 @@
         return ServiceUtil.returnSuccess();
     }
 
+    public static Map<String, Object> uspsPriorityMailInternationalLabel(DispatchContext dctx, Map<String, ? extends Object> context) {
+        GenericDelegator delegator = dctx.getDelegator();
+        LocalDispatcher dispatcher = dctx.getDispatcher();
+
+        GenericValue shipmentRouteSegment = (GenericValue) context.get("shipmentRouteSegment");
+
+        // Start the document
+        Document requestDocument;
+        boolean certify = false;
+        if (!"Y".equalsIgnoreCase(UtilProperties.getPropertyValue("shipment.properties", "shipment.usps.test"))) {
+            requestDocument = createUspsRequestDocument("PriorityMailIntlRequest", false);
+        } else {
+            requestDocument = createUspsRequestDocument("PriorityMailIntlCertifyRequest", false);
+            certify = true;
+        }
+        Element rootElement = requestDocument.getDocumentElement();
+        
+        // Retrieve from/to address and package details
+        GenericValue originAddress = null;
+        GenericValue originTelecomNumber = null;
+        GenericValue destinationAddress = null;
+        GenericValue destinationProvince = null;
+        GenericValue destinationCountry = null;
+        GenericValue destinationTelecomNumber = null;
+        List<GenericValue> shipmentPackageRouteSegs = null;
+        try {
+            originAddress = shipmentRouteSegment.getRelatedOne("OriginPostalAddress");
+            originTelecomNumber = shipmentRouteSegment.getRelatedOne("OriginTelecomNumber");
+            destinationAddress = shipmentRouteSegment.getRelatedOne("DestPostalAddress");
+            if (destinationAddress != null) {
+                destinationProvince = destinationAddress.getRelatedOne("StateProvinceGeo");
+                destinationCountry = destinationAddress.getRelatedOne("CountryGeo");
+            }
+            destinationTelecomNumber = shipmentRouteSegment.getRelatedOne("DestTelecomNumber");
+            shipmentPackageRouteSegs = shipmentRouteSegment.getRelated("ShipmentPackageRouteSeg");
+        } catch (GenericEntityException e) {
+            Debug.logError(e, module);
+        }
+        if (originAddress == null || originTelecomNumber == null) {
+            ServiceUtil.returnError("Unable to request a USPS Priority Mail International Label: ShipmentRouteSegment is missing origin phone or address details");
+        }
+
+        // Origin Info
+        // USPS wants a separate first name and last, best we can do is split the string on the white space, if that doesn't work then default to putting the attnName in both fields
+        String fromAttnName = originAddress.getString("attnName");
+        String fromFirstName = StringUtils.defaultIfEmpty(StringUtils.substringBefore(fromAttnName, " "), fromAttnName);
+        String fromLastName = StringUtils.defaultIfEmpty(StringUtils.substringAfter(fromAttnName, " "), fromAttnName);
+        UtilXml.addChildElementValue(rootElement, "FromFirstName", fromFirstName, requestDocument);
+        // UtilXml.addChildElementValue(rootElement, "FromMiddleInitial", "", requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromLastName", fromLastName, requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromFirm", originAddress.getString("toName"), requestDocument);
+        // The following 2 assignments are not typos - USPS address1 = OFBiz address2, USPS address2 = OFBiz address1
+        UtilXml.addChildElementValue(rootElement, "FromAddress1", originAddress.getString("address2"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromAddress2", originAddress.getString("address1"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromCity", originAddress.getString("city"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromState", originAddress.getString("stateProvinceGeoId"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "FromZip5", originAddress.getString("postalCode"), requestDocument);
+        // USPS expects a phone number consisting of area code + contact number as a single numeric string
+        String fromPhoneNumber = originTelecomNumber.getString("areaCode") + originTelecomNumber.getString("contactNumber");
+        fromPhoneNumber = StringUtil.removeNonNumeric(fromPhoneNumber);
+        UtilXml.addChildElementValue(rootElement, "FromPhone", fromPhoneNumber, requestDocument);
+
+        // Destination Info
+        UtilXml.addChildElementValue(rootElement, "ToName", destinationAddress.getString("attnName"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToFirm", destinationAddress.getString("toName"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToAddress1", destinationAddress.getString("address1"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToAddress2", destinationAddress.getString("address2"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToCity", destinationAddress.getString("city"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToProvince", destinationProvince.getString("geoName"), requestDocument);
+        // TODO: Test these country names, I think we're going to need to maintain a list of USPS names
+        UtilXml.addChildElementValue(rootElement, "ToCountry", destinationCountry.getString("geoName"), requestDocument);
+        UtilXml.addChildElementValue(rootElement, "ToPostalCode", destinationAddress.getString("postalCode"), requestDocument);
+        // TODO: Figure out how to answer this question accurately
+        UtilXml.addChildElementValue(rootElement, "ToPOBoxFlag", "N", requestDocument);
+        String toPhoneNumber = destinationTelecomNumber.getString("countryCode") + destinationTelecomNumber.getString("areaCode") + destinationTelecomNumber.getString("contactNumber");
+        UtilXml.addChildElementValue(rootElement, "ToPhone", toPhoneNumber, requestDocument);
+        UtilXml.addChildElementValue(rootElement, "NonDeliveryOption", "RETURN", requestDocument);
+        
+        for (GenericValue shipmentPackageRouteSeg : shipmentPackageRouteSegs) {
+            Document packageDocument = (Document) requestDocument.cloneNode(true);
+            Element packageRootElement = requestDocument.getDocumentElement();
+            // This is our reference and can be whatever we want.  For lack of a better alternative we'll use shipmentId:shipmentPackageSeqId:shipmentRouteSegmentId
+            String fromCustomsReference = shipmentRouteSegment.getString("shipmentId") + ":" + shipmentRouteSegment.getString("shipmentRouteSegmentId");
+            fromCustomsReference = StringUtils.join(
+                    UtilMisc.toList(
+                            shipmentRouteSegment.get("shipmentId"),
+                            shipmentPackageRouteSeg.get("shipmentPackageSeqId"),
+                            shipmentRouteSegment.get("shipmentRouteSegementId")
+                    ), ':');
+            UtilXml.addChildElementValue(rootElement, "FromCustomsReference", fromCustomsReference, packageDocument);
+            // Determine the container type for this package
+            String container = "VARIABLE";
+            String packageTypeCode = null;
+            GenericValue shipmentPackage = null;
+            List<GenericValue> shipmentPackageContents = null;
+            try {
+                shipmentPackage = shipmentPackageRouteSeg.getRelatedOne("ShipmentPackage");
+                shipmentPackageContents = shipmentPackage.getRelated("ShipmentPackageContent");
+                GenericValue shipmentBoxType = shipmentPackage.getRelatedOne("ShipmentBoxType");
+                if (shipmentBoxType != null) {
+                    GenericValue carrierShipmentBoxType = EntityUtil.getFirst(shipmentBoxType.getRelatedByAnd("CarrierShipmentBoxType", UtilMisc.toMap("partyId", "USPS")));
+                    if (carrierShipmentBoxType != null) {
+                        packageTypeCode = carrierShipmentBoxType.getString("packageTypeCode");
+                        // Supported type codes
+                        List<String> supportedPackageTypeCodes = UtilMisc.toList(
+                                "LGFLATRATEBOX",
+                                "SMFLATRATEBOX",
+                                "FLATRATEBOX",
+                                "MDFLATRATEBOX",
+                                "FLATRATEENV");
+                        if (supportedPackageTypeCodes.contains(packageTypeCode)) {
+                            container = packageTypeCode;
+                        }
+                    }
+                }
+            } catch (GenericEntityException e) {
+                Debug.logError(e, module);
+            }
+            UtilXml.addChildElementValue(rootElement, "Container", container, packageDocument);
+            /* TODO:
+            UtilXml.addChildElementValue(rootElement, "Insured", "", packageDocument);
+            UtilXml.addChildElementValue(rootElement, "InsuredNumber", "", packageDocument);
+            UtilXml.addChildElementValue(rootElement, "InsuredAmount", "", packageDocument);
+            */
+            // According to the docs sending an empty postage tag will cause the postage to be calculated
+            UtilXml.addChildElementValue(rootElement, "Postage", "", packageDocument);
+            
+            BigDecimal packageWeight = shipmentPackage.getBigDecimal("weight");
+            String weightUomId = shipmentPackage.getString("weightUomId");
+            BigDecimal packageWeightPounds = UomWorker.convertUom(packageWeight, weightUomId, "WT_lb", dispatcher);
+            Integer[] packagePoundsOunces = convertPoundsToPoundsOunces(packageWeightPounds);
+            UtilXml.addChildElementValue(rootElement, "GrossPounds", packagePoundsOunces[0].toString(), packageDocument);
+            UtilXml.addChildElementValue(rootElement, "GrossOunces", packagePoundsOunces[1].toString(), packageDocument);
+
+            UtilXml.addChildElementValue(rootElement, "ContentType", "MERCHANDISE", packageDocument);
+            UtilXml.addChildElementValue(rootElement, "Agreement", "N", packageDocument);
+            UtilXml.addChildElementValue(rootElement, "ImageType", "PDF", packageDocument);
+            // TODO: Try the different layouts
+            UtilXml.addChildElementValue(rootElement, "ImageType", "ALLINONEFILE", packageDocument);
+            UtilXml.addChildElementValue(rootElement, "CustomerRefNo", fromCustomsReference, packageDocument);
+            
+            // Add the shipping contents
+            Element shippingContents = UtilXml.addChildElement(rootElement, "ShippingContents", packageDocument);
+            for (GenericValue shipmentPackageContent : shipmentPackageContents) {
+                Element itemDetail = UtilXml.addChildElement(shippingContents, "ItemDetail", packageDocument);
+                GenericValue product = null;
+                GenericValue originGeo = null;
+                try {
+                    GenericValue shipmentItem = shipmentPackageContent.getRelatedOne("ShipmentItem");
+                    product = shipmentItem.getRelatedOne("Product");
+                    originGeo = product.getRelatedOne("OriginGeo");
+                } catch (GenericEntityException e) {
+                    Debug.log(e, module);
+                }
+
+                UtilXml.addChildElementValue(itemDetail, "Description", product.getString("productName"), packageDocument);
+                UtilXml.addChildElementValue(itemDetail, "Quantity", shipmentPackageContent.getBigDecimal("quantity").setScale(0, BigDecimal.ROUND_CEILING).toPlainString(), packageDocument);
+                String packageContentValue = ShipmentWorker.getShipmentPackageContentValue(shipmentPackageContent).setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString();
+                UtilXml.addChildElementValue(itemDetail, "Value", packageContentValue, packageDocument);
+                BigDecimal productWeight = ProductWorker.getProductWeight(product, "WT_lbs", delegator, dispatcher);
+                Integer[] productPoundsOunces = convertPoundsToPoundsOunces(productWeight);
+                UtilXml.addChildElementValue(itemDetail, "NetPounds", productPoundsOunces[0].toString(), packageDocument);
+                UtilXml.addChildElementValue(itemDetail, "NetOunces", productPoundsOunces[1].toString(), packageDocument);
+                UtilXml.addChildElementValue(itemDetail, "HSTariffNumber", "", packageDocument);
+                UtilXml.addChildElementValue(itemDetail, "CountryOfOrigin", originGeo.getString("geoName"), packageDocument);
+            }
+            
+            // Send the request
+            Document responseDocument = null;
+            String api = certify ? "PriorityMailIntlCertify" : "PriorityMailIntl";
+            try {
+                responseDocument = sendUspsRequest(api, requestDocument);
+            } catch (UspsRequestException e) {
+                Debug.log(e, module);
+                return ServiceUtil.returnError("Error sending request for USPS Priority Mail International service: " +
+                        e.getMessage());
+            }
+            Element responseElement = responseDocument.getDocumentElement();
+
+            // TODO: No mention of error returns in the docs
+
+            String labelImageString = UtilXml.childElementValue(responseElement, "LabelImage");
+            if (UtilValidate.isEmpty(labelImageString)) {
+                return ServiceUtil.returnError("Incomplete response from the USPS Priority Mail International service: " +
+                        "missing or empty LabelImage element");
+            }
+            shipmentPackageRouteSeg.setBytes("labelImage", Base64.base64Decode(labelImageString.getBytes()));
+            String trackingCode = UtilXml.childElementValue(responseElement, "BarcodeNumber");
+            if (UtilValidate.isEmpty(trackingCode)) {
+                return ServiceUtil.returnError("Incomplete response from the USPS Priority Mail International service: " +
+                        "missing or empty BarcodeNumber element");
+            }
+            shipmentPackageRouteSeg.set("trackingCode", trackingCode);
+            try {
+                shipmentPackageRouteSeg.store();
+            } catch (GenericEntityException e) {
+                Debug.logError(e, module);
+            }
+            
+        }
+        return ServiceUtil.returnSuccess();
+    }
+
     private static Document createUspsRequestDocument(String rootElement, boolean passwordRequired) {
 
         Document requestDocument = UtilXml.makeEmptyXmlDocument(rootElement);
@@ -1467,7 +1704,13 @@
     }
 
     private static Document sendUspsRequest(String requestType, Document requestDocument) throws UspsRequestException {
-        String conUrl = UtilProperties.getPropertyValue("shipment.properties", "shipment.usps.connect.url");
+        String conUrl = null;
+        List<String> labelRequestTypes = UtilMisc.toList("PriorityMailIntl", "PriorityMailIntlCertify");
+        if (labelRequestTypes.contains(requestType)) {
+            conUrl = UtilProperties.getPropertyValue("shipment.properties", "shipment.usps.connect.url.labels");
+        } else {
+            conUrl = UtilProperties.getPropertyValue("shipment.properties", "shipment.usps.connect.url");
+        }
         if (UtilValidate.isEmpty(conUrl)) {
             throw new UspsRequestException("Connection URL not specified; please check your configuration");
         }
@@ -1530,6 +1773,18 @@
 
         return responseDocument;
     }
+
+    /*
+     * Converts decimal pounds to pounds and ounces as an Integer array, ounces are rounded up to the nearest whole number
+     */
+    public static Integer[] convertPoundsToPoundsOunces(BigDecimal decimalPounds) {
+        if (decimalPounds == null) return null;
+        Integer[] poundsOunces = new Integer[2];
+        poundsOunces[0] = Integer.valueOf(decimalPounds.setScale(0, BigDecimal.ROUND_FLOOR).toPlainString());
+        // (weight % 1) * 16 rounded up to nearest whole number
+        poundsOunces[1] = Integer.valueOf(decimalPounds.remainder(BigDecimal.ONE).multiply(new BigDecimal("16")).setScale(0, BigDecimal.ROUND_CEILING).toPlainString());
+        return poundsOunces;
+    }
 }
 
 class UspsRequestException extends GeneralException {