mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
407 lines
13 KiB
Python
407 lines
13 KiB
Python
# -*- 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) |