Update shipping UPS API to OAuth
UPS has implemented an OAuth 2.0 security model for all APIs and deprecate the existing Access Key-based authorisation. See https://developer.ups.com/oauth-developer-guide
This commit is contained in:
1 parent
7e7fd51dd1
commit
ed30d063e7
8 files changed
+59
-51
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Update to OAuth
|
||||
|
||||
Version 6.8.0 - 2023-05-01
|
||||
--------------------------
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from oauthlib.oauth2 import BackendApplicationClient
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
from trytond.model import (
|
||||
MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
|
||||
from trytond.pool import PoolMeta
|
||||
@@ -9,11 +13,11 @@ from trytond.pyson import Eval
|
||||
class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
'UPS Credential'
|
||||
__name__ = 'carrier.credential.ups'
|
||||
_rec_name = 'account_number'
|
||||
|
||||
company = fields.Many2One('company.company', 'Company')
|
||||
user_id = fields.Char('User ID', required=True)
|
||||
password = fields.Char('Password', required=True)
|
||||
license = fields.Char('License', required=True)
|
||||
client_id = fields.Char("Client ID", required=True)
|
||||
client_secret = fields.Char("Client Secret", required=True)
|
||||
account_number = fields.Char('Account Number', required=True)
|
||||
use_metric = fields.Boolean('Use Metric')
|
||||
server = fields.Selection([
|
||||
@@ -21,6 +25,16 @@ class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
('production', 'Production'),
|
||||
], 'Server')
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module):
|
||||
table_h = cls.__table_handler__(module)
|
||||
super().__register__(module)
|
||||
|
||||
# Migration from 6.8: switch to OAuth
|
||||
table_h.drop_column('user_id')
|
||||
table_h.drop_column('password')
|
||||
table_h.drop_column('license')
|
||||
|
||||
@classmethod
|
||||
def default_use_metric(cls):
|
||||
return True
|
||||
@@ -29,6 +43,24 @@ class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
def default_server(cls):
|
||||
return 'testing'
|
||||
|
||||
def get_token(self):
|
||||
if self.server == 'production':
|
||||
url = 'https://onlinetools.ups.com/security/v1/oauth/token'
|
||||
else:
|
||||
url = 'https://wwwcie.ups.com/security/v1/oauth/token'
|
||||
client = BackendApplicationClient(client_id=self.client_id)
|
||||
oauth = OAuth2Session(client=client)
|
||||
token = oauth.fetch_token(
|
||||
token_url=url, client_id=self.client_id,
|
||||
client_secret=self.client_secret)
|
||||
return token['access_token']
|
||||
|
||||
def get_shipment_url(self):
|
||||
if self.server == 'production':
|
||||
return 'https://onlinetools.ups.com/api/shipments/v1/ship'
|
||||
else:
|
||||
return 'https://wwwcie.ups.com/api/shipments/v1/ship'
|
||||
|
||||
|
||||
class Carrier(metaclass=PoolMeta):
|
||||
__name__ = 'carrier'
|
||||
|
||||
@@ -2,9 +2,9 @@ Stock Package Shipping UPS Module
|
||||
#################################
|
||||
|
||||
The Stock Package Shipping UPS module allows you to generate the UPS labels per
|
||||
package using the UPS webservices.
|
||||
package using the UPS API.
|
||||
|
||||
The credential for the webservice is specified by the following fields:
|
||||
The credential for the API is specified by the following fields:
|
||||
|
||||
UPS Carrier Credential
|
||||
**********************
|
||||
@@ -14,15 +14,13 @@ The credentials used to connect to the UPS API will be automatically retrieved
|
||||
based on the company of the Shipment.
|
||||
|
||||
- *Company*: The company for which those credentials are valid
|
||||
- *User ID*: The User ID provided by UPS.
|
||||
- *Password*: The Password used to access the MyUPS Portal.
|
||||
- *Client ID*: The Client ID of the App.
|
||||
- *Client Secret*: The Client Secret of the App.
|
||||
- *Account Number*: The account number UPS assigned to you when you requested the
|
||||
API credentials (also known as the Shipper Number).
|
||||
- *License*: The License number UPS assigned to you when you requested the API
|
||||
credentials
|
||||
- *Server*: Are those credentials used for accessing the Testing or the
|
||||
Production server of UPS.
|
||||
- *Use Metric*: Use Metric units when communicating with the UPS webservice.
|
||||
- *Use Metric*: Use Metric units when communicating with the UPS API.
|
||||
|
||||
Carrier
|
||||
*******
|
||||
|
||||
@@ -41,7 +41,7 @@ else:
|
||||
download_url = 'http://downloads.tryton.org/%s.%s/' % (
|
||||
major_version, minor_version)
|
||||
|
||||
requires = ['requests']
|
||||
requires = ['requests', 'requests-oauthlib', 'oauthlib']
|
||||
for dep in info.get('depends', []):
|
||||
if not re.match(r'(ir|res)(\W|$)', dep):
|
||||
requires.append(get_require_version('trytond_%s' % dep))
|
||||
|
||||
@@ -21,10 +21,6 @@ from trytond.wizard import StateAction, StateTransition, Wizard
|
||||
|
||||
from .exceptions import UPSError
|
||||
|
||||
SERVER_URLS = {
|
||||
'testing': 'https://wwwcie.ups.com/json/Ship',
|
||||
'production': 'https://onlinetools.ups.com/json/Ship',
|
||||
}
|
||||
TRACKING_URL = 'https://www.ups.com/track'
|
||||
TIMEOUT = config.getfloat(
|
||||
'stock_package_shipping_ups', 'requests_timeout', default=300)
|
||||
@@ -178,15 +174,20 @@ class CreateShippingUPS(Wizard):
|
||||
carrier = shipment.carrier
|
||||
packages = shipment.root_packages
|
||||
shipment_request = self.get_request(shipment, packages, credential)
|
||||
api_url = config.get('stock_package_shipping_ups', credential.server,
|
||||
default=SERVER_URLS[credential.server])
|
||||
token = credential.get_token()
|
||||
api_url = credential.get_shipment_url()
|
||||
headers = {
|
||||
'transactionSrc': "Tryton",
|
||||
'Authorization': f"Bearer {token}",
|
||||
}
|
||||
nb_tries, response = 0, None
|
||||
error_message = ''
|
||||
try:
|
||||
while nb_tries < 5 and response is None:
|
||||
try:
|
||||
req = requests.post(
|
||||
api_url, json=shipment_request, timeout=TIMEOUT)
|
||||
api_url, json=shipment_request, headers=headers,
|
||||
timeout=TIMEOUT)
|
||||
except ssl.SSLError as e:
|
||||
error_message = e.reason
|
||||
nb_tries += 1
|
||||
@@ -201,15 +202,6 @@ class CreateShippingUPS(Wizard):
|
||||
gettext('stock_package_shipping_ups.msg_ups_webservice_error',
|
||||
message=error_message))
|
||||
|
||||
if 'Fault' in response:
|
||||
error = response['Fault']['detail']['Errors']
|
||||
message = '%s\n\n%s - %s' % (response['Fault']['faultstring'],
|
||||
error['ErrorDetail']['PrimaryErrorCode']['Code'],
|
||||
error['ErrorDetail']['PrimaryErrorCode']['Description'])
|
||||
raise UPSError(
|
||||
gettext('stock_package_shipping_ups.msg_ups_webservice_error',
|
||||
message=message))
|
||||
|
||||
shipment_response = response['ShipmentResponse']
|
||||
response_status = shipment_response['Response']['ResponseStatus']
|
||||
if response_status['Code'] != '1':
|
||||
@@ -251,17 +243,6 @@ class CreateShippingUPS(Wizard):
|
||||
if credential.match(credential_pattern):
|
||||
return credential
|
||||
|
||||
def get_security(self, credential):
|
||||
return {
|
||||
'UsernameToken': {
|
||||
'Username': credential.user_id,
|
||||
'Password': credential.password,
|
||||
},
|
||||
'ServiceAccessToken': {
|
||||
'AccessLicenseNumber': credential.license,
|
||||
},
|
||||
}
|
||||
|
||||
def get_request_container(self, shipment):
|
||||
return {
|
||||
'RequestOption': 'validate',
|
||||
@@ -396,7 +377,6 @@ class CreateShippingUPS(Wizard):
|
||||
for pkg in packages:
|
||||
pkg['ShipmentServiceOptions'] = options
|
||||
return {
|
||||
'UPSSecurity': self.get_security(credential),
|
||||
'ShipmentRequest': {
|
||||
'Request': self.get_request_container(shipment),
|
||||
'Shipment': {
|
||||
|
||||
@@ -155,9 +155,8 @@ Create a UPS Carrier and the related credential::
|
||||
|
||||
>>> credential = UPSCredential()
|
||||
>>> credential.company = company
|
||||
>>> credential.user_id = os.getenv('UPS_USER_ID')
|
||||
>>> credential.password = os.getenv('UPS_PASSWORD')
|
||||
>>> credential.license = os.getenv('UPS_LICENSE')
|
||||
>>> credential.client_id = os.getenv('UPS_CLIENT_ID')
|
||||
>>> credential.client_secret = os.getenv('UPS_CLIENT_SECRET')
|
||||
>>> credential.account_number = os.getenv('UPS_ACCOUNT_NUMBER')
|
||||
>>> credential.server = 'testing'
|
||||
>>> credential.save()
|
||||
|
||||
@@ -7,15 +7,13 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
|
||||
<label name="account_number"/>
|
||||
<field name="account_number"/>
|
||||
<label name="server"/>
|
||||
<field name="server"/>
|
||||
<separator string="Credential Information" colspan="4" id="credential_info"/>
|
||||
<label name="user_id"/>
|
||||
<field name="user_id"/>
|
||||
<label name="password"/>
|
||||
<field name="password" widget="password"/>
|
||||
<label name="license"/>
|
||||
<field name="license"/>
|
||||
<label name="account_number"/>
|
||||
<field name="account_number"/>
|
||||
<label name="client_id"/>
|
||||
<field name="client_id" widget="password"/>
|
||||
<label name="client_secret"/>
|
||||
<field name="client_secret" widget="password"/>
|
||||
</form>
|
||||
@@ -3,6 +3,6 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="sequence">
|
||||
<field name="company" expand="1"/>
|
||||
<field name="user_id" expand="2"/>
|
||||
<field name="account_number" expand="2"/>
|
||||
<field name="server"/>
|
||||
</tree>
|
||||
Reference in new issue
Block a user