Merge branch 'mig/16.0/delivery_stamps' into '16.0-test'

mig/16.0/delivery_stamps into 16.0-test

See merge request hibou-io/hibou-odoo/suite!1595
This commit is contained in:
Hibou Bot
2022-10-31 20:17:44 +00:00
13 changed files with 13580 additions and 0 deletions

View File

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

View File

@@ -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,
}

241
delivery_stamps/i18n/es.po Normal file
View File

@@ -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 <br/> <b>Tracking Number : <br/> \"%s\" "
"</b>"
msgstr "El envío ha sido creado en Stamps.com <br/> <b> Número de Seguimiento : <br/> \"%s\" </b>"
#. 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"

View File

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

View File

@@ -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.

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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 <br/> <b>Tracking Number : <br/> "%s" </b>', 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

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_delivery_carrier_form_with_provider_stamps" model="ir.ui.view">
<field name="name">delivery.carrier.form.provider.stamps</field>
<field name="model">delivery.carrier</field>
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='destination']" position='before'>
<page string="Stamps.com Configuration" attrs="{'invisible': [('delivery_type', '!=', 'stamps')]}">
<group>
<group>
<field name="stamps_username" attrs="{'required': [('delivery_type', '=', 'stamps')]}" />
<field name="stamps_password" attrs="{'required': [('delivery_type', '=', 'stamps')]}" password="True"/>
</group>
<group>
<field name="stamps_service_type" 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_addon_sc"/>
<field name="stamps_addon_dc"/>
<field name="stamps_addon_hp"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
<record id="stock_package_type_form_delivery" model="ir.ui.view">
<field name="name">stamps.stock.package.type.form.delivery</field>
<field name="model">stock.package.type</field>
<field name="inherit_id" ref="delivery.stock_package_type_form_delivery"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='barcode']" position='before'>
<field name="stamps_cubic_pricing"/>
</xpath>
</field>
</record>
</odoo>