mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
IMP connector_walmart Important change to authentication methods used by API
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Walmart Connector',
|
'name': 'Walmart Connector',
|
||||||
'version': '11.0.1.1.0',
|
'version': '11.0.1.2.0',
|
||||||
'category': 'Connector',
|
'category': 'Connector',
|
||||||
'depends': [
|
'depends': [
|
||||||
'account',
|
'account',
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
from . import walmart
|
"""python-walmart - Walmart Marketplace API"""
|
||||||
|
|
||||||
|
__version__ = '0.0.6'
|
||||||
|
__author__ = 'Fulfil.IO Inc. <hello@fulfil.io>'
|
||||||
|
__all__ = []
|
||||||
|
|
||||||
|
from .walmart import Walmart # noqa
|
||||||
|
|||||||
21
connector_walmart/components/api/exceptions.py
Normal file
21
connector_walmart/components/api/exceptions.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class BaseException(Exception):
|
||||||
|
"""
|
||||||
|
Base Exception which implements message attr on exceptions
|
||||||
|
Required for: Python 3
|
||||||
|
"""
|
||||||
|
def __init__(self, message=None, *args, **kwargs):
|
||||||
|
self.message = message
|
||||||
|
super(BaseException, self).__init__(
|
||||||
|
self.message, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message or self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class WalmartException(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WalmartAuthenticationError(WalmartException):
|
||||||
|
pass
|
||||||
@@ -30,39 +30,61 @@
|
|||||||
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||||
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
# © 2017 Hibou Corp. - Extended and converted to v3/JSON
|
|
||||||
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import uuid
|
||||||
import time
|
import csv
|
||||||
from uuid import uuid4
|
import io
|
||||||
# from lxml import etree
|
import zipfile
|
||||||
# from lxml.builder import E, ElementMaker
|
|
||||||
from json import dumps, loads
|
|
||||||
|
|
||||||
from Crypto.Hash import SHA256
|
from datetime import datetime
|
||||||
from Crypto.PublicKey import RSA
|
from requests.auth import HTTPBasicAuth
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
from lxml import etree
|
||||||
|
from lxml.builder import E, ElementMaker
|
||||||
|
|
||||||
try:
|
from .exceptions import WalmartAuthenticationError
|
||||||
from urllib.parse import urlencode
|
|
||||||
except ImportError:
|
|
||||||
from urllib import urlencode
|
def epoch_milliseconds(dt):
|
||||||
|
"Walmart accepts timestamps as epoch time in milliseconds"
|
||||||
|
epoch = datetime.utcfromtimestamp(0)
|
||||||
|
return int((dt - epoch).total_seconds() * 1000.0)
|
||||||
|
|
||||||
|
|
||||||
class Walmart(object):
|
class Walmart(object):
|
||||||
|
|
||||||
def __init__(self, consumer_id, channel_type, private_key):
|
def __init__(self, client_id, client_secret):
|
||||||
self.base_url = 'https://marketplace.walmartapis.com/v3/%s'
|
"""To get client_id and client_secret for your Walmart Marketplace
|
||||||
self.consumer_id = consumer_id
|
visit: https://developer.walmart.com/#/generateKey
|
||||||
self.channel_type = channel_type
|
"""
|
||||||
self.private_key = private_key
|
self.client_id = client_id
|
||||||
self.session = requests.Session()
|
self.client_secret = client_secret
|
||||||
self.session.headers['Accept'] = 'application/json'
|
self.token = None
|
||||||
self.session.headers['WM_SVC.NAME'] = 'Walmart Marketplace'
|
self.token_expires_in = None
|
||||||
self.session.headers['WM_CONSUMER.ID'] = self.consumer_id
|
self.base_url = "https://marketplace.walmartapis.com/v3"
|
||||||
self.session.headers['WM_CONSUMER.CHANNEL.TYPE'] = self.channel_type
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers.update({
|
||||||
|
"WM_SVC.NAME": "Walmart Marketplace",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Accept": "application/json",
|
||||||
|
})
|
||||||
|
session.auth = HTTPBasicAuth(self.client_id, self.client_secret)
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
# Get the token required for API requests
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
data = self.send_request(
|
||||||
|
"POST", "{}/token".format(self.base_url),
|
||||||
|
body={
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.token = data["access_token"]
|
||||||
|
self.token_expires_in = data["expires_in"]
|
||||||
|
|
||||||
|
self.session.headers["WM_SEC.ACCESS_TOKEN"] = self.token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def items(self):
|
def items(self):
|
||||||
@@ -80,42 +102,70 @@ class Walmart(object):
|
|||||||
def orders(self):
|
def orders(self):
|
||||||
return Orders(connection=self)
|
return Orders(connection=self)
|
||||||
|
|
||||||
def get_sign(self, url, method, timestamp):
|
@property
|
||||||
return self.sign_data(
|
def report(self):
|
||||||
'\n'.join([self.consumer_id, url, method, timestamp]) + '\n'
|
return Report(connection=self)
|
||||||
)
|
|
||||||
|
|
||||||
def sign_data(self, data):
|
@property
|
||||||
rsakey = RSA.importKey(base64.b64decode(self.private_key))
|
def feed(self):
|
||||||
signer = PKCS1_v1_5.new(rsakey)
|
return Feed(connection=self)
|
||||||
digest = SHA256.new()
|
|
||||||
digest.update(data.encode('utf-8'))
|
|
||||||
sign = signer.sign(digest)
|
|
||||||
return base64.b64encode(sign)
|
|
||||||
|
|
||||||
def get_headers(self, url, method):
|
def send_request(
|
||||||
timestamp = str(int(round(time.time() * 1000)))
|
self, method, url, params=None, body=None, json=None,
|
||||||
|
request_headers=None
|
||||||
|
):
|
||||||
|
# A unique ID which identifies each API call and used to track
|
||||||
|
# and debug issues; use a random generated GUID for this ID
|
||||||
headers = {
|
headers = {
|
||||||
'WM_SEC.AUTH_SIGNATURE': self.get_sign(url, method, timestamp),
|
"WM_QOS.CORRELATION_ID": uuid.uuid4().hex,
|
||||||
'WM_SEC.TIMESTAMP': timestamp,
|
|
||||||
'WM_QOS.CORRELATION_ID': str(uuid4()),
|
|
||||||
}
|
}
|
||||||
if method in ('POST', ):
|
if request_headers:
|
||||||
headers['Content-Type'] = 'application/json'
|
headers.update(request_headers)
|
||||||
return headers
|
|
||||||
|
|
||||||
def send_request(self, method, url, params=None, body=None):
|
response = None
|
||||||
encoded_url = url
|
if method == "GET":
|
||||||
if params:
|
response = self.session.get(url, params=params, headers=headers)
|
||||||
encoded_url += '?%s' % urlencode(params)
|
elif method == "PUT":
|
||||||
headers = self.get_headers(encoded_url, method)
|
response = self.session.put(
|
||||||
|
url, params=params, headers=headers, data=body
|
||||||
|
)
|
||||||
|
elif method == "POST":
|
||||||
|
request_params = {
|
||||||
|
"params": params,
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
if json is not None:
|
||||||
|
request_params["json"] = json
|
||||||
|
else:
|
||||||
|
request_params["data"] = body
|
||||||
|
response = self.session.post(url, **request_params)
|
||||||
|
|
||||||
if method == 'GET':
|
if response is not None:
|
||||||
return loads(self.session.get(url, params=params, headers=headers).text)
|
try:
|
||||||
elif method == 'PUT':
|
response.raise_for_status()
|
||||||
return loads(self.session.put(url, params=params, headers=headers).text)
|
except requests.exceptions.HTTPError:
|
||||||
elif method == 'POST':
|
if response.status_code == 401:
|
||||||
return loads(self.session.post(url, data=body, headers=headers).text)
|
raise WalmartAuthenticationError((
|
||||||
|
"Invalid client_id or client_secret. Please verify "
|
||||||
|
"your credentials from https://developer.walmart."
|
||||||
|
"com/#/generateKey"
|
||||||
|
))
|
||||||
|
elif response.status_code == 400:
|
||||||
|
data = response.json()
|
||||||
|
if data["error"][0]["code"] == \
|
||||||
|
"INVALID_TOKEN.GMP_GATEWAY_API":
|
||||||
|
# Refresh the token as the current token has expired
|
||||||
|
self.authenticate()
|
||||||
|
return self.send_request(
|
||||||
|
method, url, params, body, request_headers
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except ValueError:
|
||||||
|
# In case of reports, there is no JSON response, so return the
|
||||||
|
# content instead which contains the actual report
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
class Resource(object):
|
||||||
@@ -128,24 +178,21 @@ class Resource(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return self.connection.base_url % self.path
|
return "{}/{}".format(self.connection.base_url, self.path)
|
||||||
|
|
||||||
def all(self, **kwargs):
|
def all(self, **kwargs):
|
||||||
return self.connection.send_request(
|
return self.connection.send_request(
|
||||||
method='GET', url=self.url, params=kwargs)
|
method="GET", url=self.url, params=kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
url = self.url + '/%s' % id
|
url = "{}/{}".format(self.url, id)
|
||||||
return self.connection.send_request(method='GET', url=url)
|
return self.connection.send_request(method="GET", url=url)
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
return self.connection.send_request(
|
return self.connection.send_request(
|
||||||
method='PUT', url=self.url, params=kwargs)
|
method="PUT", url=self.url, params=kwargs
|
||||||
|
)
|
||||||
# def bulk_update(self, items):
|
|
||||||
# url = self.connection.base_url % 'feeds?feedType=%s' % self.feedType
|
|
||||||
# return self.connection.send_request(
|
|
||||||
# method='POST', url=url, data=self.get_payload(items))
|
|
||||||
|
|
||||||
|
|
||||||
class Items(Resource):
|
class Items(Resource):
|
||||||
@@ -155,6 +202,14 @@ class Items(Resource):
|
|||||||
|
|
||||||
path = 'items'
|
path = 'items'
|
||||||
|
|
||||||
|
def get_items(self):
|
||||||
|
"Get all the items from the Item Report"
|
||||||
|
response = self.connection.report.all(type="item")
|
||||||
|
zf = zipfile.ZipFile(io.BytesIO(response), "r")
|
||||||
|
product_report = zf.read(zf.infolist()[0]).decode("utf-8")
|
||||||
|
|
||||||
|
return list(csv.DictReader(io.StringIO(product_report)))
|
||||||
|
|
||||||
|
|
||||||
class Inventory(Resource):
|
class Inventory(Resource):
|
||||||
"""
|
"""
|
||||||
@@ -164,6 +219,74 @@ class Inventory(Resource):
|
|||||||
path = 'inventory'
|
path = 'inventory'
|
||||||
feedType = 'inventory'
|
feedType = 'inventory'
|
||||||
|
|
||||||
|
def bulk_update(self, items):
|
||||||
|
"""Updates the inventory for multiple items at once by creating the
|
||||||
|
feed on Walmart.
|
||||||
|
|
||||||
|
:param items: Items for which the inventory needs to be updated in
|
||||||
|
the format of:
|
||||||
|
[{
|
||||||
|
"sku": "XXXXXXXXX",
|
||||||
|
"availability_code": "AC",
|
||||||
|
"quantity": "10",
|
||||||
|
"uom": "EACH",
|
||||||
|
"fulfillment_lag_time": "1",
|
||||||
|
}]
|
||||||
|
"""
|
||||||
|
inventory_data = []
|
||||||
|
for item in items:
|
||||||
|
data = {
|
||||||
|
"sku": item["sku"],
|
||||||
|
"quantity": {
|
||||||
|
"amount": item["quantity"],
|
||||||
|
"unit": item.get("uom", "EACH"),
|
||||||
|
},
|
||||||
|
"fulfillmentLagTime": item.get("fulfillment_lag_time"),
|
||||||
|
}
|
||||||
|
if item.get("availability_code"):
|
||||||
|
data["availabilityCode"] = item["availability_code"]
|
||||||
|
inventory_data.append(data)
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"InventoryHeader": {
|
||||||
|
"version": "1.4",
|
||||||
|
},
|
||||||
|
"Inventory": inventory_data,
|
||||||
|
}
|
||||||
|
return self.connection.feed.create(resource="inventory", content=body)
|
||||||
|
|
||||||
|
def update_inventory(self, sku, quantity):
|
||||||
|
headers = {
|
||||||
|
'Content-Type': "application/xml"
|
||||||
|
}
|
||||||
|
return self.connection.send_request(
|
||||||
|
method='PUT',
|
||||||
|
url=self.url,
|
||||||
|
params={'sku': sku},
|
||||||
|
body=self.get_inventory_payload(sku, quantity),
|
||||||
|
request_headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_inventory_payload(self, sku, quantity):
|
||||||
|
element = ElementMaker(
|
||||||
|
namespace='http://walmart.com/',
|
||||||
|
nsmap={
|
||||||
|
'wm': 'http://walmart.com/',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return etree.tostring(
|
||||||
|
element(
|
||||||
|
'inventory',
|
||||||
|
element('sku', sku),
|
||||||
|
element(
|
||||||
|
'quantity',
|
||||||
|
element('unit', 'EACH'),
|
||||||
|
element('amount', str(quantity)),
|
||||||
|
),
|
||||||
|
element('fulfillmentLagTime', '4'),
|
||||||
|
), xml_declaration=True, encoding='utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
def get_payload(self, items):
|
def get_payload(self, items):
|
||||||
return etree.tostring(
|
return etree.tostring(
|
||||||
E.InventoryFeed(
|
E.InventoryFeed(
|
||||||
@@ -191,55 +314,53 @@ class Prices(Resource):
|
|||||||
feedType = 'price'
|
feedType = 'price'
|
||||||
|
|
||||||
def get_payload(self, items):
|
def get_payload(self, items):
|
||||||
# root = ElementMaker(
|
root = ElementMaker(
|
||||||
# nsmap={'gmp': 'http://walmart.com/'}
|
nsmap={'gmp': 'http://walmart.com/'}
|
||||||
# )
|
)
|
||||||
# return etree.tostring(
|
return etree.tostring(
|
||||||
# root.PriceFeed(
|
root.PriceFeed(
|
||||||
# E.PriceHeader(E('version', '1.5')),
|
E.PriceHeader(E('version', '1.5')),
|
||||||
# *[E.Price(
|
*[E.Price(
|
||||||
# E(
|
E(
|
||||||
# 'itemIdentifier',
|
'itemIdentifier',
|
||||||
# E('sku', item['sku'])
|
E('sku', item['sku'])
|
||||||
# ),
|
),
|
||||||
# E(
|
E(
|
||||||
# 'pricingList',
|
'pricingList',
|
||||||
# E(
|
E(
|
||||||
# 'pricing',
|
'pricing',
|
||||||
# E(
|
E(
|
||||||
# 'currentPrice',
|
'currentPrice',
|
||||||
# E(
|
E(
|
||||||
# 'value',
|
'value',
|
||||||
# **{
|
**{
|
||||||
# 'currency': item['currenctCurrency'],
|
'currency': item['currenctCurrency'],
|
||||||
# 'amount': item['currenctPrice']
|
'amount': item['currenctPrice']
|
||||||
# }
|
}
|
||||||
# )
|
)
|
||||||
# ),
|
),
|
||||||
# E('currentPriceType', item['priceType']),
|
E('currentPriceType', item['priceType']),
|
||||||
# E(
|
E(
|
||||||
# 'comparisonPrice',
|
'comparisonPrice',
|
||||||
# E(
|
E(
|
||||||
# 'value',
|
'value',
|
||||||
# **{
|
**{
|
||||||
# 'currency': item['comparisonCurrency'],
|
'currency': item['comparisonCurrency'],
|
||||||
# 'amount': item['comparisonPrice']
|
'amount': item['comparisonPrice']
|
||||||
# }
|
}
|
||||||
# )
|
)
|
||||||
# ),
|
),
|
||||||
# E(
|
E(
|
||||||
# 'priceDisplayCode',
|
'priceDisplayCode',
|
||||||
# **{
|
**{
|
||||||
# 'submapType': item['displayCode']
|
'submapType': item['displayCode']
|
||||||
# }
|
}
|
||||||
# ),
|
),
|
||||||
# )
|
)
|
||||||
# )
|
)
|
||||||
# ) for item in items]
|
) for item in items]
|
||||||
# ), xml_declaration=True, encoding='utf-8'
|
), xml_declaration=True, encoding='utf-8'
|
||||||
# )
|
)
|
||||||
payload = {}
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class Orders(Resource):
|
class Orders(Resource):
|
||||||
@@ -250,158 +371,165 @@ class Orders(Resource):
|
|||||||
path = 'orders'
|
path = 'orders'
|
||||||
|
|
||||||
def all(self, **kwargs):
|
def all(self, **kwargs):
|
||||||
next_cursor = kwargs.pop('nextCursor', '')
|
try:
|
||||||
return self.connection.send_request(
|
return super(Orders, self).all(**kwargs)
|
||||||
method='GET', url=self.url + next_cursor, params=kwargs)
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
def released(self, **kwargs):
|
# If no orders are there on walmart matching the query
|
||||||
next_cursor = kwargs.pop('nextCursor', '')
|
# filters, it throws 404. In this case return an empty
|
||||||
url = self.url + '/released'
|
# list to make the API consistent
|
||||||
return self.connection.send_request(
|
return {
|
||||||
method='GET', url=url + next_cursor, params=kwargs)
|
"list": {
|
||||||
|
"elements": {
|
||||||
|
"order": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raise
|
||||||
|
|
||||||
def acknowledge(self, id):
|
def acknowledge(self, id):
|
||||||
url = self.url + '/%s/acknowledge' % id
|
url = self.url + '/%s/acknowledge' % id
|
||||||
return self.connection.send_request(method='POST', url=url)
|
return self.send_request(method='POST', url=url)
|
||||||
|
|
||||||
def cancel(self, id, lines):
|
def cancel(self, id, lines):
|
||||||
url = self.url + '/%s/cancel' % id
|
url = self.url + '/%s/cancel' % id
|
||||||
return self.connection.send_request(
|
return self.send_request(
|
||||||
method='POST', url=url, body=self.get_cancel_payload(lines))
|
method='POST', url=url, data=self.get_cancel_payload(lines))
|
||||||
|
|
||||||
def get_cancel_payload(self, lines):
|
def get_cancel_payload(self, lines):
|
||||||
"""
|
element = ElementMaker(
|
||||||
{
|
namespace='http://walmart.com/mp/orders',
|
||||||
"orderCancellation": {
|
nsmap={
|
||||||
"orderLines": {
|
'ns2': 'http://walmart.com/mp/orders',
|
||||||
"orderLine": [
|
'ns3': 'http://walmart.com/'
|
||||||
{
|
|
||||||
"lineNumber": "1",
|
|
||||||
"orderLineStatuses": {
|
|
||||||
"orderLineStatus": [
|
|
||||||
{
|
|
||||||
"status": "Cancelled",
|
|
||||||
"cancellationReason": "CUSTOMER_REQUESTED_SELLER_TO_CANCEL",
|
|
||||||
"statusQuantity": {
|
|
||||||
"unitOfMeasurement": "EA",
|
|
||||||
"amount": "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
return etree.tostring(
|
||||||
:param lines:
|
element(
|
||||||
:return: string
|
'orderCancellation',
|
||||||
"""
|
element(
|
||||||
payload = {
|
'orderLines',
|
||||||
'orderCancellation': {
|
*[element(
|
||||||
'orderLines': [{
|
'orderLine',
|
||||||
'lineNumber': line['number'],
|
element('lineNumber', line),
|
||||||
'orderLineStatuses': {
|
element(
|
||||||
'orderLineStatus': [
|
'orderLineStatuses',
|
||||||
{
|
element(
|
||||||
'status': 'Cancelled',
|
'orderLineStatus',
|
||||||
'cancellationReason': 'CUSTOMER_REQUESTED_SELLER_TO_CANCEL',
|
element('status', 'Cancelled'),
|
||||||
'statusQuantity': {
|
element(
|
||||||
'unitOfMeasurement': 'EA',
|
'cancellationReason', 'CANCEL_BY_SELLER'),
|
||||||
'amount': line['amount'],
|
element(
|
||||||
}
|
'statusQuantity',
|
||||||
}
|
element('unitOfMeasurement', 'EACH'),
|
||||||
]
|
element('amount', '1')
|
||||||
}
|
)
|
||||||
} for line in lines]
|
)
|
||||||
}
|
)
|
||||||
}
|
) for line in lines]
|
||||||
return dumps(payload)
|
)
|
||||||
|
), xml_declaration=True, encoding='utf-8'
|
||||||
|
|
||||||
|
|
||||||
def ship(self, id, lines):
|
|
||||||
url = self.url + '/%s/shipping' % id
|
|
||||||
return self.connection.send_request(
|
|
||||||
method='POST',
|
|
||||||
url=url,
|
|
||||||
body=self.get_ship_payload(lines)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_ship_payload(self, lines):
|
def create_shipment(self, order_id, lines):
|
||||||
"""
|
"""Send shipping updates to Walmart
|
||||||
|
|
||||||
:param lines: list[ dict(number, amount, carrier, methodCode, trackingNumber, trackingUrl) ]
|
:param order_id: Purchase order ID of an order
|
||||||
:return:
|
:param lines: Order lines to be fulfilled in the format:
|
||||||
|
[{
|
||||||
|
"line_number": "123",
|
||||||
|
"uom": "EACH",
|
||||||
|
"quantity": 3,
|
||||||
|
"ship_time": datetime(2019, 04, 04, 12, 00, 00),
|
||||||
|
"other_carrier": None,
|
||||||
|
"carrier": "USPS",
|
||||||
|
"carrier_service": "Standard",
|
||||||
|
"tracking_number": "34567890567890678",
|
||||||
|
"tracking_url": "www.fedex.com",
|
||||||
|
}]
|
||||||
"""
|
"""
|
||||||
"""
|
url = self.url + "/{}/shipping".format(order_id)
|
||||||
{
|
|
||||||
"orderShipment": {
|
order_lines = []
|
||||||
"orderLines": {
|
for line in lines:
|
||||||
"orderLine": [
|
ship_time = line.get("ship_time", "")
|
||||||
{
|
if ship_time:
|
||||||
"lineNumber": "1",
|
ship_time = epoch_milliseconds(ship_time)
|
||||||
"orderLineStatuses": {
|
order_lines.append({
|
||||||
"orderLineStatus": [
|
"lineNumber": line["line_number"],
|
||||||
{
|
"orderLineStatuses": {
|
||||||
|
"orderLineStatus": [{
|
||||||
"status": "Shipped",
|
"status": "Shipped",
|
||||||
"statusQuantity": {
|
"statusQuantity": {
|
||||||
"unitOfMeasurement": "EA",
|
"unitOfMeasurement": line.get("uom", "EACH"),
|
||||||
"amount": "1"
|
"amount": str(int(line["quantity"])),
|
||||||
},
|
},
|
||||||
"trackingInfo": {
|
"trackingInfo": {
|
||||||
"shipDateTime": 1488480443000,
|
"shipDateTime": ship_time,
|
||||||
"carrierName": {
|
"carrierName": {
|
||||||
"otherCarrier": null,
|
"otherCarrier": line.get("other_carrier"),
|
||||||
"carrier": "UPS"
|
"carrier": line["carrier"],
|
||||||
},
|
},
|
||||||
"methodCode": "Express",
|
"methodCode": line.get("carrier_service", ""),
|
||||||
"trackingNumber": "12345",
|
"trackingNumber": line["tracking_number"],
|
||||||
"trackingURL": "www.fedex.com"
|
"trackingURL": line.get("tracking_url", "")
|
||||||
}
|
}
|
||||||
}
|
}],
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:param lines:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
payload = {
|
body = {
|
||||||
"orderShipment": {
|
"orderShipment": {
|
||||||
"orderLines": {
|
"orderLines": {
|
||||||
"orderLine": [
|
"orderLine": order_lines,
|
||||||
{
|
|
||||||
"lineNumber": str(line['number']),
|
|
||||||
"orderLineStatuses": {
|
|
||||||
"orderLineStatus": [
|
|
||||||
{
|
|
||||||
"status": "Shipped",
|
|
||||||
"statusQuantity": {
|
|
||||||
"unitOfMeasurement": "EA",
|
|
||||||
"amount": str(line['amount'])
|
|
||||||
},
|
|
||||||
"trackingInfo": {
|
|
||||||
"shipDateTime": line['shipDateTime'],
|
|
||||||
"carrierName": {
|
|
||||||
"otherCarrier": None,
|
|
||||||
"carrier": line['carrier']
|
|
||||||
},
|
|
||||||
"methodCode": line['methodCode'],
|
|
||||||
"trackingNumber": line['trackingNumber'],
|
|
||||||
"trackingURL": line['trackingUrl']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for line in lines]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return self.connection.send_request(
|
||||||
|
method="POST",
|
||||||
|
url=url,
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
|
||||||
return dumps(payload)
|
|
||||||
|
class Report(Resource):
|
||||||
|
"""
|
||||||
|
Get report
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = 'getReport'
|
||||||
|
|
||||||
|
|
||||||
|
class Feed(Resource):
|
||||||
|
path = "feeds"
|
||||||
|
|
||||||
|
def create(self, resource, content):
|
||||||
|
"""Creates the feed on Walmart for respective resource
|
||||||
|
|
||||||
|
Once you upload the Feed, you can use the Feed ID returned in the
|
||||||
|
response to track the status of the feed and the status of the
|
||||||
|
item within that Feed.
|
||||||
|
|
||||||
|
:param resource: The resource for which the feed needs to be created.
|
||||||
|
:param content: The content needed to create the Feed.
|
||||||
|
"""
|
||||||
|
return self.connection.send_request(
|
||||||
|
method="POST",
|
||||||
|
url=self.url,
|
||||||
|
params={
|
||||||
|
"feedType": resource,
|
||||||
|
},
|
||||||
|
json=content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_status(self, feed_id, offset=0, limit=1000):
|
||||||
|
"Returns the feed and item status for a specified Feed ID"
|
||||||
|
return self.connection.send_request(
|
||||||
|
method="GET",
|
||||||
|
url="{}/{}".format(self.url, feed_id),
|
||||||
|
params={
|
||||||
|
"includeDetails": "true",
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -199,10 +199,10 @@ class SaleOrderAdapter(Component):
|
|||||||
""" Returns the order after ack
|
""" Returns the order after ack
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
_logger.warn('BEFORE ACK ' + str(id))
|
_logger.info('BEFORE ACK ' + str(id))
|
||||||
api_instance = self.api_instance
|
api_instance = self.api_instance
|
||||||
record = api_instance.orders.acknowledge(id)
|
record = api_instance.orders.acknowledge(id)
|
||||||
_logger.warn('AFTER ACK RECORD: ' + str(record))
|
_logger.info('AFTER ACK RECORD: ' + str(record))
|
||||||
if 'order' in record:
|
if 'order' in record:
|
||||||
return record['order']
|
return record['order']
|
||||||
raise RetryableJobError('Acknowledge Order "' + str(id) + '" did not return an order response.')
|
raise RetryableJobError('Acknowledge Order "' + str(id) + '" did not return an order response.')
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ class StockPickingAdapter(Component):
|
|||||||
|
|
||||||
def create(self, id, lines):
|
def create(self, id, lines):
|
||||||
api_instance = self.api_instance
|
api_instance = self.api_instance
|
||||||
_logger.warn('BEFORE SHIPPING %s list: %s' % (str(id), str(lines)))
|
_logger.info('BEFORE SHIPPING %s list: %s' % (str(id), str(lines)))
|
||||||
record = api_instance.orders.ship(id, lines)
|
record = api_instance.orders.create_shipment(id, lines)
|
||||||
_logger.warn('AFTER SHIPPING RECORD: ' + str(record))
|
_logger.info('AFTER SHIPPING RECORD: ' + str(record))
|
||||||
if 'order' in record:
|
if 'order' in record:
|
||||||
return record['order']
|
return record['order']
|
||||||
raise RetryableJobError('Shipping Order %s did not return an order response. (lines: %s)' % (str(id), str(lines)))
|
raise RetryableJobError('Shipping Order %s did not return an order response. (lines: %s)' % (str(id), str(lines)))
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ class WalmartPickingExporter(Component):
|
|||||||
"""
|
"""
|
||||||
Normalizes picking line data into the format to export to Walmart.
|
Normalizes picking line data into the format to export to Walmart.
|
||||||
:param binding: walmart.stock.picking
|
:param binding: walmart.stock.picking
|
||||||
:return: list[ dict(number, amount, carrier, methodCode, trackingNumber, trackingUrl=None) ]
|
:return: list[ dict(line_number, uom="EACH", quantity, ship_time, carrier, carrier_service, tracking_number, tracking_url=None) ]
|
||||||
"""
|
"""
|
||||||
ship_date = binding.date_done
|
ship_date = binding.date_done
|
||||||
# in ms
|
# in ms
|
||||||
ship_date_time = int(fields.Datetime.from_string(ship_date).strftime('%s')) * 1000
|
ship_date_time = fields.Datetime.from_string(ship_date)
|
||||||
lines = []
|
lines = []
|
||||||
for line in binding.move_lines:
|
for line in binding.move_lines:
|
||||||
sale_line = line.sale_line_id
|
sale_line = line.sale_line_id
|
||||||
@@ -43,19 +43,17 @@ class WalmartPickingExporter(Component):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
number = walmart_sale_line.walmart_number
|
number = walmart_sale_line.walmart_number
|
||||||
amount = 1 if line.product_qty > 0 else 0
|
amount = 1 if line.product_qty > 0 else 0 # potentially because of EACH?
|
||||||
carrier = binding.carrier_id.walmart_carrier_code
|
carrier = binding.carrier_id.walmart_carrier_code
|
||||||
methodCode = binding.walmart_order_id.shipping_method_code
|
carrier_service = binding.walmart_order_id.shipping_method_code
|
||||||
trackingNumber = binding.carrier_tracking_ref
|
tracking_number = binding.carrier_tracking_ref
|
||||||
trackingUrl = None
|
|
||||||
lines.append(dict(
|
lines.append(dict(
|
||||||
shipDateTime=ship_date_time,
|
line_number=number,
|
||||||
number=number,
|
quantity=amount,
|
||||||
amount=amount,
|
ship_time=ship_date_time,
|
||||||
carrier=carrier,
|
carrier=carrier,
|
||||||
methodCode=methodCode,
|
carrier_service=carrier_service,
|
||||||
trackingNumber=trackingNumber,
|
tracking_number=tracking_number,
|
||||||
trackingUrl=trackingUrl,
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -20,21 +20,8 @@ class WalmartBackend(models.Model):
|
|||||||
_inherit = 'connector.backend'
|
_inherit = 'connector.backend'
|
||||||
|
|
||||||
name = fields.Char(string='Name')
|
name = fields.Char(string='Name')
|
||||||
consumer_id = fields.Char(
|
client_id = fields.Char(string='Client ID')
|
||||||
string='Consumer ID',
|
client_secret = fields.Char(string='Client Secret')
|
||||||
required=True,
|
|
||||||
help='Walmart Consumer ID',
|
|
||||||
)
|
|
||||||
channel_type = fields.Char(
|
|
||||||
string='Channel Type',
|
|
||||||
required=True,
|
|
||||||
help='Walmart Channel Type',
|
|
||||||
)
|
|
||||||
private_key = fields.Char(
|
|
||||||
string='Private Key',
|
|
||||||
required=True,
|
|
||||||
help='Walmart Private Key'
|
|
||||||
)
|
|
||||||
warehouse_id = fields.Many2one(
|
warehouse_id = fields.Many2one(
|
||||||
comodel_name='stock.warehouse',
|
comodel_name='stock.warehouse',
|
||||||
string='Warehouse',
|
string='Warehouse',
|
||||||
@@ -89,7 +76,7 @@ class WalmartBackend(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def work_on(self, model_name, **kwargs):
|
def work_on(self, model_name, **kwargs):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
walmart_api = Walmart(self.consumer_id, self.channel_type, self.private_key)
|
walmart_api = Walmart(self.client_id, self.client_secret)
|
||||||
_super = super(WalmartBackend, self)
|
_super = super(WalmartBackend, self)
|
||||||
with _super.work_on(model_name, walmart_api=walmart_api, **kwargs) as work:
|
with _super.work_on(model_name, walmart_api=walmart_api, **kwargs) as work:
|
||||||
yield work
|
yield work
|
||||||
@@ -98,9 +85,8 @@ class WalmartBackend(models.Model):
|
|||||||
def _scheduler_import_sale_orders(self):
|
def _scheduler_import_sale_orders(self):
|
||||||
# potential hook for customization (e.g. pad from date or provide its own)
|
# potential hook for customization (e.g. pad from date or provide its own)
|
||||||
backends = self.search([
|
backends = self.search([
|
||||||
('consumer_id', '!=', False),
|
('client_id', '!=', False),
|
||||||
('channel_type', '!=', False),
|
('client_secret', '!=', False),
|
||||||
('private_key', '!=', False),
|
|
||||||
('import_orders_from_date', '!=', False),
|
('import_orders_from_date', '!=', False),
|
||||||
])
|
])
|
||||||
return backends.import_sale_orders()
|
return backends.import_sale_orders()
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
<notebook>
|
<notebook>
|
||||||
<page string="API" name="api">
|
<page string="API" name="api">
|
||||||
<group colspan="4" col="4">
|
<group colspan="4" col="4">
|
||||||
<field name="consumer_id"/>
|
<field name="client_id"/>
|
||||||
<field name="channel_type"/>
|
<field name="client_secret" password="1"/>
|
||||||
<field name="private_key" password="1"/>
|
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
|
|||||||
Reference in New Issue
Block a user