[IMP] delivery_stamps: international and latest wsdl

This commit is contained in:
Jared Kipe
2021-10-04 10:56:49 -07:00
parent 251db70bb6
commit d78a4d0657
10 changed files with 5442 additions and 3900 deletions

View File

@@ -1 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models from . import models

View File

@@ -1,7 +1,9 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{ {
'name': 'Stamps.com (USPS) Shipping', 'name': 'Stamps.com (USPS) Shipping',
'summary': 'Send your shippings through Stamps.com and track them online.', 'summary': 'Send your shippings through Stamps.com and track them online.',
'version': '11.0.2.0.0', 'version': '11.0.2.1.1',
'author': "Hibou Corp.", 'author': "Hibou Corp.",
'category': 'Warehouse', 'category': 'Warehouse',
'license': 'OPL-1', 'license': 'OPL-1',
@@ -15,7 +17,6 @@ Send your shippings through Stamps.com and track them online.
""", """,
'depends': [ 'depends': [
'delivery',
'delivery_hibou', 'delivery_hibou',
], ],
'demo': [], 'demo': [],

View File

@@ -1 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import delivery_stamps from . import delivery_stamps

View File

@@ -1,3 +1,4 @@
Copyright (c) 2019 by Hibou Corp.
Copyright (c) 2014 by Jonathan Zempel. Copyright (c) 2014 by Jonathan Zempel.
Some rights reserved. Some rights reserved.

View File

@@ -15,7 +15,7 @@ from urllib.parse import urljoin
import os import os
VERSION = 84 VERSION = 111
class StampsConfiguration(object): class StampsConfiguration(object):

View File

@@ -145,12 +145,12 @@ class StampsService(BaseService):
def create_add_on(self): def create_add_on(self):
"""Create a new add-on object. """Create a new add-on object.
""" """
return self.create("AddOnV15") return self.create("AddOnV17")
def create_customs(self): def create_customs(self):
"""Create a new customs object. """Create a new customs object.
""" """
return self.create("CustomsV3") return self.create("CustomsV7")
def create_array_of_customs_lines(self): def create_array_of_customs_lines(self):
"""Create a new array of customs objects. """Create a new array of customs objects.
@@ -188,7 +188,7 @@ class StampsService(BaseService):
def create_shipping(self): def create_shipping(self):
"""Create a new shipping object. """Create a new shipping object.
""" """
return self.create("RateV31") return self.create("RateV40")
def get_address(self, address): def get_address(self, address):
"""Get a shipping address. """Get a shipping address.
@@ -202,7 +202,7 @@ class StampsService(BaseService):
""" """
return self.call("GetAccountInfo") return self.call("GetAccountInfo")
def get_label(self, from_address, to_address, rate, transaction_id, image_type=None, def get_label(self, rate, transaction_id, image_type=None,
customs=None, sample=False, extended_postage_info=False): customs=None, sample=False, extended_postage_info=False):
"""Get a shipping label. """Get a shipping label.
@@ -215,7 +215,7 @@ class StampsService(BaseService):
:param sample: Default ``False``. Get a sample label without postage. :param sample: Default ``False``. Get a sample label without postage.
""" """
return self.call("CreateIndicium", IntegratorTxID=transaction_id, return self.call("CreateIndicium", IntegratorTxID=transaction_id,
Rate=rate, From=from_address, To=to_address, ImageType=image_type, Customs=customs, Rate=rate, ImageType=image_type, Customs=customs,
SampleOnly=sample, ExtendedPostageInfo=extended_postage_info) SampleOnly=sample, ExtendedPostageInfo=extended_postage_info)
def get_postage_status(self, transaction_id): def get_postage_status(self, transaction_id):

View File

@@ -1,3 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
import hashlib
from datetime import date from datetime import date
from logging import getLogger from logging import getLogger
from urllib.request import urlopen from urllib.request import urlopen
@@ -31,6 +34,13 @@ STAMPS_PACKAGE_TYPES = [
'Regional Rate Box C', 'Regional Rate Box C',
] ]
STAMPS_CONTENT_TYPES = {
'Letter': 'Document',
'Postcard': 'Document',
}
STAMPS_INTEGRATION_ID = 'f62cb4f0-aa07-4701-a1dd-f0e7c853ed3c'
class ProductPackaging(models.Model): class ProductPackaging(models.Model):
_inherit = 'product.packaging' _inherit = 'product.packaging'
@@ -44,12 +54,14 @@ class ProviderStamps(models.Model):
delivery_type = fields.Selection(selection_add=[('stamps', 'Stamps.com (USPS)')]) delivery_type = fields.Selection(selection_add=[('stamps', 'Stamps.com (USPS)')])
stamps_integration_id = fields.Char(string='Stamps.com Integration ID', groups='base.group_system')
stamps_username = fields.Char(string='Stamps.com Username', groups='base.group_system') stamps_username = fields.Char(string='Stamps.com Username', groups='base.group_system')
stamps_password = fields.Char(string='Stamps.com Password', groups='base.group_system') stamps_password = fields.Char(string='Stamps.com Password', groups='base.group_system')
stamps_service_type = fields.Selection([('US-FC', 'First-Class'), stamps_service_type = fields.Selection([('US-FC', 'First-Class'),
('US-FCI', 'First-Class International'),
('US-PM', 'Priority'), ('US-PM', 'Priority'),
('US-PMI', 'Priority Mail International'),
('US-EMI', ' Priority Mail Express International'),
], ],
required=True, string="Service Type", default="US-PM") required=True, string="Service Type", default="US-PM")
stamps_default_packaging_id = fields.Many2one('product.packaging', string='Default Package Type') stamps_default_packaging_id = fields.Many2one('product.packaging', string='Default Package Type')
@@ -67,12 +79,21 @@ class ProviderStamps(models.Model):
('BZpl', 'BZPL'), ('BZpl', 'BZPL'),
], ],
required=True, string="Image Type", default="Pdf") required=True, string="Image Type", default="Pdf")
stamps_addon_sc = fields.Boolean(string='Add Signature Confirmation')
stamps_addon_dc = fields.Boolean(string='Add Delivery Confirmation')
stamps_addon_hp = fields.Boolean(string='Add Hidden Postage')
def _stamps_package_type(self, package=None): def _stamps_package_type(self, package=None):
if not package: if not package:
return self.stamps_default_packaging_id.shipper_package_code return self.stamps_default_packaging_id.shipper_package_code
return package.packaging_id.shipper_package_code if package.packaging_id.shipper_package_code in STAMPS_PACKAGE_TYPES else 'Package' return package.packaging_id.shipper_package_code if package.packaging_id.shipper_package_code in STAMPS_PACKAGE_TYPES else 'Package'
def _stamps_content_type(self, package=None):
package_type = self._stamps_package_type(package=package)
if package_type in STAMPS_CONTENT_TYPES:
return STAMPS_CONTENT_TYPES[package_type]
return 'Merchandise'
def _stamps_package_is_cubic_pricing(self, package=None): def _stamps_package_is_cubic_pricing(self, package=None):
if not package: if not package:
return self.stamps_default_packaging_id.stamps_cubic_pricing return self.stamps_default_packaging_id.stamps_cubic_pricing
@@ -87,15 +108,21 @@ class ProviderStamps(models.Model):
def _get_stamps_service(self): def _get_stamps_service(self):
sudoself = self.sudo() sudoself = self.sudo()
config = StampsConfiguration(integration_id=sudoself.stamps_integration_id, config = StampsConfiguration(integration_id=STAMPS_INTEGRATION_ID,
username=sudoself.stamps_username, username=sudoself.stamps_username,
password=sudoself.stamps_password) password=sudoself.stamps_password,
wsdl=('testing' if not sudoself.prod_environment else None))
return StampsService(configuration=config) return StampsService(configuration=config)
def _stamps_convert_weight(self, weight): def _stamps_convert_weight(self, weight):
""" weight always expressed in KG """ """ weight always expressed in database units (KG/LBS) """
if self.stamps_default_packaging_id.max_weight and self.stamps_default_packaging_id.max_weight < weight: if self.stamps_default_packaging_id.max_weight and self.stamps_default_packaging_id.max_weight < weight:
raise ValidationError('Stamps cannot ship for weight: ' + str(weight) + 'kgs.') raise ValidationError('Stamps cannot ship for weight: ' + str(weight) + ' kgs/lbs.')
get_param = self.env['ir.config_parameter'].sudo().get_param
product_weight_in_lbs_param = get_param('product.weight_in_lbs')
if product_weight_in_lbs_param == '1':
return weight
weight_in_pounds = weight * 2.20462 weight_in_pounds = weight * 2.20462
return weight_in_pounds return weight_in_pounds
@@ -108,9 +135,10 @@ class ProviderStamps(models.Model):
raise ValidationError('Stamps needs ZIP. From: ' + str(order.warehouse_id.partner_id.zip) + ' To: ' + str(order.partner_shipping_id.zip)) raise ValidationError('Stamps needs ZIP. From: ' + str(order.warehouse_id.partner_id.zip) + ' To: ' + str(order.partner_shipping_id.zip))
ret_val = service.create_shipping() ret_val = service.create_shipping()
ret_val.ShipDate = date_planned.split()[0] if date_planned else date.today().isoformat() ret_val.ShipDate = date_planned.split(' ')[0] if date_planned else date.today().isoformat()
ret_val.FromZIPCode = self.get_shipper_warehouse(order=order).zip.split('-')[0] shipper_partner = self.get_shipper_warehouse(order=order)
ret_val.ToZIPCode = order.partner_shipping_id.zip.split('-')[0] ret_val.From = self._stamps_address(service, shipper_partner)
ret_val.To = self._stamps_address(service, order.partner_shipping_id)
ret_val.PackageType = self._stamps_package_type() ret_val.PackageType = self._stamps_package_type()
ret_val.ServiceType = self.stamps_service_type ret_val.ServiceType = self.stamps_service_type
ret_val.WeightLb = weight ret_val.WeightLb = weight
@@ -133,12 +161,12 @@ class ProviderStamps(models.Model):
raise ValidationError('Stamps needs ZIP. From: ' + str(shipper.zip) + ' To: ' + str(recipient.zip)) raise ValidationError('Stamps needs ZIP. From: ' + str(shipper.zip) + ' To: ' + str(recipient.zip))
ret_val = service.create_shipping() ret_val = service.create_shipping()
ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat() ret_val.ShipDate = date_planned.split(' ')[0] if date_planned else date.today().isoformat()
ret_val.From = self._stamps_address(service, shipper) ret_val.From = self._stamps_address(service, shipper)
ret_val.To = self._stamps_address(service, recipient) ret_val.To = self._stamps_address(service, recipient)
ret_val.PackageType = self._stamps_package_type(package=package) ret_val.PackageType = self._stamps_package_type(package=package)
ret_val.WeightLb = weight ret_val.WeightLb = weight
ret_val.ContentType = self._stamps_content_type() ret_val.ContentType = 'Merchandise'
return ret_val return ret_val
def _stamps_get_addresses_for_picking(self, picking): def _stamps_get_addresses_for_picking(self, picking):
@@ -147,11 +175,36 @@ class ProviderStamps(models.Model):
to = self.get_recipient(picking=picking) to = self.get_recipient(picking=picking)
return company, from_, to return company, from_, to
def _stamps_address(self, service, partner):
address = service.create_address()
if not partner.name or len(partner.name) < 2:
raise ValidationError('Partner (%s) name must be more than 2 characters.' % (partner, ))
address.FullName = partner.name
address.Address1 = partner.street
if partner.street2:
address.Address2 = partner.street2
address.City = partner.city
address.State = partner.state_id.code
if partner.country_id.code == 'US':
zip_pieces = partner.zip.split('-')
address.ZIPCode = zip_pieces[0]
if len(zip_pieces) >= 2:
address.ZIPCodeAddOn = zip_pieces[1]
else:
address.PostalCode = partner.zip or ''
address.Country = partner.country_id.code
res = service.get_address(address).Address
return res
def _stamps_hash_partner(self, partner):
to_hash = ''.join(f[1] if isinstance(f, tuple) else str(f) for f in partner.read(['name', 'street', 'street2', 'city', 'country_id', 'state_id', 'zip', 'phone', 'email'])[0].values())
return hashlib.sha1(to_hash.encode()).hexdigest()
def _stamps_get_shippings_for_picking(self, service, picking): def _stamps_get_shippings_for_picking(self, service, picking):
ret = [] ret = []
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking) company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
if not all((from_partner.zip, to_partner.zip)): if not all((from_partner.zip, to_partner.zip)):
raise ValidationError('Stamps needs ZIP. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip)) raise ValidationError('Stamps needs ZIP/PostalCode. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip))
picking_packages = picking.package_ids picking_packages = picking.package_ids
package_carriers = picking_packages.mapped('carrier_id') package_carriers = picking_packages.mapped('carrier_id')
@@ -165,8 +218,8 @@ class ProviderStamps(models.Model):
ret_val = service.create_shipping() ret_val = service.create_shipping()
ret_val.ShipDate = date.today().isoformat() ret_val.ShipDate = date.today().isoformat()
ret_val.FromZIPCode = from_partner.zip.split('-')[0] ret_val.From = self._stamps_address(service, from_partner)
ret_val.ToZIPCode = to_partner.zip.split('-')[0] ret_val.To = self._stamps_address(service, to_partner)
ret_val.PackageType = self._stamps_package_type(package=package) ret_val.PackageType = self._stamps_package_type(package=package)
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing(package=package) ret_val.CubicPricing = self._stamps_package_is_cubic_pricing(package=package)
ret_val.Length = l ret_val.Length = l
@@ -182,8 +235,8 @@ class ProviderStamps(models.Model):
ret_val = service.create_shipping() ret_val = service.create_shipping()
ret_val.ShipDate = date.today().isoformat() ret_val.ShipDate = date.today().isoformat()
ret_val.FromZIPCode = from_partner.zip.split('-')[0] ret_val.From = self._stamps_address(service, from_partner)
ret_val.ToZIPCode = to_partner.zip.split('-')[0] ret_val.To = self._stamps_address(service, to_partner)
ret_val.PackageType = self._stamps_package_type() ret_val.PackageType = self._stamps_package_type()
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing() ret_val.CubicPricing = self._stamps_package_is_cubic_pricing()
ret_val.Length = l ret_val.Length = l
@@ -191,8 +244,8 @@ class ProviderStamps(models.Model):
ret_val.Height = h ret_val.Height = h
ret_val.ServiceType = self.stamps_service_type ret_val.ServiceType = self.stamps_service_type
ret_val.WeightLb = weight ret_val.WeightLb = weight
ret_val.ContentType = 'Merchandise' ret_val.ContentType = self._stamps_content_type()
ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val)) ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
return ret return ret
@@ -254,6 +307,9 @@ class ProviderStamps(models.Model):
return result return result
return result return result
def _stamps_needs_customs(self, from_partner, to_partner):
return from_partner.country_id.code != to_partner.country_id.code
def stamps_send_shipping(self, pickings): def stamps_send_shipping(self, pickings):
res = [] res = []
service = self._get_stamps_service() service = self._get_stamps_service()
@@ -266,31 +322,9 @@ class ProviderStamps(models.Model):
continue continue
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking) company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
from_address = service.create_address() customs = None
from_address.FullName = company.name if self._stamps_needs_customs(from_partner, to_partner):
from_address.Address1 = from_partner.street customs = service.create_customs()
if from_partner.street2:
from_address.Address2 = from_partner.street2
from_address.City = from_partner.city
from_address.State = from_partner.state_id.code
from_zip_pieces = from_partner.zip.split('-')
from_address.ZIPCode = from_zip_pieces[0]
if len(from_zip_pieces) >= 2:
from_address.ZIPCodeAddOn = from_zip_pieces[1]
from_address = service.get_address(from_address).Address
to_address = service.create_address()
to_address.FullName = to_partner.name
to_address.Address1 = to_partner.street
if to_partner.street2:
to_address.Address2 = to_partner.street2
to_address.City = to_partner.city
to_address.State = to_partner.state_id.code
to_zip_pieces = to_partner.zip.split('-')
to_address.ZIPCode = to_zip_pieces[0]
if len(to_zip_pieces) >= 2:
to_address.ZIPCodeAddOn = to_zip_pieces[1]
to_address = service.get_address(to_address).Address
try: try:
for txn_id, shipping in shippings: for txn_id, shipping in shippings:
@@ -302,20 +336,78 @@ class ProviderStamps(models.Model):
shipping.DeliverDays = rate.DeliverDays shipping.DeliverDays = rate.DeliverDays
if hasattr(rate, 'DimWeighting'): if hasattr(rate, 'DimWeighting'):
shipping.DimWeighting = rate.DimWeighting shipping.DimWeighting = rate.DimWeighting
shipping.Zone = rate.Zone
shipping.RateCategory = rate.RateCategory shipping.RateCategory = rate.RateCategory
shipping.ToState = rate.ToState # shipping.ToState = rate.ToState
add_on = service.create_add_on() addons = []
add_on.AddOnType = 'US-A-DC' if self.stamps_addon_sc:
add_on2 = service.create_add_on() add_on = service.create_add_on()
add_on2.AddOnType = 'SC-A-HP' add_on.AddOnType = 'US-A-SC'
shipping.AddOns.AddOnV15 = [add_on, add_on2] addons.append(add_on)
if self.stamps_addon_dc:
add_on = service.create_add_on()
add_on.AddOnType = 'US-A-DC'
addons.append(add_on)
if self.stamps_addon_hp:
add_on = service.create_add_on()
add_on.AddOnType = 'SC-A-HP'
addons.append(add_on)
shipping.AddOns.AddOnV17 = addons
extended_postage_info = service.create_extended_postage_info() extended_postage_info = service.create_extended_postage_info()
if self.is_amazon(picking=picking): if self.is_amazon(picking=picking):
extended_postage_info.bridgeProfileType = 'Amazon MWS' extended_postage_info.bridgeProfileType = 'Amazon MWS'
label = service.get_label(from_address, to_address, shipping,
if customs:
customs.ContentType = shipping.ContentType
if not picking.package_ids:
raise ValidationError('Cannot use customs without packing items to ship first.')
customs_total = 0.0
product_values = {}
# Note multiple packages will result in all product being on customs form.
# Recommended to ship one customs international package at a time.
for quant in picking.mapped('package_ids.quant_ids'):
# Customs should have the price for the destination but we may not be able
# to rely on the price from the SO (e.g. kit BoM)
product = quant.product_id
quantity = quant.quantity
price = product.lst_price
if to_partner.property_product_pricelist:
# Note the quantity is used for the price, but it is per unit
price = to_partner.property_product_pricelist.get_product_price(product, quantity, to_partner)
if product not in product_values:
product_values[product] = {
'quantity': 0.0,
'value': 0.0,
}
product_values[product]['quantity'] += quantity
product_values[product]['value'] += price * quantity
# Note that Stamps will not allow you to use the scale weight if it is not equal
# to the sum of the customs lines.
# Thus we sum the line
new_total_weight = 0.0
customs_lines = []
for product, values in product_values.items():
customs_line = service.create_customs_lines()
customs_line.Description = product.name
customs_line.Quantity = values['quantity']
customs_total += round(values['value'], 2)
customs_line.Value = round(values['value'], 2)
line_weight = round(self._stamps_convert_weight(product.weight * values['quantity']), 2)
customs_line.WeightLb = line_weight
new_total_weight += line_weight
customs_line.HSTariffNumber = product.hs_code or ''
# customs_line.CountryOfOrigin =
customs_line.sku = product.default_code or ''
customs_lines.append(customs_line)
customs.CustomsLines.CustomsLine = customs_lines
shipping.DeclaredValue = round(customs_total, 2)
shipping.WeightLb = round(new_total_weight, 2)
label = service.get_label(shipping,
transaction_id=txn_id, image_type=self.stamps_image_type, transaction_id=txn_id, image_type=self.stamps_image_type,
extended_postage_info=extended_postage_info) extended_postage_info=extended_postage_info,
customs=customs)
package_labels.append((txn_id, label)) package_labels.append((txn_id, label))
except WebFault as e: except WebFault as e:
_logger.warn(e) _logger.warn(e)
@@ -339,9 +431,13 @@ class ProviderStamps(models.Model):
carrier_price += float(label.Rate.Amount) carrier_price += float(label.Rate.Amount)
url = label.URL url = label.URL
response = urlopen(url) url_spaces = url.split(' ')
attachment = response.read() attachments = []
picking.message_post(body=body, attachments=[('LabelStamps-%s.%s' % (label.TrackingNumber, self.stamps_image_type), attachment)]) for i, url in enumerate(url_spaces, 1):
response = urlopen(url)
attachment = response.read()
attachments.append(('LabelStamps-%s-%s.%s' % (label.TrackingNumber, i, self.stamps_image_type), attachment))
picking.message_post(body=body, attachments=attachments)
shipping_data = {'exact_price': carrier_price, 'tracking_number': ','.join(tracking_numbers)} shipping_data = {'exact_price': carrier_price, 'tracking_number': ','.join(tracking_numbers)}
res = res + [shipping_data] res = res + [shipping_data]
return res return res
@@ -356,7 +452,9 @@ class ProviderStamps(models.Model):
def stamps_cancel_shipment(self, picking): def stamps_cancel_shipment(self, picking):
service = self._get_stamps_service() service = self._get_stamps_service()
try: try:
service.remove_label(picking.carrier_tracking_ref) all_tracking = picking.carrier_tracking_ref
for tracking in all_tracking.split(','):
service.remove_label(tracking.strip())
picking.message_post(body=_(u'Shipment N° %s has been cancelled' % picking.carrier_tracking_ref)) picking.message_post(body=_(u'Shipment N° %s has been cancelled' % picking.carrier_tracking_ref))
picking.write({'carrier_tracking_ref': '', picking.write({'carrier_tracking_ref': '',
'carrier_price': 0.0}) 'carrier_price': 0.0})

