diff --git a/delivery_stamps/__init__.py b/delivery_stamps/__init__.py
new file mode 100644
index 00000000..09434554
--- /dev/null
+++ b/delivery_stamps/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import models
diff --git a/delivery_stamps/__manifest__.py b/delivery_stamps/__manifest__.py
new file mode 100644
index 00000000..5f9d225a
--- /dev/null
+++ b/delivery_stamps/__manifest__.py
@@ -0,0 +1,28 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+{
+ 'name': 'Stamps.com (USPS) Shipping',
+ 'summary': 'Send your shippings through Stamps.com and track them online.',
+ 'version': '16.0.1.1.0',
+ 'author': "Hibou Corp.",
+ 'category': 'Warehouse',
+ 'license': 'OPL-1',
+ 'images': [],
+ 'website': "https://hibou.io",
+ 'description': """
+Stamps.com (USPS) Shipping
+==========================
+
+Send your shippings through Stamps.com and track them online.
+
+""",
+ 'depends': [
+ 'delivery_hibou',
+ ],
+ 'demo': [],
+ 'data': [
+ 'views/delivery_stamps_view.xml',
+ ],
+ 'auto_install': False,
+ 'installable': True,
+}
diff --git a/delivery_stamps/i18n/es.po b/delivery_stamps/i18n/es.po
new file mode 100644
index 00000000..d7b4036a
--- /dev/null
+++ b/delivery_stamps/i18n/es.po
@@ -0,0 +1,241 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * delivery_stamps
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 15.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-10-12 01:20+0000\n"
+"PO-Revision-Date: 2021-10-12 01:20+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_service_type__us-emi
+msgid " Priority Mail Express International"
+msgstr "Servicio Postal Prioritario Express Internacional"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__azpl
+msgid "AZPL"
+msgstr "AZPL"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_addon_dc
+msgid "Add Delivery Confirmation"
+msgstr "Agregar Confirmación de Entrega"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_addon_hp
+msgid "Add Hidden Postage"
+msgstr "Agregar Franqueo Oculto"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_addon_sc
+msgid "Add Signature Confirmation"
+msgstr "Agregar Firma de Confirmación"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__auto
+msgid "Auto"
+msgstr "Automático"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__bzpl
+msgid "BZPL"
+msgstr "BZPL"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Cancelling due to error: "
+msgstr "Cancelando debido a un error"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Cannot use customs without packing items to ship first."
+msgstr "No se puede usar el servicio de aduana sin empacar los artículos para enviar primero."
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_stock_package_type__package_carrier_type
+msgid "Carrier"
+msgstr "Transportista"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_default_packaging_id
+msgid "Default Package Type"
+msgstr "Tipo de Paquete Predeterminado"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__epl
+msgid "EPL"
+msgstr "EPL"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__encryptedpngurl
+msgid "Encrypted PNG URL"
+msgstr "URL de PNG Encriptado"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Error Retrieving Response from Stamps.com"
+msgstr "Error al recuperar una respuesta de Stamps.com"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Error on full shipment. Attempted to cancel any previously shipped."
+msgstr "Error en el envío completo. Se intentó cancelar cualquier envío que se había enviado anteriormente"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Error on shipment. \"%s\""
+msgstr "Error en el envío. \"%s\""
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_service_type__us-fc
+msgid "First-Class"
+msgstr "Primera-Clase"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_service_type__us-fci
+msgid "First-Class International"
+msgstr "Primera Clase Internacional"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__gif
+msgid "GIF"
+msgstr "GIF"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_image_type
+msgid "Image Type"
+msgstr "Tipo de Imagen"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__jpg
+msgid "JPG"
+msgstr "JPG"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "No valid rates returned from Stamps.com"
+msgstr "No se devolvieron tarifas válidas de Stamps.com"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__pdf
+msgid "PDF"
+msgstr "PDF"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__png
+msgid "PNG"
+msgstr "PNG"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Partner (%s) name must be more than 2 characters."
+msgstr "El nombre del Socio (%s) debe tener más de dos caracteres"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__printoncepdf
+msgid "Print Once PDF"
+msgstr "Imprimir PDF Una Vez"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_service_type__us-pm
+msgid "Priority"
+msgstr "Prioridad"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_service_type__us-pmi
+msgid "Priority Mail International"
+msgstr "Servicio Postal Prioritaria Internacional"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__delivery_type
+msgid "Provider"
+msgstr "Proveedor"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_service_type
+msgid "Service Type"
+msgstr "Tipo de Servicio"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Shipment N° %s has been cancelled"
+msgstr "Número de Envío %s ha sido cancelado"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid ""
+"Shipment created into Stamps.com
Tracking Number :
\"%s\" "
+""
+msgstr "El envío ha sido creado en Stamps.com
Número de Seguimiento :
\"%s\" "
+
+#. module: delivery_stamps
+#: model:ir.model,name:delivery_stamps.model_delivery_carrier
+msgid "Shipping Methods"
+msgstr "Métodos de Envío"
+
+#. module: delivery_stamps
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#: code:addons/delivery_stamps/models/delivery_stamps.py:0
+#, python-format
+msgid "Stamps needs ZIP. From: \"%s\" To: \"%s\""
+msgstr "Stamps requiere un Código Postal. Desde: %s Hasta: %s"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__stock_package_type__package_carrier_type__stamps
+msgid "Stamps.com"
+msgstr "Stamps.com"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__delivery_type__stamps
+msgid "Stamps.com (USPS)"
+msgstr "Stamps.com (USPS)"
+
+#. module: delivery_stamps
+#: model_terms:ir.ui.view,arch_db:delivery_stamps.view_delivery_carrier_form_with_provider_stamps
+msgid "Stamps.com Configuration"
+msgstr "Stamps.com Configuración"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_password
+msgid "Stamps.com Password"
+msgstr "Stamps.com Clave"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_stock_package_type__stamps_cubic_pricing
+msgid "Stamps.com Use Cubic Pricing"
+msgstr "Stamps.com Utilice el Precio Cúbicos"
+
+#. module: delivery_stamps
+#: model:ir.model.fields,field_description:delivery_stamps.field_delivery_carrier__stamps_username
+msgid "Stamps.com Username"
+msgstr "Stamps.com Usuario"
+
+#. module: delivery_stamps
+#: model:ir.model,name:delivery_stamps.model_stock_package_type
+msgid "Stock package type"
+msgstr "Tipo de Paquete de Stock"
+
+#. module: delivery_stamps
+#: model:ir.model.fields.selection,name:delivery_stamps.selection__delivery_carrier__stamps_image_type__zpl
+msgid "ZPL"
+msgstr "ZPL"
diff --git a/delivery_stamps/models/__init__.py b/delivery_stamps/models/__init__.py
new file mode 100644
index 00000000..1f944aa8
--- /dev/null
+++ b/delivery_stamps/models/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import delivery_stamps
diff --git a/delivery_stamps/models/api/LICENSE b/delivery_stamps/models/api/LICENSE
new file mode 100755
index 00000000..b699676b
--- /dev/null
+++ b/delivery_stamps/models/api/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 2019 by Hibou Corp.
+Copyright (c) 2014 by Jonathan Zempel.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+* The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/delivery_stamps/models/api/__init__.py b/delivery_stamps/models/api/__init__.py
new file mode 100755
index 00000000..0654679c
--- /dev/null
+++ b/delivery_stamps/models/api/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+"""
+ stamps
+ ~~~~~~
+
+ Stamps.com API.
+
+ :copyright: 2014 by Jonathan Zempel.
+ :license: BSD, see LICENSE for more details.
+"""
+
+__author__ = "Jonathan Zempel"
+__license__ = "BSD"
+__version__ = "0.9.1"
diff --git a/delivery_stamps/models/api/config.py b/delivery_stamps/models/api/config.py
new file mode 100755
index 00000000..937ef6b7
--- /dev/null
+++ b/delivery_stamps/models/api/config.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+"""
+ stamps.config
+ ~~~~~~~~~~~~~
+
+ Stamps.com configuration.
+
+ :copyright: 2014 by Jonathan Zempel.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from configparser import NoOptionError, NoSectionError, SafeConfigParser
+from urllib.request import pathname2url
+from urllib.parse import urljoin
+import os
+
+
+VERSION = 111
+
+
+class StampsConfiguration(object):
+ """Stamps service configuration. The service configuration may be provided
+ directly via parameter values, or it can be read from a configuration file.
+ If no parameters are given, the configuration will attempt to read from a
+ ``'.stamps.cfg'`` file in the user's HOME directory. Alternately, a
+ configuration filename can be passed to the constructor.
+
+ Here is a sample configuration (by default the constructor reads from a
+ ``'default'`` section)::
+
+ [default]
+ integration_id = XXXXXXXX-1111-2222-3333-YYYYYYYYYYYY
+ username = stampy
+ password = secret
+
+ :param integration_id: Default `None`. Unique ID, provided by Stamps.com,
+ that represents your application.
+ :param username: Default `None`. Stamps.com account username.
+ :param password: Default `None`. Stamps.com password.
+ :param wsdl: Default `None`. WSDL URI. Use ``'testing'`` to use the test
+ server WSDL.
+ :param port: Default `None`. The name of the WSDL port to use.
+ :param file_name: Default `None`. Optional configuration file name.
+ :param section: Default ``'default'``. The configuration section to use.
+ """
+
+ def __init__(self, integration_id=None, username=None, password=None,
+ wsdl=None, port=None, file_name=None, section="default"):
+ parser = SafeConfigParser()
+
+ if file_name:
+ parser.read([file_name])
+ else:
+ parser.read([os.path.expanduser("~/.stamps.cfg")])
+
+ self.integration_id = self.__get(parser, section, "integration_id",
+ integration_id)
+ self.username = self.__get(parser, section, "username", username)
+ self.password = self.__get(parser, section, "password", password)
+ self.wsdl = self.__get(parser, section, "wsdl", wsdl)
+ self.port = self.__get(parser, section, "port", port)
+
+ if self.wsdl is None or wsdl == "testing":
+ file_path = os.path.abspath(__file__)
+ directory_path = os.path.dirname(file_path)
+
+ if wsdl == "testing":
+ file_name = "stamps_v{0}.test.wsdl".format(VERSION)
+ else:
+ file_name = "stamps_v{0}.wsdl".format(VERSION)
+
+ wsdl = os.path.join(directory_path, "wsdls", file_name)
+ self.wsdl = urljoin("file:", pathname2url(wsdl))
+
+ if self.port is None:
+ self.port = "SwsimV{0}Soap12".format(VERSION)
+
+ assert self.integration_id
+ assert self.username
+ assert self.password
+ assert self.wsdl
+ assert self.port
+
+ @staticmethod
+ def __get(parser, section, name, default):
+ """Get a configuration value for the named section.
+
+ :param parser: The configuration parser.
+ :param section: The section for the given name.
+ :param name: The name of the value to retrieve.
+ """
+ if default:
+ vars = {name: default}
+ else:
+ vars = None
+
+ try:
+ ret_val = parser.get(section, name, vars=vars)
+ except (NoSectionError, NoOptionError):
+ ret_val = default
+
+ return ret_val
diff --git a/delivery_stamps/models/api/services.py b/delivery_stamps/models/api/services.py
new file mode 100755
index 00000000..a53aaea9
--- /dev/null
+++ b/delivery_stamps/models/api/services.py
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+"""
+ stamps.services
+ ~~~~~~~~~~~~~~~
+
+ Stamps.com services.
+
+ :copyright: 2014 by Jonathan Zempel.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from decimal import Decimal
+from logging import getLogger
+from re import compile
+from suds import WebFault
+from suds.bindings.document import Document
+from suds.client import Client
+from suds.plugin import MessagePlugin
+from suds.sax.element import Element
+from suds.sudsobject import asdict
+from suds.xsd.sxbase import XBuiltin
+from suds.xsd.sxbuiltin import Factory
+
+
+PATTERN_HEX = r"[0-9a-fA-F]"
+PATTERN_ID = r"{hex}{{8}}-{hex}{{4}}-{hex}{{4}}-{hex}{{4}}-{hex}{{12}}".format(
+ hex=PATTERN_HEX)
+RE_TRANSACTION_ID = compile(PATTERN_ID)
+
+
+# class LogPlugin(MessagePlugin):
+# def __init__(self):
+# self.logger = getLogger('stamps2')
+# self.last_sent_raw = None
+# self.last_received_raw = None
+#
+# def sending(self, context):
+# self.last_sent_raw = str(context.envelope)
+# self.logger.warning(self.last_sent_raw)
+#
+# def received(self, context):
+# self.last_received_raw = str(context.reply)
+# self.logger.warning(self.last_received_raw)
+
+
+class AuthenticatorPlugin(MessagePlugin):
+ """Handle message authentication.
+
+ :param credentials: Stamps API credentials.
+ :param wsdl: Configured service client.
+ """
+
+ def __init__(self, credentials, client):
+ self.credentials = credentials
+ self.client = client
+ self.authenticator = None
+
+ def marshalled(self, context):
+ """Add an authenticator token to the document before it is sent.
+
+ :param context: The current message context.
+ """
+ body = context.envelope.getChild("Body")
+ operation = body[0]
+
+ if operation.name in ("AuthenticateUser", "RegisterAccount"):
+ pass
+ elif self.authenticator:
+ namespace = operation.namespace()
+ element = Element("Authenticator", ns=namespace)
+ element.setText(self.authenticator)
+ operation.insert(element)
+ else:
+ document = Document(self.client.wsdl)
+ method = self.client.service.AuthenticateUser.method
+ parameter = document.param_defs(method)[0]
+ element = document.mkparam(method, parameter, self.credentials)
+ operation.insert(element)
+
+ def unmarshalled(self, context):
+ """Store the authenticator token for the next call.
+
+ :param context: The current message context.
+ """
+ if hasattr(context.reply, "Authenticator"):
+ self.authenticator = context.reply.Authenticator
+ del context.reply.Authenticator
+ else:
+ self.authenticator = None
+
+ return context
+
+
+class BaseService(object):
+ """Base service.
+
+ :param configuration: API configuration.
+ """
+
+ def __init__(self, configuration):
+ Factory.maptag("decimal", XDecimal)
+ self.client = Client(configuration.wsdl)
+ credentials = self.create("Credentials")
+ credentials.IntegrationID = configuration.integration_id
+ credentials.Username = configuration.username
+ credentials.Password = configuration.password
+ self.plugin = AuthenticatorPlugin(credentials, self.client)
+ # put in plugins=[] as well
+ # self.logplugin = LogPlugin()
+ self.client.set_options(plugins=[self.plugin], port=configuration.port)
+ self.logger = getLogger("stamps")
+
+ def call(self, method, **kwargs):
+ """Call the given web service method.
+
+ :param method: The name of the web service operation to call.
+ :param kwargs: Method keyword-argument parameters.
+ """
+ self.logger.debug("%s(%s)", method, kwargs)
+ instance = getattr(self.client.service, method)
+
+ try:
+ ret_val = instance(**kwargs)
+ except WebFault as error:
+ self.logger.warning("Retry %s", method, exc_info=True)
+ self.plugin.authenticator = None
+
+ try: # retry with a re-authenticated user.
+ ret_val = instance(**kwargs)
+ except WebFault as error:
+ self.logger.exception("%s retry failed", method)
+ self.plugin.authenticator = None
+ raise error
+
+ return ret_val
+
+ def create(self, wsdl_type):
+ """Create an object of the given WSDL type.
+
+ :param wsdl_type: The WSDL type to create an object for.
+ """
+ return self.client.factory.create(wsdl_type)
+
+
+class StampsService(BaseService):
+ """Stamps.com service.
+ """
+
+ def add_postage(self, amount, transaction_id=None):
+ """Add postage to the account.
+
+ :param amount: The amount of postage to purchase.
+ :param transaction_id: Default `None`. ID that may be used to retry the
+ purchase of this postage.
+ """
+ account = self.get_account()
+ control = account.AccountInfo.PostageBalance.ControlTotal
+
+ return self.call("PurchasePostage", PurchaseAmount=amount,
+ ControlTotal=control, IntegratorTxID=transaction_id)
+
+ def create_add_on(self):
+ """Create a new add-on object.
+ """
+ return self.create("AddOnV17")
+
+ def create_customs(self):
+ """Create a new customs object.
+ """
+ return self.create("CustomsV7")
+
+ def create_array_of_customs_lines(self):
+ """Create a new array of customs objects.
+ """
+ return self.create("ArrayOfCustomsLine")
+
+ def create_customs_lines(self):
+ """Create new customs lines.
+ """
+ return self.create("CustomsLine")
+
+ def create_address(self):
+ """Create a new address object.
+ """
+ return self.create("Address")
+
+ def create_purchase_status(self):
+ """Create a new purchase status object.
+ """
+ return self.create("PurchaseStatus")
+
+ def create_registration(self):
+ """Create a new registration object.
+ """
+ ret_val = self.create("RegisterAccount")
+ ret_val.IntegrationID = self.plugin.credentials.IntegrationID
+ ret_val.UserName = self.plugin.credentials.Username
+ ret_val.Password = self.plugin.credentials.Password
+
+ return ret_val
+
+ def create_extended_postage_info(self):
+ return self.create("ExtendedPostageInfoV1")
+
+ def create_shipping(self):
+ """Create a new shipping object.
+ """
+ return self.create("RateV40")
+
+ def get_address(self, address):
+ """Get a shipping address.
+
+ :param address: Address instance to get a clean shipping address for.
+ """
+ return self.call("CleanseAddress", Address=address)
+
+ def get_account(self):
+ """Get account information.
+ """
+ return self.call("GetAccountInfo")
+
+ def get_label(self, rate, transaction_id, image_type=None,
+ customs=None, sample=False, extended_postage_info=False):
+ """Get a shipping label.
+
+ :param from_address: The shipping 'from' address.
+ :param to_address: The shipping 'to' address.
+ :param rate: A rate instance for the shipment.
+ :param transaction_id: ID that may be used to retry/rollback the
+ purchase of this label.
+ :param customs: A customs instance for international shipments.
+ :param sample: Default ``False``. Get a sample label without postage.
+ """
+ return self.call("CreateIndicium", IntegratorTxID=transaction_id,
+ Rate=rate, ImageType=image_type, Customs=customs,
+ SampleOnly=sample, ExtendedPostageInfo=extended_postage_info)
+
+ def get_postage_status(self, transaction_id):
+ """Get postage purchase status.
+
+ :param transaction_id: The transaction ID returned by
+ :meth:`add_postage`.
+ """
+ return self.call("GetPurchaseStatus", TransactionID=transaction_id)
+
+ def get_rates(self, shipping):
+ """Get shipping rates.
+
+ :param shipping: Shipping instance to get rates for.
+ """
+ rates = self.call("GetRates", Rate=shipping)
+
+ if rates.Rates:
+ ret_val = [rate for rate in rates.Rates.Rate]
+ else:
+ ret_val = []
+
+ return ret_val
+
+ def get_tracking(self, transaction_id):
+ """Get tracking events for a shipment.
+
+ :param transaction_id: The transaction ID (or tracking number) returned
+ by :meth:`get_label`.
+ """
+ if RE_TRANSACTION_ID.match(transaction_id):
+ arguments = dict(StampsTxID=transaction_id)
+ else:
+ arguments = dict(TrackingNumber=transaction_id)
+
+ return self.call("TrackShipment", **arguments)
+
+ def register_account(self, registration):
+ """Register a new account.
+
+ :param registration: Registration instance.
+ """
+ arguments = asdict(registration)
+
+ return self.call("RegisterAccount", **arguments)
+
+ def remove_label(self, transaction_id):
+ """Cancel a shipping label.
+
+ :param transaction_id: The transaction ID (or tracking number) returned
+ by :meth:`get_label`.
+ """
+ if RE_TRANSACTION_ID.match(transaction_id):
+ arguments = dict(StampsTxID=transaction_id)
+ else:
+ arguments = dict(TrackingNumber=transaction_id)
+
+ return self.call("CancelIndicium", **arguments)
+
+
+class XDecimal(XBuiltin):
+ """Represents an XSD decimal type.
+ """
+
+ def translate(self, value, topython=True):
+ """Translate between string and decimal values.
+
+ :param value: The value to translate.
+ :param topython: Default `True`. Determine whether to translate the
+ value for python.
+ """
+ if topython:
+ if isinstance(value, str) and len(value):
+ ret_val = Decimal(value)
+ else:
+ ret_val = None
+ else:
+ if isinstance(value, (int, float, Decimal)):
+ ret_val = str(value)
+ else:
+ ret_val = value
+
+ return ret_val
diff --git a/delivery_stamps/models/api/tests.py b/delivery_stamps/models/api/tests.py
new file mode 100755
index 00000000..5f5669e9
--- /dev/null
+++ b/delivery_stamps/models/api/tests.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+"""
+ stamps.tests
+ ~~~~~~~~~~~~
+
+ Stamps.com API tests.
+
+ :copyright: 2014 by Jonathan Zempel.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from .config import StampsConfiguration
+from .services import StampsService
+from datetime import date, datetime
+from time import sleep
+from unittest import TestCase
+import logging
+import os
+
+
+logging.basicConfig()
+logging.getLogger("suds.client").setLevel(logging.DEBUG)
+file_path = os.path.abspath(__file__)
+directory_path = os.path.dirname(file_path)
+file_name = os.path.join(directory_path, "tests.cfg")
+CONFIGURATION = StampsConfiguration(wsdl="testing", file_name=file_name)
+
+
+def get_rate(service):
+ """Get a test rate.
+
+ :param service: Instance of the stamps service.
+ """
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date.today().isoformat()
+ ret_val.FromZIPCode = "94107"
+ ret_val.ToZIPCode = "20500"
+ ret_val.PackageType = "Package"
+ rate = service.get_rates(ret_val)[0]
+ ret_val.Amount = rate.Amount
+ ret_val.ServiceType = rate.ServiceType
+ ret_val.DeliverDays = rate.DeliverDays
+ ret_val.DimWeighting = rate.DimWeighting
+ ret_val.Zone = rate.Zone
+ ret_val.RateCategory = rate.RateCategory
+ ret_val.ToState = rate.ToState
+ add_on = service.create_add_on()
+ add_on.AddOnType = "US-A-DC"
+ ret_val.AddOns.AddOnV15.append(add_on)
+
+ return ret_val
+
+
+def get_from_address(service):
+ """Get a test 'from' address.
+
+ :param service: Instance of the stamps service.
+ """
+ address = service.create_address()
+ address.FullName = "Pickwick & Weller"
+ address.Address1 = "300 Brannan St."
+ address.Address2 = "Suite 405"
+ address.City = "San Francisco"
+ address.State = "CA"
+
+ return service.get_address(address).Address
+
+
+def get_to_address(service):
+ """Get a test 'to' address.
+
+ :param service: Instance of the stamps service.
+ """
+ address = service.create_address()
+ address.FullName = "POTUS"
+ address.Address1 = "1600 Pennsylvania Avenue NW"
+ address.City = "Washington"
+ address.State = "DC"
+
+ return service.get_address(address).Address
+
+
+class StampsTestCase(TestCase):
+
+ initialized = False
+
+ def setUp(self):
+ if not StampsTestCase.initialized:
+ self.service = StampsService(CONFIGURATION)
+ StampsTestCase.initalized = True
+
+ def _test_0(self):
+ """Test account registration.
+ """
+ registration = self.service.create_registration()
+ type = self.service.create("CodewordType")
+ registration.Codeword1Type = type.Last4SocialSecurityNumber
+ registration.Codeword1 = 1234
+ registration.Codeword2Type = type.Last4DriversLicense
+ registration.Codeword2 = 1234
+ registration.PhysicalAddress = get_from_address(self.service)
+ registration.MachineInfo.IPAddress = "127.0.0.1"
+ registration.Email = "sws-support@stamps.com"
+ type = self.service.create("AccountType")
+ registration.AccountType = type.OfficeBasedBusiness
+ result = self.service.register_account(registration)
+ print result
+
+ def _test_1(self):
+ """Test postage purchase.
+ """
+ transaction_id = datetime.now().isoformat()
+ result = self.service.add_postage(10, transaction_id=transaction_id)
+ transaction_id = result.TransactionID
+ status = self.service.create_purchase_status()
+ seconds = 4
+
+ while result.PurchaseStatus in (status.Pending, status.Processing):
+ seconds = 32 if seconds * 2 >= 32 else seconds * 2
+ print "Waiting {0:d} seconds to get status...".format(seconds)
+ sleep(seconds)
+ result = self.service.get_postage_status(transaction_id)
+
+ print result
+
+ def test_2(self):
+ """Test label generation.
+ """
+ self.service = StampsService(CONFIGURATION)
+ rate = get_rate(self.service)
+ from_address = get_from_address(self.service)
+ to_address = get_to_address(self.service)
+ transaction_id = datetime.now().isoformat()
+ label = self.service.get_label(from_address, to_address, rate,
+ transaction_id=transaction_id)
+ self.service.get_tracking(label.StampsTxID)
+ self.service.get_tracking(label.TrackingNumber)
+ self.service.remove_label(label.StampsTxID)
+ print label
+
+ def test_3(self):
+ """Test authentication retry.
+ """
+ self.service.get_account()
+ authenticator = self.service.plugin.authenticator
+ self.service.get_account()
+ self.service.plugin.authenticator = authenticator
+ result = self.service.get_account()
+ print result
diff --git a/delivery_stamps/models/api/wsdls/stamps_v111.test.wsdl b/delivery_stamps/models/api/wsdls/stamps_v111.test.wsdl
new file mode 100644
index 00000000..ad1a2011
--- /dev/null
+++ b/delivery_stamps/models/api/wsdls/stamps_v111.test.wsdl
@@ -0,0 +1,6039 @@
+
+
+ Stamps.com Web Services for Individual Meters (SWS/IM) Version 111
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generate an indicium.
+
+
+
+
+ Calculate a rate or a list of rates.
+
+
+
+
+ Generate an envelope indicium.
+
+
+
+
+ Generate a mailing label sheet.
+
+
+
+
+ Generate NetStamps indicia.
+
+
+
+
+ Get URL for a Stamps.com web page.
+
+
+
+
+ Register a new Stamps.com account.
+
+
+
+
+ Add the image uploaded by the user.
+
+
+
+
+ Create Branding.
+
+
+
+
+ Get Branding.
+
+
+
+
+ Modify Branding.
+
+
+
+
+ Delete Branding.
+
+
+
+
+ Schedule carrier pickup.
+
+
+
+
+ Modify existing carrier pickup request.
+
+
+
+
+ Cancel existing carrier pickup request.
+
+
+
+
+ Begin Reprint Indicium
+
+
+
+
+ Add carrier.
+
+
+
+
+ Delete carrier.
+
+
+
+
+ Get tracking events for shipment.
+
+
+
+
+ Check carrier pickup availability for an address.
+
+
+
+
+ Creates a Parcel Guard insurance claim.
+
+
+
+
+ Get list of shipments.
+
+
+
+
+ Generate a Carrier Shipment Manifest.
+
+
+
+
+ Start account verification for phone.
+
+
+
+
+ Finish account verification for phone.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Recover Username.
+
+
+
+
+ Purchase additional postage.
+
+
+
+
+ Authenticate Bridge Authenticator
+
+
+
+
+ Get the images uploaded by the user.
+
+
+
+
+ Delete an image uploaded by the user.
+
+
+
+
+ Get the existing carrier pickup requests.
+
+
+
+
+ Cancel an Account.
+
+
+
+
+ Cleanse an address.
+
+
+
+
+ Price Store Orders.
+
+
+
+
+ Place Store Orders.
+
+
+
+
+ Get list of shipments.
+
+
+
+
+ Change Plan.
+
+
+
+
+ Get list of transactions.
+
+
+
+
+ Get list of transactions.
+
+
+
+
+ Cancel a previously issued indicium.
+
+
+
+
+ Set CodeWord information
+
+
+
+
+ Set auto-buy settings
+
+
+
+
+ Return the list of available CodeWord types.
+
+
+
+
+ Get list of supported countries.
+
+
+
+
+ Change Password.
+
+
+
+
+ Get NetStamps Images.
+
+
+
+
+ Get status of plan change.
+
+
+
+
+ Resubmit Purchase.
+
+
+
+
+ Get list of NetStamps layouts.
+
+
+
+
+ Get list of cost codes.
+
+
+
+
+ Authenticate with transfer authenticator.
+
+
+
+
+ Start a password reset by sending a temporary password to the e-mail address on file.
+
+
+
+
+ Finish a password reset, setting the permanent password to a new password.
+
+
+
+
+ Retrieve codeword questions for user for starting password reset.
+
+
+
+
+ Initial authentication.
+
+
+
+
+ Get account information, including postage balance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Stamps.com Web Services for Individual Meters (SWS/IM) Version 111
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/delivery_stamps/models/api/wsdls/stamps_v111.wsdl b/delivery_stamps/models/api/wsdls/stamps_v111.wsdl
new file mode 100644
index 00000000..78de10ee
--- /dev/null
+++ b/delivery_stamps/models/api/wsdls/stamps_v111.wsdl
@@ -0,0 +1,6039 @@
+
+
+ Stamps.com Web Services for Individual Meters (SWS/IM) Version 111
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generate an indicium.
+
+
+
+
+ Calculate a rate or a list of rates.
+
+
+
+
+ Generate an envelope indicium.
+
+
+
+
+ Generate a mailing label sheet.
+
+
+
+
+ Generate NetStamps indicia.
+
+
+
+
+ Get URL for a Stamps.com web page.
+
+
+
+
+ Register a new Stamps.com account.
+
+
+
+
+ Add the image uploaded by the user.
+
+
+
+
+ Create Branding.
+
+
+
+
+ Get Branding.
+
+
+
+
+ Modify Branding.
+
+
+
+
+ Delete Branding.
+
+
+
+
+ Schedule carrier pickup.
+
+
+
+
+ Modify existing carrier pickup request.
+
+
+
+
+ Cancel existing carrier pickup request.
+
+
+
+
+ Begin Reprint Indicium
+
+
+
+
+ Add carrier.
+
+
+
+
+ Delete carrier.
+
+
+
+
+ Get tracking events for shipment.
+
+
+
+
+ Check carrier pickup availability for an address.
+
+
+
+
+ Creates a Parcel Guard insurance claim.
+
+
+
+
+ Get list of shipments.
+
+
+
+
+ Generate a Carrier Shipment Manifest.
+
+
+
+
+ Start account verification for phone.
+
+
+
+
+ Finish account verification for phone.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Add a payment method for the user.
+
+
+
+
+ Recover Username.
+
+
+
+
+ Purchase additional postage.
+
+
+
+
+ Authenticate Bridge Authenticator
+
+
+
+
+ Get the images uploaded by the user.
+
+
+
+
+ Delete an image uploaded by the user.
+
+
+
+
+ Get the existing carrier pickup requests.
+
+
+
+
+ Cancel an Account.
+
+
+
+
+ Cleanse an address.
+
+
+
+
+ Price Store Orders.
+
+
+
+
+ Place Store Orders.
+
+
+
+
+ Get list of shipments.
+
+
+
+
+ Change Plan.
+
+
+
+
+ Get list of transactions.
+
+
+
+
+ Get list of transactions.
+
+
+
+
+ Cancel a previously issued indicium.
+
+
+
+
+ Set CodeWord information
+
+
+
+
+ Set auto-buy settings
+
+
+
+
+ Return the list of available CodeWord types.
+
+
+
+
+ Get list of supported countries.
+
+
+
+
+ Change Password.
+
+
+
+
+ Get NetStamps Images.
+
+
+
+
+ Get status of plan change.
+
+
+
+
+ Resubmit Purchase.
+
+
+
+
+ Get list of NetStamps layouts.
+
+
+
+
+ Get list of cost codes.
+
+
+
+
+ Authenticate with transfer authenticator.
+
+
+
+
+ Start a password reset by sending a temporary password to the e-mail address on file.
+
+
+
+
+ Finish a password reset, setting the permanent password to a new password.
+
+
+
+
+ Retrieve codeword questions for user for starting password reset.
+
+
+
+
+ Initial authentication.
+
+
+
+
+ Get account information, including postage balance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Stamps.com Web Services for Individual Meters (SWS/IM) Version 111
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/delivery_stamps/models/delivery_stamps.py b/delivery_stamps/models/delivery_stamps.py
new file mode 100644
index 00000000..dee8aec8
--- /dev/null
+++ b/delivery_stamps/models/delivery_stamps.py
@@ -0,0 +1,571 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+import hashlib
+from datetime import date
+from logging import getLogger
+from urllib.request import urlopen
+from suds import WebFault
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError, ValidationError
+
+from .api.config import StampsConfiguration
+from .api.services import StampsService
+
+_logger = getLogger(__name__)
+
+STAMPS_US_APO_FPO_STATE_CODES = (
+ 'AE',
+ 'AP',
+ 'AA',
+)
+
+STAMPS_PACKAGE_TYPES = [
+ 'Unknown',
+ 'Postcard',
+ 'Letter',
+ 'Large Envelope or Flat',
+ 'Thick Envelope',
+ 'Package',
+ 'Flat Rate Box',
+ 'Small Flat Rate Box',
+ 'Large Flat Rate Box',
+ 'Flat Rate Envelope',
+ 'Flat Rate Padded Envelope',
+ 'Large Package',
+ 'Oversized Package',
+ 'Regional Rate Box A',
+ 'Regional Rate Box B',
+ 'Legal Flat Rate Envelope',
+ 'Regional Rate Box C',
+]
+
+STAMPS_CONTENT_TYPES = {
+ 'Letter': 'Document',
+ 'Postcard': 'Document',
+}
+
+STAMPS_INTEGRATION_ID = 'f62cb4f0-aa07-4701-a1dd-f0e7c853ed3c'
+
+
+class StockPackageType(models.Model):
+ _inherit = 'stock.package.type'
+
+ package_carrier_type = fields.Selection(selection_add=[('stamps', 'Stamps.com')])
+ stamps_cubic_pricing = fields.Boolean(string="Stamps.com Use Cubic Pricing")
+
+
+class ProviderStamps(models.Model):
+ _inherit = 'delivery.carrier'
+
+ delivery_type = fields.Selection(selection_add=[('stamps', 'Stamps.com')],
+ ondelete={'stamps': 'set default'})
+
+ 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_service_type = fields.Selection([('US-FC', 'USPS First-Class'),
+ ('US-PM', 'USPS Priority'),
+ ('US-XM', 'USPS Express'),
+ ('US-MM', 'USPS Media Mail'),
+ ('US-FCI', 'USPS First-Class International'),
+ ('US-PMI', 'USPS Priority International'),
+ ('US-EMI', 'USPS Express International'),
+ ('SC-GPE', 'GlobalPost Economy'),
+ ('SC-GPP', 'GlobalPost Standard'),
+ ('SC-GPESS', 'GlobalPost SmartSaver Economy'),
+ ('SC-GPPSS', 'GlobalPost SmartSaver Standard'),
+ ],
+ required=True, string="Service Type", default="US-PM")
+ stamps_default_packaging_id = fields.Many2one('stock.package.type', string='Default Package Type')
+
+ stamps_image_type = fields.Selection([('Auto', 'Auto'),
+ ('Png', 'PNG'),
+ ('Gif', 'GIF'),
+ ('Pdf', 'PDF'),
+ ('Epl', 'EPL'),
+ ('Jpg', 'JPG'),
+ ('PrintOncePdf', 'Print Once PDF'),
+ ('EncryptedPngUrl', 'Encrypted PNG URL'),
+ ('Zpl', 'ZPL'),
+ ('AZpl', 'AZPL'),
+ ('BZpl', 'BZPL'),
+ ],
+ required=True, string="Image Type", default="Pdf",
+ help='Generally PDF or ZPL are the great options.')
+ 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):
+ if not package:
+ return self.stamps_default_packaging_id.shipper_package_code
+ return package.package_type_id.shipper_package_code if package.package_type_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):
+ if not package:
+ return self.stamps_default_packaging_id.stamps_cubic_pricing
+ return package.package_type_id.stamps_cubic_pricing
+
+ def _stamps_package_dimensions(self, package=None):
+ if not package:
+ package_type = self.stamps_default_packaging_id
+ else:
+ package_type = package.package_type_id
+ length_uom = self.env['product.template']._get_length_uom_id_from_ir_config_parameter()
+ if length_uom.name == 'ft':
+ return round(package_type.packaging_length / 12.0), round(package_type.width / 12.0), round(package_type.height / 12.0)
+ elif length_uom.name == 'mm':
+ return round(package_type.packaging_length * 0.0393701), round(package_type.width * 0.0393701), round(package_type.height * 0.0393701)
+ return package_type.packaging_length, package_type.width, package_type.height
+
+ def _get_stamps_service(self):
+ sudoself = self.sudo()
+ config = StampsConfiguration(integration_id=STAMPS_INTEGRATION_ID,
+ username=sudoself.stamps_username,
+ password=sudoself.stamps_password,
+ wsdl=('testing' if not sudoself.prod_environment else None))
+ return StampsService(configuration=config)
+
+ def _stamps_convert_weight(self, weight):
+ """ weight always expressed in database units (KG/LBS) """
+ weight_uom = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
+ if weight_uom.name == 'kg':
+ weight_in_pounds = weight * 2.20462
+ else:
+ weight_in_pounds = weight
+ return weight_in_pounds
+
+ def _get_stamps_shipping_for_order(self, service, order, date_planned):
+ weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
+ weight = self._stamps_convert_weight(weight)
+
+ if not all((order.warehouse_id.partner_id.zip, order.partner_shipping_id.zip)):
+ raise ValidationError(_('Stamps needs ZIP. From: "%s" To: "%s"',
+ order.warehouse_id.partner_id.zip,
+ order.partner_shipping_id.zip))
+
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat()
+ shipper_partner = self.get_shipper_warehouse(order=order)
+ 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.ServiceType = self.stamps_service_type
+ ret_val.WeightLb = weight
+ ret_val.ContentType = self._stamps_content_type()
+ return ret_val
+
+ def _get_stamps_shipping_multi(self, service, date_planned, order=False, picking=False, package=False):
+ if order:
+ weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
+ elif not package:
+ weight = picking.shipping_weight
+ else:
+ weight = package.shipping_weight or package.weight
+ weight = self._stamps_convert_weight(weight)
+
+ shipper = self.get_shipper_warehouse(order=order, picking=picking)
+ recipient = self.get_recipient(order=order, picking=picking)
+
+ if not all((shipper.zip, recipient.zip)):
+ raise ValidationError(_('Stamps needs ZIP. From: "%s" To: "%s"',
+ shipper.zip,
+ recipient.zip))
+
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat()
+ ret_val.From = self._stamps_address(service, shipper)
+ ret_val.To = self._stamps_address(service, recipient)
+ ret_val.PackageType = self._stamps_package_type(package=package)
+ ret_val.WeightLb = weight
+ ret_val.ContentType = 'Merchandise'
+ return ret_val
+
+ def _stamps_get_addresses_for_picking(self, picking):
+ company = self.get_shipper_company(picking=picking)
+ from_ = self.get_shipper_warehouse(picking=picking)
+ to = self.get_recipient(picking=picking)
+ 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):
+ ret = []
+ company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
+ if not all((from_partner.zip, to_partner.zip)):
+ raise ValidationError(_('Stamps needs ZIP. From: "%s" To: "%s"',
+ from_partner.zip,
+ to_partner.zip))
+
+ picking_packages = picking.package_ids
+ package_carriers = picking_packages.mapped('carrier_id')
+ if package_carriers:
+ # only ship ours
+ picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
+
+ for package in picking_packages:
+ weight = self._stamps_convert_weight(package.shipping_weight)
+ l, w, h = self._stamps_package_dimensions(package=package)
+
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date.today().isoformat()
+ ret_val.From = self._stamps_address(service, from_partner)
+ ret_val.To = self._stamps_address(service, to_partner)
+ ret_val.PackageType = self._stamps_package_type(package=package)
+ ret_val.CubicPricing = self._stamps_package_is_cubic_pricing(package=package)
+ ret_val.Length = l
+ ret_val.Width = w
+ ret_val.Height = h
+ ret_val.ServiceType = self.stamps_service_type
+ ret_val.WeightLb = weight
+ ret_val.ContentType = self._stamps_content_type(package=package)
+ ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
+ if not ret and not package_carriers:
+ weight = self._stamps_convert_weight(picking.shipping_weight)
+ l, w, h = self._stamps_package_dimensions()
+
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date.today().isoformat()
+ ret_val.From = self._stamps_address(service, from_partner)
+ ret_val.To = self._stamps_address(service, to_partner)
+ ret_val.PackageType = self._stamps_package_type()
+ ret_val.CubicPricing = self._stamps_package_is_cubic_pricing()
+ ret_val.Length = l
+ ret_val.Width = w
+ ret_val.Height = h
+ ret_val.ServiceType = self.stamps_service_type
+ ret_val.WeightLb = weight
+ ret_val.ContentType = self._stamps_content_type()
+ ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
+
+ return ret
+
+ def stamps_get_shipping_price_from_so(self, orders):
+ res = self.stamps_get_shipping_price_for_plan(orders, date.today().isoformat())
+ return map(lambda r: r[0] if r else 0.0, res)
+
+ def stamps_get_shipping_price_for_plan(self, orders, date_planned):
+ res = []
+ service = self._get_stamps_service()
+
+ for order in orders:
+ shipping = self._get_stamps_shipping_for_order(service, order, date_planned)
+ rates = service.get_rates(shipping)
+ if rates and len(rates) >= 1:
+ rate = rates[0]
+ price = float(rate.Amount)
+
+ if order.currency_id.name != 'USD':
+ quote_currency = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)
+ price = quote_currency.compute(rate.Amount, order.currency_id)
+
+ delivery_days = rate.DeliverDays
+ if delivery_days.find('-') >= 0:
+ delivery_days = delivery_days.split('-')
+ transit_days = int(delivery_days[-1])
+ else:
+ transit_days = int(delivery_days)
+ date_delivered = None
+ if date_planned and transit_days > 0:
+ date_delivered = self.calculate_date_delivered(date_planned, transit_days)
+
+ res = res + [(price, transit_days, date_delivered)]
+ continue
+ res = res + [(0.0, 0, None)]
+ return res
+
+ def stamps_rate_shipment(self, order):
+ self.ensure_one()
+ result = {
+ 'success': False,
+ 'price': 0.0,
+ 'error_message': _('Error Retrieving Response from Stamps.com'),
+ 'warning_message': False
+ }
+ date_planned = None
+ if self.env.context.get('date_planned'):
+ date_planned = self.env.context.get('date_planned')
+ rate = self.stamps_get_shipping_price_for_plan(order, date_planned)
+ if rate:
+ price, transit_time, date_delivered = rate[0]
+ result.update({
+ 'success': True,
+ 'price': price,
+ 'error_message': False,
+ 'transit_time': transit_time,
+ 'date_delivered': date_delivered,
+ })
+ return result
+ return result
+
+ def _stamps_needs_customs(self, from_partner, to_partner, picking=None):
+ return from_partner.country_id.code != to_partner.country_id.code or \
+ (to_partner.country_id.code == 'US' and to_partner.state_id.code in STAMPS_US_APO_FPO_STATE_CODES)
+
+ def stamps_send_shipping(self, pickings):
+ res = []
+ service = self._get_stamps_service()
+ had_customs = False
+
+ for picking in pickings:
+ package_labels = []
+
+ shippings = self._stamps_get_shippings_for_picking(service, picking)
+ if not shippings:
+ continue
+ company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
+
+ customs = None
+ if self._stamps_needs_customs(from_partner, to_partner, picking=picking):
+ customs = service.create_customs()
+ had_customs = bool(had_customs or customs)
+
+ try:
+ for txn_id, shipping in shippings:
+ rates = service.get_rates(shipping)
+ if rates and len(rates) >= 1:
+ rate = rates[0]
+ shipping.Amount = rate.Amount
+ shipping.ServiceType = rate.ServiceType
+ shipping.DeliverDays = rate.DeliverDays
+ if hasattr(rate, 'DimWeighting'):
+ shipping.DimWeighting = rate.DimWeighting
+ shipping.RateCategory = rate.RateCategory
+ # shipping.ToState = rate.ToState
+ addons = []
+ if self.stamps_addon_sc:
+ add_on = service.create_add_on()
+ add_on.AddOnType = 'US-A-SC'
+ 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()
+ if self.is_amazon(picking=picking):
+ extended_postage_info.bridgeProfileType = 'Amazon MWS'
+
+ 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,
+ 'weight': 0.0,
+ }
+ product_values[product]['quantity'] += quantity
+ product_values[product]['value'] += price * quantity
+ product_values[product]['weight'] += self._stamps_convert_weight(product.weight * quantity) # not rounded so we can sum better....
+
+ # Note that Stamps must match customs weight to the shipment itself
+ # IF we just take the weight from the products, then we're wrong by the difference between shipping weight
+ # IF INSTEAD we want the shipping weight to be accurate, we can ratio the weights of the customs lines.
+ # e.g. shipping_weight = 5, customs_weight (2 items at 1lb and 3lb) = 4
+ # to get to a shipping_weight of 5, the weights we will send will be (5/4) * 1lb and (5/4) * 3lb = 5
+ shipment_weight = shipping.WeightLb
+ customs_total_weight = sum(round(v['weight'], 2) for k, v in product_values.items())
+ if not all((shipment_weight, customs_total_weight)):
+ raise UserError(_('Must have a shipment and customs weight to proceed. (shipment_weight %s, customs_weight %s') % (shipment_weight, customs_total_weight))
+ customs_lines = []
+ new_total_weight = 0.0
+ for product, values in product_values.items():
+ line_weight_ratio = shipment_weight / customs_total_weight
+ 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']) * line_weight_ratio, 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)
+ # still set it, but it should be much closer to the original weight
+ shipping.WeightLb = round(new_total_weight, 2)
+
+ label = service.get_label(shipping,
+ transaction_id=txn_id, image_type=self.stamps_image_type,
+ extended_postage_info=extended_postage_info,
+ customs=customs)
+ package_labels.append((txn_id, label))
+ except WebFault as e:
+ _logger.warning(e)
+ if package_labels:
+ for name, label in package_labels:
+ body = _(u'Cancelling due to error: ', label.TrackingNumber)
+ try:
+ service.remove_label(label.TrackingNumber)
+ except WebFault as e:
+ raise ValidationError(e)
+ else:
+ picking.message_post(body=body)
+ raise ValidationError(_('Error on full shipment. Attempted to cancel any previously shipped.'))
+ raise ValidationError(_('Error on shipment. "%s"', e))
+ else:
+ carrier_price = 0.0
+ tracking_numbers = []
+ for name, label in package_labels:
+ body = _(u'Shipment created into Stamps.com
Tracking Number :
"%s" ', label.TrackingNumber)
+ tracking_numbers.append(label.TrackingNumber)
+ carrier_price += float(label.Rate.Amount)
+ url = label.URL
+
+ url_spaces = url.split(' ')
+ attachments = []
+ for i, url in enumerate(url_spaces, 1):
+ response = urlopen(url)
+ attachment = response.read()
+ # Stamps.com sends labels that set the print rate (print speed) to 8 Inches per Second
+ # this is too fast for international/customs forms that have fine detail
+ # set it to the general minimum of 2IPS
+ if had_customs:
+ attachment = attachment.replace(b'^PR8,8,8\r\n', b'^PR2\r\n')
+ 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)}
+ res = res + [shipping_data]
+ return res
+
+ def stamps_get_tracking_link(self, pickings):
+ res = []
+ for picking in pickings:
+ ref = picking.carrier_tracking_ref
+ res = res + ['https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=%s' % ref]
+ return res
+
+ def stamps_cancel_shipment(self, picking):
+ service = self._get_stamps_service()
+ try:
+ 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.write({'carrier_tracking_ref': '',
+ 'carrier_price': 0.0})
+ except WebFault as e:
+ raise ValidationError(e)
+
+ def stamps_rate_shipment_multi(self, order=None, picking=None, packages=None):
+ if not packages:
+ return self._stamps_rate_shipment_multi_package(order=order, picking=picking)
+ else:
+ rates = []
+ for package in packages:
+ rates += self._stamps_rate_shipment_multi_package(order=order, picking=picking, package=package)
+ return rates
+
+ def _stamps_rate_shipment_multi_package(self, order=None, picking=None, package=None):
+ self.ensure_one()
+ date_planned = fields.Datetime.now()
+ if self.env.context.get('date_planned'):
+ date_planned = self.env.context.get('date_planned')
+ res = []
+ service = self._get_stamps_service()
+ shipping = self._get_stamps_shipping_multi(service, date_planned, order=order, picking=picking, package=package)
+ rates = service.get_rates(shipping)
+ for rate in rates:
+ price = float(rate.Amount)
+ if order:
+ currency = order.currency_id
+ else:
+ currency = picking.sale_id.currency_id if picking.sale_id else picking.company_id.currency_id
+ if currency.name != 'USD':
+ quote_currency = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)
+ price = quote_currency.compute(rate.Amount, currency)
+
+ delivery_days = rate.DeliverDays
+ if delivery_days.find('-') >= 0:
+ delivery_days = delivery_days.split('-')
+ transit_days = int(delivery_days[-1])
+ else:
+ transit_days = int(delivery_days)
+ date_delivered = None
+ if transit_days > 0:
+ date_delivered = self.calculate_date_delivered(date_planned, transit_days)
+ service_code = rate.ServiceType
+ carrier = self.stamps_find_delivery_carrier_for_service(service_code)
+ if carrier:
+ res.append({
+ 'carrier': carrier,
+ 'package': package or self.env['stock.quant.package'].browse(),
+ 'success': True,
+ 'price': price,
+ 'error_message': False,
+ 'warning_message': False,
+ 'transit_days': transit_days,
+ 'date_delivered': date_delivered,
+ 'date_planned': date_planned,
+ 'service_code': service_code,
+ })
+ if not res:
+ res.append({
+ 'success': False,
+ 'price': 0.0,
+ 'error_message': _('No valid rates returned from Stamps.com'),
+ 'warning_message': False
+ })
+ return res
+
+ def stamps_find_delivery_carrier_for_service(self, service_code):
+ if self.stamps_service_type == service_code:
+ return self
+ # arbitrary decision, lets find the same user name
+ carrier = self.search([('stamps_username', '=', self.stamps_username),
+ ('stamps_service_type', '=', service_code)
+ ], limit=1)
+ return carrier
diff --git a/delivery_stamps/views/delivery_stamps_view.xml b/delivery_stamps/views/delivery_stamps_view.xml
new file mode 100644
index 00000000..90dcc20a
--- /dev/null
+++ b/delivery_stamps/views/delivery_stamps_view.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ delivery.carrier.form.provider.stamps
+ delivery.carrier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stamps.stock.package.type.form.delivery
+ stock.package.type
+
+
+
+
+
+
+
+
+