mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit of connector_walmart for Odoo 11.0 (using beta version of connector_ecommerce)
This commit is contained in:
407
connector_walmart/components/api/walmart.py
Normal file
407
connector_walmart/components/api/walmart.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# BSD License
|
||||
#
|
||||
# Copyright (c) 2016, Fulfil.IO Inc.
|
||||
# All 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.
|
||||
#
|
||||
# * Neither the name of Fulfil nor the names of its
|
||||
# contributors may 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 HOLDER 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.
|
||||
|
||||
# © 2017 Hibou Corp. - Extended and converted to v3/JSON
|
||||
|
||||
|
||||
import requests
|
||||
import base64
|
||||
import time
|
||||
from uuid import uuid4
|
||||
# from lxml import etree
|
||||
# from lxml.builder import E, ElementMaker
|
||||
from json import dumps, loads
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
class Walmart(object):
|
||||
|
||||
def __init__(self, consumer_id, channel_type, private_key):
|
||||
self.base_url = 'https://marketplace.walmartapis.com/v3/%s'
|
||||
self.consumer_id = consumer_id
|
||||
self.channel_type = channel_type
|
||||
self.private_key = private_key
|
||||
self.session = requests.Session()
|
||||
self.session.headers['Accept'] = 'application/json'
|
||||
self.session.headers['WM_SVC.NAME'] = 'Walmart Marketplace'
|
||||
self.session.headers['WM_CONSUMER.ID'] = self.consumer_id
|
||||
self.session.headers['WM_CONSUMER.CHANNEL.TYPE'] = self.channel_type
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return Items(connection=self)
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
return Inventory(connection=self)
|
||||
|
||||
@property
|
||||
def prices(self):
|
||||
return Prices(connection=self)
|
||||
|
||||
@property
|
||||
def orders(self):
|
||||
return Orders(connection=self)
|
||||
|
||||
def get_sign(self, url, method, timestamp):
|
||||
return self.sign_data(
|
||||
'\n'.join([self.consumer_id, url, method, timestamp]) + '\n'
|
||||
)
|
||||
|
||||
def sign_data(self, data):
|
||||
rsakey = RSA.importKey(base64.b64decode(self.private_key))
|
||||
signer = PKCS1_v1_5.new(rsakey)
|
||||
digest = SHA256.new()
|
||||
digest.update(data.encode('utf-8'))
|
||||
sign = signer.sign(digest)
|
||||
return base64.b64encode(sign)
|
||||
|
||||
def get_headers(self, url, method):
|
||||
timestamp = str(int(round(time.time() * 1000)))
|
||||
headers = {
|
||||
'WM_SEC.AUTH_SIGNATURE': self.get_sign(url, method, timestamp),
|
||||
'WM_SEC.TIMESTAMP': timestamp,
|
||||
'WM_QOS.CORRELATION_ID': str(uuid4()),
|
||||
}
|
||||
if method in ('POST', ):
|
||||
headers['Content-Type'] = 'application/json'
|
||||
return headers
|
||||
|
||||
def send_request(self, method, url, params=None, body=None):
|
||||
encoded_url = url
|
||||
if params:
|
||||
encoded_url += '?%s' % urlencode(params)
|
||||
headers = self.get_headers(encoded_url, method)
|
||||
|
||||
if method == 'GET':
|
||||
return loads(self.session.get(url, params=params, headers=headers).text)
|
||||
elif method == 'PUT':
|
||||
return loads(self.session.put(url, params=params, headers=headers).text)
|
||||
elif method == 'POST':
|
||||
return loads(self.session.post(url, data=body, headers=headers).text)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A base class for all Resources to extend
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.connection.base_url % self.path
|
||||
|
||||
def all(self, **kwargs):
|
||||
return self.connection.send_request(
|
||||
method='GET', url=self.url, params=kwargs)
|
||||
|
||||
def get(self, id):
|
||||
url = self.url + '/%s' % id
|
||||
return self.connection.send_request(method='GET', url=url)
|
||||
|
||||
def update(self, **kwargs):
|
||||
return self.connection.send_request(
|
||||
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):
|
||||
"""
|
||||
Get all items
|
||||
"""
|
||||
|
||||
path = 'items'
|
||||
|
||||
|
||||
class Inventory(Resource):
|
||||
"""
|
||||
Retreives inventory of an item
|
||||
"""
|
||||
|
||||
path = 'inventory'
|
||||
feedType = 'inventory'
|
||||
|
||||
def get_payload(self, items):
|
||||
return etree.tostring(
|
||||
E.InventoryFeed(
|
||||
E.InventoryHeader(E('version', '1.4')),
|
||||
*[E(
|
||||
'inventory',
|
||||
E('sku', item['sku']),
|
||||
E(
|
||||
'quantity',
|
||||
E('unit', 'EACH'),
|
||||
E('amount', item['quantity']),
|
||||
)
|
||||
) for item in items],
|
||||
xmlns='http://walmart.com/'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Prices(Resource):
|
||||
"""
|
||||
Retreives price of an item
|
||||
"""
|
||||
|
||||
path = 'prices'
|
||||
feedType = 'price'
|
||||
|
||||
def get_payload(self, items):
|
||||
# root = ElementMaker(
|
||||
# nsmap={'gmp': 'http://walmart.com/'}
|
||||
# )
|
||||
# return etree.tostring(
|
||||
# root.PriceFeed(
|
||||
# E.PriceHeader(E('version', '1.5')),
|
||||
# *[E.Price(
|
||||
# E(
|
||||
# 'itemIdentifier',
|
||||
# E('sku', item['sku'])
|
||||
# ),
|
||||
# E(
|
||||
# 'pricingList',
|
||||
# E(
|
||||
# 'pricing',
|
||||
# E(
|
||||
# 'currentPrice',
|
||||
# E(
|
||||
# 'value',
|
||||
# **{
|
||||
# 'currency': item['currenctCurrency'],
|
||||
# 'amount': item['currenctPrice']
|
||||
# }
|
||||
# )
|
||||
# ),
|
||||
# E('currentPriceType', item['priceType']),
|
||||
# E(
|
||||
# 'comparisonPrice',
|
||||
# E(
|
||||
# 'value',
|
||||
# **{
|
||||
# 'currency': item['comparisonCurrency'],
|
||||
# 'amount': item['comparisonPrice']
|
||||
# }
|
||||
# )
|
||||
# ),
|
||||
# E(
|
||||
# 'priceDisplayCode',
|
||||
# **{
|
||||
# 'submapType': item['displayCode']
|
||||
# }
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# ) for item in items]
|
||||
# ), xml_declaration=True, encoding='utf-8'
|
||||
# )
|
||||
payload = {}
|
||||
return
|
||||
|
||||
|
||||
class Orders(Resource):
|
||||
"""
|
||||
Retrieves Order details
|
||||
"""
|
||||
|
||||
path = 'orders'
|
||||
|
||||
def all(self, **kwargs):
|
||||
next_cursor = kwargs.pop('nextCursor', '')
|
||||
return self.connection.send_request(
|
||||
method='GET', url=self.url + next_cursor, params=kwargs)
|
||||
|
||||
def released(self, **kwargs):
|
||||
next_cursor = kwargs.pop('nextCursor', '')
|
||||
url = self.url + '/released'
|
||||
return self.connection.send_request(
|
||||
method='GET', url=url + next_cursor, params=kwargs)
|
||||
|
||||
def acknowledge(self, id):
|
||||
url = self.url + '/%s/acknowledge' % id
|
||||
return self.connection.send_request(method='POST', url=url)
|
||||
|
||||
def cancel(self, id, lines):
|
||||
url = self.url + '/%s/cancel' % id
|
||||
return self.connection.send_request(
|
||||
method='POST', url=url, body=self.get_cancel_payload(lines))
|
||||
|
||||
def get_cancel_payload(self, lines):
|
||||
"""
|
||||
{
|
||||
"orderCancellation": {
|
||||
"orderLines": {
|
||||
"orderLine": [
|
||||
{
|
||||
"lineNumber": "1",
|
||||
"orderLineStatuses": {
|
||||
"orderLineStatus": [
|
||||
{
|
||||
"status": "Cancelled",
|
||||
"cancellationReason": "CUSTOMER_REQUESTED_SELLER_TO_CANCEL",
|
||||
"statusQuantity": {
|
||||
"unitOfMeasurement": "EA",
|
||||
"amount": "1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
:param lines:
|
||||
:return: string
|
||||
"""
|
||||
payload = {
|
||||
'orderCancellation': {
|
||||
'orderLines': [{
|
||||
'lineNumber': line['number'],
|
||||
'orderLineStatuses': {
|
||||
'orderLineStatus': [
|
||||
{
|
||||
'status': 'Cancelled',
|
||||
'cancellationReason': 'CUSTOMER_REQUESTED_SELLER_TO_CANCEL',
|
||||
'statusQuantity': {
|
||||
'unitOfMeasurement': 'EA',
|
||||
'amount': line['amount'],
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
} for line in lines]
|
||||
}
|
||||
}
|
||||
return dumps(payload)
|
||||
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
|
||||
:param lines: list[ dict(number, amount, carrier, methodCode, trackingNumber, trackingUrl) ]
|
||||
:return:
|
||||
"""
|
||||
"""
|
||||
{
|
||||
"orderShipment": {
|
||||
"orderLines": {
|
||||
"orderLine": [
|
||||
{
|
||||
"lineNumber": "1",
|
||||
"orderLineStatuses": {
|
||||
"orderLineStatus": [
|
||||
{
|
||||
"status": "Shipped",
|
||||
"statusQuantity": {
|
||||
"unitOfMeasurement": "EA",
|
||||
"amount": "1"
|
||||
},
|
||||
"trackingInfo": {
|
||||
"shipDateTime": 1488480443000,
|
||||
"carrierName": {
|
||||
"otherCarrier": null,
|
||||
"carrier": "UPS"
|
||||
},
|
||||
"methodCode": "Express",
|
||||
"trackingNumber": "12345",
|
||||
"trackingURL": "www.fedex.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
:param lines:
|
||||
:return:
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"orderShipment": {
|
||||
"orderLines": {
|
||||
"orderLine": [
|
||||
{
|
||||
"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 dumps(payload)
|
||||
Reference in New Issue
Block a user