View File

@@ -10,7 +10,6 @@
<page string="Stamps.com Configuration" attrs="{'invisible': [('delivery_type', '!=', 'stamps')]}"> <page string="Stamps.com Configuration" attrs="{'invisible': [('delivery_type', '!=', 'stamps')]}">
<group> <group>
<group> <group>
<field name="stamps_integration_id" attrs="{'required': [('delivery_type', '=', 'stamps')]}" />
<field name="stamps_username" attrs="{'required': [('delivery_type', '=', 'stamps')]}" /> <field name="stamps_username" attrs="{'required': [('delivery_type', '=', 'stamps')]}" />
<field name="stamps_password" attrs="{'required': [('delivery_type', '=', 'stamps')]}" password="True"/> <field name="stamps_password" attrs="{'required': [('delivery_type', '=', 'stamps')]}" password="True"/>
</group> </group>
@@ -18,6 +17,9 @@
<field name="stamps_service_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/> <field name="stamps_service_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
<field name="stamps_default_packaging_id" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/> <field name="stamps_default_packaging_id" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
<field name="stamps_image_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/> <field name="stamps_image_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
<field name="stamps_addon_sc"/>
<field name="stamps_addon_dc"/>
<field name="stamps_addon_hp"/>
</group> </group>
</group> </group>
</page> </page>