Merge branch 'develop' into fifo-slot-by-wh
This commit is contained in:
commit
cd728fc9e7
@ -1,524 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import csv
|
||||
import math
|
||||
import time
|
||||
from io import StringIO
|
||||
|
||||
import dateutil
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
|
||||
|
||||
|
||||
#Get and Create Products
|
||||
def get_products_details():
|
||||
products = get_products_instance()
|
||||
reports = get_reports_instance()
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
market_place_list = return_as_list(mws_settings.market_place_id)
|
||||
|
||||
for marketplace in market_place_list:
|
||||
report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list)
|
||||
|
||||
if report_id:
|
||||
listings_response = reports.get_report(report_id=report_id)
|
||||
|
||||
#Get ASIN Codes
|
||||
string_io = StringIO(frappe.safe_decode(listings_response.original))
|
||||
csv_rows = list(csv.reader(string_io, delimiter='\t'))
|
||||
asin_list = list(set([row[1] for row in csv_rows[1:]]))
|
||||
#break into chunks of 10
|
||||
asin_chunked_list = list(chunks(asin_list, 10))
|
||||
|
||||
#Map ASIN Codes to SKUs
|
||||
sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]]
|
||||
|
||||
#Fetch Products List from ASIN
|
||||
for asin_list in asin_chunked_list:
|
||||
products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace,
|
||||
asins=asin_list)
|
||||
|
||||
matching_products_list = products_response.parsed
|
||||
for product in matching_products_list:
|
||||
skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN]
|
||||
for sku in skus:
|
||||
create_item_code(product, sku)
|
||||
|
||||
def get_products_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
products = mws.Products(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region = mws_settings.region,
|
||||
domain = mws_settings.domain
|
||||
)
|
||||
|
||||
return products
|
||||
|
||||
def get_reports_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
reports = mws.Reports(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region = mws_settings.region,
|
||||
domain = mws_settings.domain
|
||||
)
|
||||
|
||||
return reports
|
||||
|
||||
#returns list as expected by amazon API
|
||||
def return_as_list(input_value):
|
||||
if isinstance(input_value, list):
|
||||
return input_value
|
||||
else:
|
||||
return [input_value]
|
||||
|
||||
#function to chunk product data
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
|
||||
def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None):
|
||||
reports = get_reports_instance()
|
||||
report_response = reports.request_report(report_type=report_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
marketplaceids=marketplaceids)
|
||||
|
||||
report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"]
|
||||
generated_report_id = None
|
||||
#poll to get generated report
|
||||
for x in range(1,10):
|
||||
report_request_list_response = reports.get_report_request_list(requestids=[report_request_id])
|
||||
report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"]
|
||||
|
||||
if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_":
|
||||
#add time delay to wait for amazon to generate report
|
||||
time.sleep(15)
|
||||
continue
|
||||
elif report_status == "_CANCELLED_":
|
||||
break
|
||||
elif report_status == "_DONE_NO_DATA_":
|
||||
break
|
||||
elif report_status == "_DONE_":
|
||||
generated_report_id = report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"]
|
||||
break
|
||||
return generated_report_id
|
||||
|
||||
def call_mws_method(mws_method, *args, **kwargs):
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
max_retries = mws_settings.max_retry_limit
|
||||
|
||||
for x in range(0, max_retries):
|
||||
try:
|
||||
response = mws_method(*args, **kwargs)
|
||||
return response
|
||||
except Exception as e:
|
||||
delay = math.pow(4, x) * 125
|
||||
frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
|
||||
time.sleep(delay)
|
||||
continue
|
||||
|
||||
mws_settings.enable_sync = 0
|
||||
mws_settings.save()
|
||||
|
||||
frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded"))
|
||||
|
||||
def create_item_code(amazon_item_json, sku):
|
||||
if frappe.db.get_value("Item", sku):
|
||||
return
|
||||
|
||||
item = frappe.new_doc("Item")
|
||||
|
||||
new_manufacturer = create_manufacturer(amazon_item_json)
|
||||
new_brand = create_brand(amazon_item_json)
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
item.item_code = sku
|
||||
item.amazon_item_code = amazon_item_json.ASIN
|
||||
item.item_group = mws_settings.item_group
|
||||
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
||||
item.brand = new_brand
|
||||
item.manufacturer = new_manufacturer
|
||||
|
||||
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
|
||||
|
||||
temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup
|
||||
|
||||
item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group})
|
||||
|
||||
if not item_group:
|
||||
igroup = frappe.new_doc("Item Group")
|
||||
igroup.item_group_name = temp_item_group
|
||||
igroup.parent_item_group = mws_settings.item_group
|
||||
igroup.insert()
|
||||
|
||||
item.append("item_defaults", {'company':mws_settings.company})
|
||||
|
||||
item.insert(ignore_permissions=True)
|
||||
create_item_price(amazon_item_json, item.item_code)
|
||||
|
||||
return item.name
|
||||
|
||||
def create_manufacturer(amazon_item_json):
|
||||
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer:
|
||||
return None
|
||||
|
||||
existing_manufacturer = frappe.db.get_value("Manufacturer",
|
||||
filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer})
|
||||
|
||||
if not existing_manufacturer:
|
||||
manufacturer = frappe.new_doc("Manufacturer")
|
||||
manufacturer.short_name = amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer
|
||||
manufacturer.insert()
|
||||
return manufacturer.short_name
|
||||
else:
|
||||
return existing_manufacturer
|
||||
|
||||
def create_brand(amazon_item_json):
|
||||
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand:
|
||||
return None
|
||||
|
||||
existing_brand = frappe.db.get_value("Brand",
|
||||
filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand})
|
||||
if not existing_brand:
|
||||
brand = frappe.new_doc("Brand")
|
||||
brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand
|
||||
brand.insert()
|
||||
return brand.brand
|
||||
else:
|
||||
return existing_brand
|
||||
|
||||
def create_item_price(amazon_item_json, item_code):
|
||||
item_price = frappe.new_doc("Item Price")
|
||||
item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list")
|
||||
if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes):
|
||||
item_price.price_list_rate = 0
|
||||
else:
|
||||
item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount
|
||||
|
||||
item_price.item_code = item_code
|
||||
item_price.insert()
|
||||
|
||||
#Get and create Orders
|
||||
def get_orders(after_date):
|
||||
try:
|
||||
orders = get_orders_instance()
|
||||
statuses = ["PartiallyShipped", "Unshipped", "Shipped", "Canceled"]
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
market_place_list = return_as_list(mws_settings.market_place_id)
|
||||
|
||||
orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list,
|
||||
fulfillment_channels=["MFN", "AFN"],
|
||||
lastupdatedafter=after_date,
|
||||
orderstatus=statuses,
|
||||
max_results='50')
|
||||
|
||||
while True:
|
||||
orders_list = []
|
||||
|
||||
if "Order" in orders_response.parsed.Orders:
|
||||
orders_list = return_as_list(orders_response.parsed.Orders.Order)
|
||||
|
||||
if len(orders_list) == 0:
|
||||
break
|
||||
|
||||
for order in orders_list:
|
||||
create_sales_order(order, after_date)
|
||||
|
||||
if not "NextToken" in orders_response.parsed:
|
||||
break
|
||||
|
||||
next_token = orders_response.parsed.NextToken
|
||||
orders_response = call_mws_method(orders.list_orders_by_next_token, next_token)
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(title="get_orders", message=e)
|
||||
|
||||
def get_orders_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
orders = mws.Orders(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region= mws_settings.region,
|
||||
domain= mws_settings.domain,
|
||||
version="2013-09-01"
|
||||
)
|
||||
|
||||
return orders
|
||||
|
||||
def create_sales_order(order_json,after_date):
|
||||
customer_name = create_customer(order_json)
|
||||
create_address(order_json, customer_name)
|
||||
|
||||
market_place_order_id = order_json.AmazonOrderId
|
||||
|
||||
so = frappe.db.get_value("Sales Order",
|
||||
filters={"amazon_order_id": market_place_order_id},
|
||||
fieldname="name")
|
||||
|
||||
taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges")
|
||||
|
||||
if so:
|
||||
return
|
||||
|
||||
if not so:
|
||||
items = get_order_items(market_place_order_id)
|
||||
delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d")
|
||||
transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d")
|
||||
|
||||
so = frappe.get_doc({
|
||||
"doctype": "Sales Order",
|
||||
"naming_series": "SO-",
|
||||
"amazon_order_id": market_place_order_id,
|
||||
"marketplace_id": order_json.MarketplaceId,
|
||||
"customer": customer_name,
|
||||
"delivery_date": delivery_date,
|
||||
"transaction_date": transaction_date,
|
||||
"items": items,
|
||||
"company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company")
|
||||
})
|
||||
|
||||
try:
|
||||
if taxes_and_charges:
|
||||
charges_and_fees = get_charges_and_fees(market_place_order_id)
|
||||
for charge in charges_and_fees.get("charges"):
|
||||
so.append('taxes', charge)
|
||||
|
||||
for fee in charges_and_fees.get("fees"):
|
||||
so.append('taxes', fee)
|
||||
|
||||
so.insert(ignore_permissions=True)
|
||||
so.submit()
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
frappe.log_error(message=traceback.format_exc(), title="Create Sales Order")
|
||||
|
||||
def create_customer(order_json):
|
||||
order_customer_name = ""
|
||||
|
||||
if not("BuyerName" in order_json):
|
||||
order_customer_name = "Buyer - " + order_json.AmazonOrderId
|
||||
else:
|
||||
order_customer_name = order_json.BuyerName
|
||||
|
||||
existing_customer_name = frappe.db.get_value("Customer",
|
||||
filters={"name": order_customer_name}, fieldname="name")
|
||||
|
||||
if existing_customer_name:
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Customer"],
|
||||
["Dynamic Link", "link_name", "=", existing_customer_name],
|
||||
["Dynamic Link", "parenttype", "=", "Contact"]
|
||||
]
|
||||
|
||||
existing_contacts = frappe.get_list("Contact", filters)
|
||||
|
||||
if existing_contacts:
|
||||
pass
|
||||
else:
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.first_name = order_customer_name
|
||||
new_contact.append('links', {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": existing_customer_name
|
||||
})
|
||||
new_contact.insert()
|
||||
|
||||
return existing_customer_name
|
||||
else:
|
||||
mws_customer_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
new_customer = frappe.new_doc("Customer")
|
||||
new_customer.customer_name = order_customer_name
|
||||
new_customer.customer_group = mws_customer_settings.customer_group
|
||||
new_customer.territory = mws_customer_settings.territory
|
||||
new_customer.customer_type = mws_customer_settings.customer_type
|
||||
new_customer.save()
|
||||
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.first_name = order_customer_name
|
||||
new_contact.append('links', {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": new_customer.name
|
||||
})
|
||||
|
||||
new_contact.insert()
|
||||
|
||||
return new_customer.name
|
||||
|
||||
def create_address(amazon_order_item_json, customer_name):
|
||||
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Customer"],
|
||||
["Dynamic Link", "link_name", "=", customer_name],
|
||||
["Dynamic Link", "parenttype", "=", "Address"]
|
||||
]
|
||||
|
||||
existing_address = frappe.get_list("Address", filters)
|
||||
|
||||
if not("ShippingAddress" in amazon_order_item_json):
|
||||
return None
|
||||
else:
|
||||
make_address = frappe.new_doc("Address")
|
||||
|
||||
if "AddressLine1" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.address_line1 = amazon_order_item_json.ShippingAddress.AddressLine1
|
||||
else:
|
||||
make_address.address_line1 = "Not Provided"
|
||||
|
||||
if "City" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.city = amazon_order_item_json.ShippingAddress.City
|
||||
else:
|
||||
make_address.city = "Not Provided"
|
||||
|
||||
if "StateOrRegion" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.state = amazon_order_item_json.ShippingAddress.StateOrRegion
|
||||
|
||||
if "PostalCode" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.pincode = amazon_order_item_json.ShippingAddress.PostalCode
|
||||
|
||||
for address in existing_address:
|
||||
address_doc = frappe.get_doc("Address", address["name"])
|
||||
if (address_doc.address_line1 == make_address.address_line1 and
|
||||
address_doc.pincode == make_address.pincode):
|
||||
return address
|
||||
|
||||
make_address.append("links", {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": customer_name
|
||||
})
|
||||
make_address.address_type = "Shipping"
|
||||
make_address.insert()
|
||||
|
||||
def get_order_items(market_place_order_id):
|
||||
mws_orders = get_orders_instance()
|
||||
|
||||
order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id)
|
||||
final_order_items = []
|
||||
|
||||
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
|
||||
|
||||
warehouse = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "warehouse")
|
||||
|
||||
while True:
|
||||
for order_item in order_items_list:
|
||||
|
||||
if not "ItemPrice" in order_item:
|
||||
price = 0
|
||||
else:
|
||||
price = order_item.ItemPrice.Amount
|
||||
|
||||
final_order_items.append({
|
||||
"item_code": get_item_code(order_item),
|
||||
"item_name": order_item.SellerSKU,
|
||||
"description": order_item.Title,
|
||||
"rate": price,
|
||||
"qty": order_item.QuantityOrdered,
|
||||
"stock_uom": "Nos",
|
||||
"warehouse": warehouse,
|
||||
"conversion_factor": "1.0"
|
||||
})
|
||||
|
||||
if not "NextToken" in order_items_response.parsed:
|
||||
break
|
||||
|
||||
next_token = order_items_response.parsed.NextToken
|
||||
|
||||
order_items_response = call_mws_method(mws_orders.list_order_items_by_next_token, next_token)
|
||||
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
|
||||
|
||||
return final_order_items
|
||||
|
||||
def get_item_code(order_item):
|
||||
sku = order_item.SellerSKU
|
||||
item_code = frappe.db.get_value("Item", {"item_code": sku}, "item_code")
|
||||
if item_code:
|
||||
return item_code
|
||||
|
||||
def get_charges_and_fees(market_place_order_id):
|
||||
finances = get_finances_instance()
|
||||
|
||||
charges_fees = {"charges":[], "fees":[]}
|
||||
|
||||
response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id)
|
||||
|
||||
shipment_event_list = return_as_list(response.parsed.FinancialEvents.ShipmentEventList)
|
||||
|
||||
for shipment_event in shipment_event_list:
|
||||
if shipment_event:
|
||||
shipment_item_list = return_as_list(shipment_event.ShipmentEvent.ShipmentItemList.ShipmentItem)
|
||||
|
||||
for shipment_item in shipment_item_list:
|
||||
charges, fees = [], []
|
||||
|
||||
if 'ItemChargeList' in shipment_item.keys():
|
||||
charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent)
|
||||
|
||||
if 'ItemFeeList' in shipment_item.keys():
|
||||
fees = return_as_list(shipment_item.ItemFeeList.FeeComponent)
|
||||
|
||||
for charge in charges:
|
||||
if(charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0:
|
||||
charge_account = get_account(charge.ChargeType)
|
||||
charges_fees.get("charges").append({
|
||||
"charge_type":"Actual",
|
||||
"account_head": charge_account,
|
||||
"tax_amount": charge.ChargeAmount.CurrencyAmount,
|
||||
"description": charge.ChargeType + " for " + shipment_item.SellerSKU
|
||||
})
|
||||
|
||||
for fee in fees:
|
||||
if float(fee.FeeAmount.CurrencyAmount) != 0:
|
||||
fee_account = get_account(fee.FeeType)
|
||||
charges_fees.get("fees").append({
|
||||
"charge_type":"Actual",
|
||||
"account_head": fee_account,
|
||||
"tax_amount": fee.FeeAmount.CurrencyAmount,
|
||||
"description": fee.FeeType + " for " + shipment_item.SellerSKU
|
||||
})
|
||||
|
||||
return charges_fees
|
||||
|
||||
def get_finances_instance():
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
finances = mws.Finances(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region= mws_settings.region,
|
||||
domain= mws_settings.domain,
|
||||
version="2015-05-01"
|
||||
)
|
||||
|
||||
return finances
|
||||
|
||||
def get_account(name):
|
||||
existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)})
|
||||
account_name = existing_account
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
if not existing_account:
|
||||
try:
|
||||
new_account = frappe.new_doc("Account")
|
||||
new_account.account_name = "Amazon {0}".format(name)
|
||||
new_account.company = mws_settings.company
|
||||
new_account.parent_account = mws_settings.market_place_account_group
|
||||
new_account.insert(ignore_permissions=True)
|
||||
account_name = new_account.name
|
||||
except Exception as e:
|
||||
frappe.log_error(message=e, title="Create Account")
|
||||
|
||||
return account_name
|
@ -1,651 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Basic interface to Amazon MWS
|
||||
# Based on http://code.google.com/p/amazon-mws-python
|
||||
# Extended to include finances object
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from erpnext.erpnext_integrations.doctype.amazon_mws_settings import xml_utils
|
||||
|
||||
try:
|
||||
from xml.etree.ElementTree import ParseError as XMLError
|
||||
except ImportError:
|
||||
from xml.parsers.expat import ExpatError as XMLError
|
||||
|
||||
from time import gmtime, strftime
|
||||
|
||||
from requests import request
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
__all__ = [
|
||||
'Feeds',
|
||||
'Inventory',
|
||||
'MWSError',
|
||||
'Reports',
|
||||
'Orders',
|
||||
'Products',
|
||||
'Recommendations',
|
||||
'Sellers',
|
||||
'Finances'
|
||||
]
|
||||
|
||||
# See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8
|
||||
# for a list of the end points and marketplace IDs
|
||||
|
||||
MARKETPLACES = {
|
||||
"CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
|
||||
"US": "https://mws.amazonservices.com", #ATVPDKIKX0DER",
|
||||
"DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
|
||||
"ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
|
||||
"FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
|
||||
"IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV
|
||||
"IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
|
||||
"UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
|
||||
"JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528
|
||||
"CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
|
||||
"AE": " https://mws.amazonservices.ae", #A2VIGQ35RCS4UG
|
||||
"MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8
|
||||
"BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC
|
||||
}
|
||||
|
||||
|
||||
class MWSError(Exception):
|
||||
"""
|
||||
Main MWS Exception class
|
||||
"""
|
||||
# Allows quick access to the response object.
|
||||
# Do not rely on this attribute, always check if its not None.
|
||||
response = None
|
||||
|
||||
def calc_md5(string):
|
||||
"""Calculates the MD5 encryption for the given string
|
||||
"""
|
||||
md = hashlib.md5()
|
||||
md.update(string)
|
||||
return base64.encodebytes(md.digest()).decode().strip()
|
||||
|
||||
|
||||
|
||||
def remove_empty(d):
|
||||
"""
|
||||
Helper function that removes all keys from a dictionary (d),
|
||||
that have an empty value.
|
||||
"""
|
||||
for key in list(d):
|
||||
if not d[key]:
|
||||
del d[key]
|
||||
return d
|
||||
|
||||
def remove_namespace(xml):
|
||||
xml = xml.decode('utf-8')
|
||||
regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)')
|
||||
return regex.sub('', xml)
|
||||
|
||||
class DictWrapper(object):
|
||||
def __init__(self, xml, rootkey=None):
|
||||
self.original = xml
|
||||
self._rootkey = rootkey
|
||||
self._mydict = xml_utils.xml2dict().fromstring(remove_namespace(xml))
|
||||
self._response_dict = self._mydict.get(list(self._mydict)[0], self._mydict)
|
||||
|
||||
@property
|
||||
def parsed(self):
|
||||
if self._rootkey:
|
||||
return self._response_dict.get(self._rootkey)
|
||||
else:
|
||||
return self._response_dict
|
||||
|
||||
class DataWrapper(object):
|
||||
"""
|
||||
Text wrapper in charge of validating the hash sent by Amazon.
|
||||
"""
|
||||
def __init__(self, data, header):
|
||||
self.original = data
|
||||
if 'content-md5' in header:
|
||||
hash_ = calc_md5(self.original)
|
||||
if header['content-md5'] != hash_:
|
||||
raise MWSError("Wrong Contentlength, maybe amazon error...")
|
||||
|
||||
@property
|
||||
def parsed(self):
|
||||
return self.original
|
||||
|
||||
class MWS(object):
|
||||
""" Base Amazon API class """
|
||||
|
||||
# This is used to post/get to the different uris used by amazon per api
|
||||
# ie. /Orders/2011-01-01
|
||||
# All subclasses must define their own URI only if needed
|
||||
URI = "/"
|
||||
|
||||
# The API version varies in most amazon APIs
|
||||
VERSION = "2009-01-01"
|
||||
|
||||
# There seem to be some xml namespace issues. therefore every api subclass
|
||||
# is recommended to define its namespace, so that it can be referenced
|
||||
# like so AmazonAPISubclass.NS.
|
||||
# For more information see http://stackoverflow.com/a/8719461/389453
|
||||
NS = ''
|
||||
|
||||
# Some APIs are available only to either a "Merchant" or "Seller"
|
||||
# the type of account needs to be sent in every call to the amazon MWS.
|
||||
# This constant defines the exact name of the parameter Amazon expects
|
||||
# for the specific API being used.
|
||||
# All subclasses need to define this if they require another account type
|
||||
# like "Merchant" in which case you define it like so.
|
||||
# ACCOUNT_TYPE = "Merchant"
|
||||
# Which is the name of the parameter for that specific account type.
|
||||
ACCOUNT_TYPE = "SellerId"
|
||||
|
||||
def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""):
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.account_id = account_id
|
||||
self.version = version or self.VERSION
|
||||
self.uri = uri or self.URI
|
||||
|
||||
if domain:
|
||||
self.domain = domain
|
||||
elif region in MARKETPLACES:
|
||||
self.domain = MARKETPLACES[region]
|
||||
else:
|
||||
error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % {
|
||||
"marketplaces" : ', '.join(MARKETPLACES.keys()),
|
||||
"region" : region,
|
||||
}
|
||||
raise MWSError(error_msg)
|
||||
|
||||
def make_request(self, extra_data, method="GET", **kwargs):
|
||||
"""Make request to Amazon MWS API with these parameters
|
||||
"""
|
||||
|
||||
# Remove all keys with an empty value because
|
||||
# Amazon's MWS does not allow such a thing.
|
||||
extra_data = remove_empty(extra_data)
|
||||
|
||||
params = {
|
||||
'AWSAccessKeyId': self.access_key,
|
||||
self.ACCOUNT_TYPE: self.account_id,
|
||||
'SignatureVersion': '2',
|
||||
'Timestamp': self.get_timestamp(),
|
||||
'Version': self.version,
|
||||
'SignatureMethod': 'HmacSHA256',
|
||||
}
|
||||
params.update(extra_data)
|
||||
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
|
||||
signature = self.calc_signature(method, request_description)
|
||||
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
|
||||
headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'}
|
||||
headers.update(kwargs.get('extra_headers', {}))
|
||||
|
||||
try:
|
||||
# Some might wonder as to why i don't pass the params dict as the params argument to request.
|
||||
# My answer is, here i have to get the url parsed string of params in order to sign it, so
|
||||
# if i pass the params dict as params to request, request will repeat that step because it will need
|
||||
# to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :).
|
||||
response = request(method, url, data=kwargs.get('body', ''), headers=headers)
|
||||
response.raise_for_status()
|
||||
# When retrieving data from the response object,
|
||||
# be aware that response.content returns the content in bytes while response.text calls
|
||||
# response.content and converts it to unicode.
|
||||
data = response.content
|
||||
|
||||
# I do not check the headers to decide which content structure to server simply because sometimes
|
||||
# Amazon's MWS API returns XML error responses with "text/plain" as the Content-Type.
|
||||
try:
|
||||
parsed_response = DictWrapper(data, extra_data.get("Action") + "Result")
|
||||
except XMLError:
|
||||
parsed_response = DataWrapper(data, response.headers)
|
||||
|
||||
except HTTPError as e:
|
||||
error = MWSError(str(e))
|
||||
error.response = e.response
|
||||
raise error
|
||||
|
||||
# Store the response object in the parsed_response for quick access
|
||||
parsed_response.response = response
|
||||
return parsed_response
|
||||
|
||||
def get_service_status(self):
|
||||
"""
|
||||
Returns a GREEN, GREEN_I, YELLOW or RED status.
|
||||
Depending on the status/availability of the API its being called from.
|
||||
"""
|
||||
|
||||
return self.make_request(extra_data=dict(Action='GetServiceStatus'))
|
||||
|
||||
def calc_signature(self, method, request_description):
|
||||
"""Calculate MWS signature to interface with Amazon
|
||||
"""
|
||||
sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description
|
||||
sig_data = sig_data.encode('utf-8')
|
||||
secret_key = self.secret_key.encode('utf-8')
|
||||
digest = hmac.new(secret_key, sig_data, hashlib.sha256).digest()
|
||||
return base64.b64encode(digest).decode('utf-8')
|
||||
|
||||
def get_timestamp(self):
|
||||
"""
|
||||
Returns the current timestamp in proper format.
|
||||
"""
|
||||
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
|
||||
|
||||
def enumerate_param(self, param, values):
|
||||
"""
|
||||
Builds a dictionary of an enumerated parameter.
|
||||
Takes any iterable and returns a dictionary.
|
||||
ie.
|
||||
enumerate_param('MarketplaceIdList.Id', (123, 345, 4343))
|
||||
returns
|
||||
{
|
||||
MarketplaceIdList.Id.1: 123,
|
||||
MarketplaceIdList.Id.2: 345,
|
||||
MarketplaceIdList.Id.3: 4343
|
||||
}
|
||||
"""
|
||||
params = {}
|
||||
if values is not None:
|
||||
if not param.endswith('.'):
|
||||
param = "%s." % param
|
||||
for num, value in enumerate(values):
|
||||
params['%s%d' % (param, (num + 1))] = value
|
||||
return params
|
||||
|
||||
|
||||
class Feeds(MWS):
|
||||
""" Amazon MWS Feeds API """
|
||||
|
||||
ACCOUNT_TYPE = "Merchant"
|
||||
|
||||
def submit_feed(self, feed, feed_type, marketplaceids=None,
|
||||
content_type="text/xml", purge='false'):
|
||||
"""
|
||||
Uploads a feed ( xml or .tsv ) to the seller's inventory.
|
||||
Can be used for creating/updating products on Amazon.
|
||||
"""
|
||||
data = dict(Action='SubmitFeed',
|
||||
FeedType=feed_type,
|
||||
PurgeAndReplace=purge)
|
||||
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
|
||||
md = calc_md5(feed)
|
||||
return self.make_request(data, method="POST", body=feed,
|
||||
extra_headers={'Content-MD5': md, 'Content-Type': content_type})
|
||||
|
||||
def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None,
|
||||
processingstatuses=None, fromdate=None, todate=None):
|
||||
"""
|
||||
Returns a list of all feed submissions submitted in the previous 90 days.
|
||||
That match the query parameters.
|
||||
"""
|
||||
|
||||
data = dict(Action='GetFeedSubmissionList',
|
||||
MaxCount=max_count,
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate,)
|
||||
data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids))
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_submission_list_by_next_token(self, token):
|
||||
data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetFeedSubmissionCount',
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate)
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None):
|
||||
data = dict(Action='CancelFeedSubmissions',
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate)
|
||||
data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids))
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_feed_submission_result(self, feedid):
|
||||
data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid)
|
||||
return self.make_request(data)
|
||||
|
||||
class Reports(MWS):
|
||||
""" Amazon MWS Reports API """
|
||||
|
||||
ACCOUNT_TYPE = "Merchant"
|
||||
|
||||
## REPORTS ###
|
||||
|
||||
def get_report(self, report_id):
|
||||
data = dict(Action='GetReport', ReportId=report_id)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportCount',
|
||||
Acknowledged=acknowledged,
|
||||
AvailableFromDate=fromdate,
|
||||
AvailableToDate=todate)
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None,
|
||||
fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportList',
|
||||
Acknowledged=acknowledged,
|
||||
AvailableFromDate=fromdate,
|
||||
AvailableToDate=todate,
|
||||
MaxCount=max_count)
|
||||
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_list_by_next_token(self, token):
|
||||
data = dict(Action='GetReportListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportRequestCount',
|
||||
RequestedFromDate=fromdate,
|
||||
RequestedToDate=todate)
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
|
||||
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_list(self, requestids=(), types=(), processingstatuses=(),
|
||||
max_count=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportRequestList',
|
||||
MaxCount=max_count,
|
||||
RequestedFromDate=fromdate,
|
||||
RequestedToDate=todate)
|
||||
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_list_by_next_token(self, token):
|
||||
data = dict(Action='GetReportRequestListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()):
|
||||
data = dict(Action='RequestReport',
|
||||
ReportType=report_type,
|
||||
StartDate=start_date,
|
||||
EndDate=end_date)
|
||||
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
|
||||
return self.make_request(data)
|
||||
|
||||
### ReportSchedule ###
|
||||
|
||||
def get_report_schedule_list(self, types=()):
|
||||
data = dict(Action='GetReportScheduleList')
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_schedule_count(self, types=()):
|
||||
data = dict(Action='GetReportScheduleCount')
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Orders(MWS):
|
||||
""" Amazon Orders API """
|
||||
|
||||
URI = "/Orders/2013-09-01"
|
||||
VERSION = "2013-09-01"
|
||||
NS = '{https://mws.amazonservices.com/Orders/2011-01-01}'
|
||||
|
||||
def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None,
|
||||
lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(),
|
||||
payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'):
|
||||
|
||||
data = dict(Action='ListOrders',
|
||||
CreatedAfter=created_after,
|
||||
CreatedBefore=created_before,
|
||||
LastUpdatedAfter=lastupdatedafter,
|
||||
LastUpdatedBefore=lastupdatedbefore,
|
||||
BuyerEmail=buyer_email,
|
||||
SellerOrderId=seller_orderid,
|
||||
MaxResultsPerPage=max_results,
|
||||
)
|
||||
data.update(self.enumerate_param('OrderStatus.Status.', orderstatus))
|
||||
data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids))
|
||||
data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels))
|
||||
data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods))
|
||||
return self.make_request(data)
|
||||
|
||||
def list_orders_by_next_token(self, token):
|
||||
data = dict(Action='ListOrdersByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_order(self, amazon_order_ids):
|
||||
data = dict(Action='GetOrder')
|
||||
data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids))
|
||||
return self.make_request(data)
|
||||
|
||||
def list_order_items(self, amazon_order_id):
|
||||
data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id)
|
||||
return self.make_request(data)
|
||||
|
||||
def list_order_items_by_next_token(self, token):
|
||||
data = dict(Action='ListOrderItemsByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Products(MWS):
|
||||
""" Amazon MWS Products API """
|
||||
|
||||
URI = '/Products/2011-10-01'
|
||||
VERSION = '2011-10-01'
|
||||
NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}'
|
||||
|
||||
def list_matching_products(self, marketplaceid, query, contextid=None):
|
||||
""" Returns a list of products and their attributes, ordered by
|
||||
relevancy, based on a search query that you specify.
|
||||
Your search query can be a phrase that describes the product
|
||||
or it can be a product identifier such as a UPC, EAN, ISBN, or JAN.
|
||||
"""
|
||||
data = dict(Action='ListMatchingProducts',
|
||||
MarketplaceId=marketplaceid,
|
||||
Query=query,
|
||||
QueryContextId=contextid)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_matching_product(self, marketplaceid, asins):
|
||||
""" Returns a list of products and their attributes, based on a list of
|
||||
ASIN values that you specify.
|
||||
"""
|
||||
data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_matching_product_for_id(self, marketplaceid, type, id):
|
||||
""" Returns a list of products and their attributes, based on a list of
|
||||
product identifier values (asin, sellersku, upc, ean, isbn and JAN)
|
||||
Added in Fourth Release, API version 2011-10-01
|
||||
"""
|
||||
data = dict(Action='GetMatchingProductForId',
|
||||
MarketplaceId=marketplaceid,
|
||||
IdType=type)
|
||||
data.update(self.enumerate_param('IdList.Id', id))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_competitive_pricing_for_sku(self, marketplaceid, skus):
|
||||
""" Returns the current competitive pricing of a product,
|
||||
based on the SellerSKU and MarketplaceId that you specify.
|
||||
"""
|
||||
data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_competitive_pricing_for_asin(self, marketplaceid, asins):
|
||||
""" Returns the current competitive pricing of a product,
|
||||
based on the ASIN and MarketplaceId that you specify.
|
||||
"""
|
||||
data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"):
|
||||
data = dict(Action='GetLowestOfferListingsForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition,
|
||||
ExcludeMe=excludeme)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"):
|
||||
data = dict(Action='GetLowestOfferListingsForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition,
|
||||
ExcludeMe=excludeme)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_product_categories_for_sku(self, marketplaceid, sku):
|
||||
data = dict(Action='GetProductCategoriesForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
SellerSKU=sku)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_product_categories_for_asin(self, marketplaceid, asin):
|
||||
data = dict(Action='GetProductCategoriesForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ASIN=asin)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_my_price_for_sku(self, marketplaceid, skus, condition=None):
|
||||
data = dict(Action='GetMyPriceForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_my_price_for_asin(self, marketplaceid, asins, condition=None):
|
||||
data = dict(Action='GetMyPriceForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Sellers(MWS):
|
||||
""" Amazon MWS Sellers API """
|
||||
|
||||
URI = '/Sellers/2011-07-01'
|
||||
VERSION = '2011-07-01'
|
||||
NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}'
|
||||
|
||||
def list_marketplace_participations(self):
|
||||
"""
|
||||
Returns a list of marketplaces a seller can participate in and
|
||||
a list of participations that include seller-specific information in that marketplace.
|
||||
The operation returns only those marketplaces where the seller's account is in an active state.
|
||||
"""
|
||||
|
||||
data = dict(Action='ListMarketplaceParticipations')
|
||||
return self.make_request(data)
|
||||
|
||||
def list_marketplace_participations_by_next_token(self, token):
|
||||
"""
|
||||
Takes a "NextToken" and returns the same information as "list_marketplace_participations".
|
||||
Based on the "NextToken".
|
||||
"""
|
||||
data = dict(Action='ListMarketplaceParticipations', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
#### Fulfillment APIs ####
|
||||
|
||||
class InboundShipments(MWS):
|
||||
URI = "/FulfillmentInboundShipment/2010-10-01"
|
||||
VERSION = '2010-10-01'
|
||||
|
||||
# To be completed
|
||||
|
||||
|
||||
class Inventory(MWS):
|
||||
""" Amazon MWS Inventory Fulfillment API """
|
||||
|
||||
URI = '/FulfillmentInventory/2010-10-01'
|
||||
VERSION = '2010-10-01'
|
||||
NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}"
|
||||
|
||||
def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'):
|
||||
""" Returns information on available inventory """
|
||||
|
||||
data = dict(Action='ListInventorySupply',
|
||||
QueryStartDateTime=datetime,
|
||||
ResponseGroup=response_group,
|
||||
)
|
||||
data.update(self.enumerate_param('SellerSkus.member.', skus))
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_inventory_supply_by_next_token(self, token):
|
||||
data = dict(Action='ListInventorySupplyByNextToken', NextToken=token)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
|
||||
class OutboundShipments(MWS):
|
||||
URI = "/FulfillmentOutboundShipment/2010-10-01"
|
||||
VERSION = "2010-10-01"
|
||||
# To be completed
|
||||
|
||||
|
||||
class Recommendations(MWS):
|
||||
|
||||
""" Amazon MWS Recommendations API """
|
||||
|
||||
URI = '/Recommendations/2013-04-01'
|
||||
VERSION = '2013-04-01'
|
||||
NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}"
|
||||
|
||||
def get_last_updated_time_for_recommendations(self, marketplaceid):
|
||||
"""
|
||||
Checks whether there are active recommendations for each category for the given marketplace, and if there are,
|
||||
returns the time when recommendations were last updated for each category.
|
||||
"""
|
||||
|
||||
data = dict(Action='GetLastUpdatedTimeForRecommendations',
|
||||
MarketplaceId=marketplaceid)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_recommendations(self, marketplaceid, recommendationcategory=None):
|
||||
"""
|
||||
Returns your active recommendations for a specific category or for all categories for a specific marketplace.
|
||||
"""
|
||||
|
||||
data = dict(Action="ListRecommendations",
|
||||
MarketplaceId=marketplaceid,
|
||||
RecommendationCategory=recommendationcategory)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_recommendations_by_next_token(self, token):
|
||||
"""
|
||||
Returns the next page of recommendations using the NextToken parameter.
|
||||
"""
|
||||
|
||||
data = dict(Action="ListRecommendationsByNextToken",
|
||||
NextToken=token)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
class Finances(MWS):
|
||||
""" Amazon Finances API"""
|
||||
URI = '/Finances/2015-05-01'
|
||||
VERSION = '2015-05-01'
|
||||
NS = "{https://mws.amazonservices.com/Finances/2015-05-01}"
|
||||
|
||||
def list_financial_events(self , posted_after=None, posted_before=None,
|
||||
amazon_order_id=None, max_results='100'):
|
||||
|
||||
data = dict(Action='ListFinancialEvents',
|
||||
PostedAfter=posted_after,
|
||||
PostedBefore=posted_before,
|
||||
AmazonOrderId=amazon_order_id,
|
||||
MaxResultsPerPage=max_results,
|
||||
)
|
||||
return self.make_request(data)
|
@ -1,2 +0,0 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
@ -1,237 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2018-07-31 05:51:41.357047",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_amazon",
|
||||
"mws_credentials",
|
||||
"seller_id",
|
||||
"aws_access_key_id",
|
||||
"mws_auth_token",
|
||||
"secret_key",
|
||||
"column_break_4",
|
||||
"market_place_id",
|
||||
"region",
|
||||
"domain",
|
||||
"section_break_13",
|
||||
"company",
|
||||
"warehouse",
|
||||
"item_group",
|
||||
"price_list",
|
||||
"column_break_17",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"customer_type",
|
||||
"market_place_account_group",
|
||||
"section_break_12",
|
||||
"after_date",
|
||||
"taxes_charges",
|
||||
"sync_products",
|
||||
"sync_orders",
|
||||
"column_break_10",
|
||||
"enable_sync",
|
||||
"max_retry_limit"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_amazon",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Amazon"
|
||||
},
|
||||
{
|
||||
"fieldname": "mws_credentials",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "MWS Credentials"
|
||||
},
|
||||
{
|
||||
"fieldname": "seller_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Seller ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "aws_access_key_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "AWS Access Key ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mws_auth_token",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "MWS Auth Token",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secret_key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Secret Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "market_place_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Market Place ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "region",
|
||||
"fieldtype": "Select",
|
||||
"label": "Region",
|
||||
"options": "\nAE\nAU\nBR\nCA\nCN\nDE\nES\nFR\nIN\nJP\nIT\nMX\nUK\nUS",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "domain",
|
||||
"fieldtype": "Data",
|
||||
"label": "Domain",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Group",
|
||||
"options": "Customer Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Customer Type",
|
||||
"options": "Individual\nCompany",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "market_place_account_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Market Place Account Group",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Amazon will synch data updated after this date",
|
||||
"fieldname": "after_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "After Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Get financial breakup of Taxes and charges data by Amazon ",
|
||||
"fieldname": "taxes_charges",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sync Taxes and Charges"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
"fieldname": "max_retry_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max Retry Limit"
|
||||
},
|
||||
{
|
||||
"description": "Always sync your products from Amazon MWS before synching the Orders details",
|
||||
"fieldname": "sync_products",
|
||||
"fieldtype": "Button",
|
||||
"label": "Sync Products",
|
||||
"options": "get_products_details"
|
||||
},
|
||||
{
|
||||
"description": "Click this button to pull your Sales Order data from Amazon MWS.",
|
||||
"fieldname": "sync_orders",
|
||||
"fieldtype": "Button",
|
||||
"label": "Sync Orders",
|
||||
"options": "get_order_details"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Check this to enable a scheduled Daily synchronization routine via scheduler",
|
||||
"fieldname": "enable_sync",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Scheduled Sync"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-07 14:26:20.174848",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Amazon MWS Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import dateutil
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_orders
|
||||
|
||||
|
||||
class AmazonMWSSettings(Document):
|
||||
def validate(self):
|
||||
if self.enable_amazon == 1:
|
||||
self.enable_sync = 1
|
||||
setup_custom_fields()
|
||||
else:
|
||||
self.enable_sync = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_products_details(self):
|
||||
if self.enable_amazon == 1:
|
||||
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_order_details(self):
|
||||
if self.enable_amazon == 1:
|
||||
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
|
||||
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date)
|
||||
|
||||
def schedule_get_order_details():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
if mws_settings.enable_sync and mws_settings.enable_amazon:
|
||||
after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d")
|
||||
get_orders(after_date = after_date)
|
||||
|
||||
def setup_custom_fields():
|
||||
custom_fields = {
|
||||
"Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code',
|
||||
fieldtype='Data', insert_after='series', read_only=1, print_hide=1)],
|
||||
"Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID',
|
||||
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields)
|
@ -1,8 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestAmazonMWSSettings(unittest.TestCase):
|
||||
pass
|
@ -1,104 +0,0 @@
|
||||
"""
|
||||
Created on Tue Jun 26 15:42:07 2012
|
||||
|
||||
Borrowed from https://github.com/timotheus/ebaysdk-python
|
||||
|
||||
@author: pierre
|
||||
"""
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class object_dict(dict):
|
||||
"""object view of dict, you can
|
||||
>>> a = object_dict()
|
||||
>>> a.fish = 'fish'
|
||||
>>> a['fish']
|
||||
'fish'
|
||||
>>> a['water'] = 'water'
|
||||
>>> a.water
|
||||
'water'
|
||||
>>> a.test = {'value': 1}
|
||||
>>> a.test2 = object_dict({'name': 'test2', 'value': 2})
|
||||
>>> a.test, a.test2.name, a.test2.value
|
||||
(1, 'test2', 2)
|
||||
"""
|
||||
def __init__(self, initd=None):
|
||||
if initd is None:
|
||||
initd = {}
|
||||
dict.__init__(self, initd)
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
||||
try:
|
||||
d = self.__getitem__(item)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
if isinstance(d, dict) and 'value' in d and len(d) == 1:
|
||||
return d['value']
|
||||
else:
|
||||
return d
|
||||
|
||||
# if value is the only key in object, you can omit it
|
||||
def __setstate__(self, item):
|
||||
return False
|
||||
|
||||
def __setattr__(self, item, value):
|
||||
self.__setitem__(item, value)
|
||||
|
||||
def getvalue(self, item, value=None):
|
||||
return self.get(item, {}).get('value', value)
|
||||
|
||||
|
||||
class xml2dict(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _parse_node(self, node):
|
||||
node_tree = object_dict()
|
||||
# Save attrs and text, hope there will not be a child with same name
|
||||
if node.text:
|
||||
node_tree.value = node.text
|
||||
for (k, v) in node.attrib.items():
|
||||
k, v = self._namespace_split(k, object_dict({'value':v}))
|
||||
node_tree[k] = v
|
||||
#Save childrens
|
||||
for child in node.getchildren():
|
||||
tag, tree = self._namespace_split(child.tag,
|
||||
self._parse_node(child))
|
||||
if tag not in node_tree: # the first time, so store it in dict
|
||||
node_tree[tag] = tree
|
||||
continue
|
||||
old = node_tree[tag]
|
||||
if not isinstance(old, list):
|
||||
node_tree.pop(tag)
|
||||
node_tree[tag] = [old] # multi times, so change old dict to a list
|
||||
node_tree[tag].append(tree) # add the new one
|
||||
|
||||
return node_tree
|
||||
|
||||
def _namespace_split(self, tag, value):
|
||||
"""
|
||||
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
|
||||
ns = http://cs.sfsu.edu/csc867/myscheduler
|
||||
name = patients
|
||||
"""
|
||||
result = re.compile(r"\{(.*)\}(.*)").search(tag)
|
||||
if result:
|
||||
value.namespace, tag = result.groups()
|
||||
|
||||
return (tag, value)
|
||||
|
||||
def parse(self, file):
|
||||
"""parse a xml file to a dict"""
|
||||
f = open(file, 'r')
|
||||
return self.fromstring(f.read())
|
||||
|
||||
def fromstring(self, s):
|
||||
"""parse a string"""
|
||||
t = ET.fromstring(s)
|
||||
root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
|
||||
return object_dict({root_tag: root_tree})
|
@ -29,17 +29,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Amazon MWS Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "Amazon MWS Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
|
@ -333,7 +333,6 @@ scheduler_events = {
|
||||
"hourly": [
|
||||
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
|
||||
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
|
@ -351,3 +351,4 @@ erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||
erpnext.patches.v13_0.update_disbursement_account
|
||||
erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v14_0.delete_amazon_mws_doctype
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0]
|
||||
if count == 0:
|
||||
frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';")
|
||||
|
||||
frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings")
|
5
erpnext/patches/v14_0/delete_amazon_mws_doctype.py
Normal file
5
erpnext/patches/v14_0/delete_amazon_mws_doctype.py
Normal file
@ -0,0 +1,5 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc("DocType", "Amazon MWS Settings", ignore_missing=True)
|
@ -343,6 +343,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
|
||||
args.conversion_factor = out.conversion_factor
|
||||
out.stock_qty = out.qty * out.conversion_factor
|
||||
args.stock_qty = out.stock_qty
|
||||
|
||||
# calculate last purchase rate
|
||||
if args.get('doctype') in purchase_doctypes:
|
||||
|
Loading…
x
Reference in New Issue
Block a user